Skip to main content
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.
// 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())
}

How it works

1

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

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

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

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

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.

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

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.