> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/bluenviron/gortsplib/llms.txt
> Use this file to discover all available pages before exploring further.

# Building a proxy

> Relay RTSP streams from one server to another using Client and Server together.

A proxy reads an existing RTSP stream from an upstream source and re-publishes it so that multiple downstream clients can connect. The core pattern is straightforward: a `gortsplib.Client` subscribes to the upstream server, and a `gortsplib.Server` serves that same stream to local readers. Every inbound RTP packet is forwarded to the server stream in the `OnPacketRTPAny` callback.

## Data flow

```
Upstream RTSP server
        │  RTSP/RTP (Client reads)
        ▼
  gortsplib.Client
        │  stream.WritePacketRTP()
        ▼
  gortsplib.ServerStream
        │  RTSP/RTP (Server writes)
        ▼
 Downstream readers
```

## Complete example

The proxy is split across three files that form a single `main` package.

<CodeGroup>
  ```go main.go theme={null}
  // Package main contains an example.
  package main

  import "log"

  // This example shows how to:
  // 1. create a server that serves a single stream.
  // 2. create a client, that reads an existing stream from another server or camera.
  // 3. route the stream from the client to the server, and from the server to all connected readers.

  func main() {
  	// allocate the server.
  	s := &server{}
  	s.initialize()

  	// allocate the client.
  	// allow client to use the server.
  	c := &client{server: s}
  	c.initialize()

  	// start server and wait until a fatal error
  	log.Printf("server is ready on %s", s.server.RTSPAddress)
  	panic(s.server.StartAndWait())
  }
  ```

  ```go client.go theme={null}
  package main

  import (
  	"log"
  	"time"

  	"github.com/bluenviron/gortsplib/v5"
  	"github.com/bluenviron/gortsplib/v5/pkg/base"
  	"github.com/bluenviron/gortsplib/v5/pkg/description"
  	"github.com/bluenviron/gortsplib/v5/pkg/format"
  	"github.com/pion/rtp"
  )

  const (
  	existingStream = "rtsp://myuser:mypass@x.x.x.x:8554/mystream"
  	reconnectPause = 2 * time.Second
  )

  type client struct {
  	server *server
  }

  func (c *client) initialize() {
  	// start a separated routine
  	go c.run()
  }

  func (c *client) run() {
  	for {
  		err := c.read()
  		log.Printf("ERR: %s\n", err)

  		time.Sleep(reconnectPause)
  	}
  }

  func (c *client) read() error {
  	// parse URL
  	u, err := base.ParseURL(existingStream)
  	if err != nil {
  		return err
  	}

  	rc := gortsplib.Client{
  		Scheme: u.Scheme,
  		Host:   u.Host,
  	}

  	// connect to the server
  	err = rc.Start()
  	if err != nil {
  		return err
  	}
  	defer rc.Close()

  	// find available medias
  	desc, _, err := rc.Describe(u)
  	if err != nil {
  		return err
  	}

  	// setup all medias
  	err = rc.SetupAll(desc.BaseURL, desc.Medias)
  	if err != nil {
  		return err
  	}

  	// notify the server that we are ready
  	stream := c.server.setStreamReady(desc)
  	defer c.server.setStreamUnready()

  	log.Printf("stream is ready and can be read from the server at rtsp://localhost:8554/stream\n")

  	// called when a RTP packet arrives
  	rc.OnPacketRTPAny(func(medi *description.Media, _ format.Format, pkt *rtp.Packet) {
  		// route inbound packets to the server stream
  		err2 := stream.WritePacketRTP(medi, pkt)
  		if err2 != nil {
  			log.Printf("ERR: %v", err2)
  		}
  	})

  	// start playing
  	_, err = rc.Play(nil)
  	if err != nil {
  		return err
  	}

  	// wait until a fatal error
  	return rc.Wait()
  }
  ```

  ```go server.go theme={null}
  package main

  import (
  	"log"
  	"sync"

  	"github.com/bluenviron/gortsplib/v5"
  	"github.com/bluenviron/gortsplib/v5/pkg/base"
  	"github.com/bluenviron/gortsplib/v5/pkg/description"
  )

  type server struct {
  	server *gortsplib.Server
  	mutex  sync.RWMutex
  	stream *gortsplib.ServerStream
  }

  func (s *server) initialize() {
  	// configure the server
  	s.server = &gortsplib.Server{
  		Handler:           s,
  		RTSPAddress:       ":8556",
  		UDPRTPAddress:     ":8002",
  		UDPRTCPAddress:    ":8003",
  		MulticastIPRange:  "224.1.0.0/16",
  		MulticastRTPPort:  8002,
  		MulticastRTCPPort: 8003,
  	}
  }

  // called when a connection is opened.
  func (s *server) OnConnOpen(_ *gortsplib.ServerHandlerOnConnOpenCtx) {
  	log.Printf("conn opened")
  }

  // called when a connection is closed.
  func (s *server) OnConnClose(ctx *gortsplib.ServerHandlerOnConnCloseCtx) {
  	log.Printf("conn closed (%v)", ctx.Error)
  }

  // called when a session is opened.
  func (s *server) OnSessionOpen(_ *gortsplib.ServerHandlerOnSessionOpenCtx) {
  	log.Printf("session opened")
  }

  // called when a session is closed.
  func (s *server) OnSessionClose(_ *gortsplib.ServerHandlerOnSessionCloseCtx) {
  	log.Printf("session closed")
  }

  // called when receiving a DESCRIBE request.
  func (s *server) OnDescribe(_ *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) {
  	log.Printf("DESCRIBE request")

  	s.mutex.RLock()
  	defer s.mutex.RUnlock()

  	// stream is not available yet
  	if s.stream == nil {
  		return &base.Response{
  			StatusCode: base.StatusNotFound,
  		}, nil, nil
  	}

  	return &base.Response{
  		StatusCode: base.StatusOK,
  	}, s.stream, nil
  }

  // called when receiving a SETUP request.
  func (s *server) OnSetup(_ *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {
  	log.Printf("SETUP request")

  	s.mutex.RLock()
  	defer s.mutex.RUnlock()

  	// stream is not available yet
  	if s.stream == nil {
  		return &base.Response{
  			StatusCode: base.StatusNotFound,
  		}, nil, nil
  	}

  	return &base.Response{
  		StatusCode: base.StatusOK,
  	}, s.stream, nil
  }

  // called when receiving a PLAY request.
  func (s *server) OnPlay(_ *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {
  	log.Printf("PLAY request")

  	return &base.Response{
  		StatusCode: base.StatusOK,
  	}, nil
  }

  func (s *server) setStreamReady(desc *description.Session) *gortsplib.ServerStream {
  	s.mutex.Lock()
  	defer s.mutex.Unlock()

  	s.stream = &gortsplib.ServerStream{
  		Server: s.server,
  		Desc:   desc,
  	}

  	err := s.stream.Initialize()
  	if err != nil {
  		panic(err)
  	}

  	return s.stream
  }

  func (s *server) setStreamUnready() {
  	s.mutex.Lock()
  	defer s.mutex.Unlock()

  	s.stream.Close()
  	s.stream = nil
  }
  ```
</CodeGroup>

## How it works

<Steps>
  <Step title="Server initializes">
    `server.initialize()` configures a `gortsplib.Server` listening on `:8556`. It supports UDP unicast, UDP multicast, and TCP transport. No stream exists yet — `DESCRIBE` and `SETUP` return `404` until the client connects upstream.
  </Step>

  <Step title="Client connects upstream">
    `client.initialize()` starts a goroutine that calls `read()` in a loop, reconnecting after `reconnectPause` on any error. Inside `read()`, a `gortsplib.Client` connects to the upstream URL, calls `Describe` to fetch the SDP, and calls `SetupAll` to set up every media track.
  </Step>

  <Step title="Stream becomes available">
    Once setup succeeds, `setStreamReady(desc)` creates a `ServerStream` using the upstream `description.Session` directly. The same media descriptions are forwarded to downstream readers, so the SDP is an exact mirror of the upstream.
  </Step>

  <Step title="Packets are routed">
    `OnPacketRTPAny` fires for every inbound RTP packet on any media. The callback calls `stream.WritePacketRTP(medi, pkt)`, which fans the packet out to all connected downstream readers.
  </Step>

  <Step title="Reconnection on failure">
    When the upstream connection drops, `read()` returns an error. The loop sleeps for `reconnectPause` and retries. `setStreamUnready()` closes the `ServerStream` so that downstream readers receive an appropriate error.
  </Step>
</Steps>

## Transport selection

By default `gortsplib.Client` negotiates transport automatically (UDP preferred, falls back to TCP). In a proxy you may want to force TCP to avoid NAT/firewall issues with UDP on the upstream leg:

```go theme={null}
rc := gortsplib.Client{
	Scheme:    u.Scheme,
	Host:      u.Host,
	Transport: func() *gortsplib.Transport {
		t := gortsplib.TransportTCP
		return &t
	}(),
}
```

The downstream server independently negotiates transport with each reader, so you can mix TCP upstream with UDP downstream.

## Tunneling

The server supports RTSP-over-HTTP tunneling out of the box — set `RTSPOverHTTPAddress` on the `gortsplib.Server` to enable it. RTSP-over-WebSocket tunneling can be enabled with `RTSPOverWebSocketAddress`.

<Warning>
  Do not modify the `Timestamp` field of RTP packets when proxying. The downstream SDP clock rates are copied verbatim from upstream, so altering timestamps will cause receivers to compute incorrect presentation times or break lip-sync between audio and video tracks.
</Warning>

## Back-channel proxy

When the upstream source is an ONVIF camera with a back channel (two-way audio), the proxy must also route RTP packets from downstream readers back to the upstream client. Set `RequestBackChannels: true` on the client and wire up `OnPacketRTPAny` on the server's `OnPlay` handler to call `rc.WritePacketRTP` back to the upstream. See the `examples/proxy-backchannel` directory for the complete implementation.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.