Skip to main content

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.

The server dispatches incoming RTSP messages to your handler struct by checking which ServerHandler* interfaces it implements. Each interface defines a single method. Implement only the interfaces relevant to your use case.

Handler interfaces

Connection lifecycle

Called when a new TCP connection is accepted by the server.
func (h *myHandler) OnConnOpen(ctx *gortsplib.ServerHandlerOnConnOpenCtx) {
    // ctx.Conn — the new *ServerConn
    log.Printf("conn opened")
}
Called when a connection is closed, whether by the client, the server, or an error.
func (h *myHandler) OnConnClose(ctx *gortsplib.ServerHandlerOnConnCloseCtx) {
    // ctx.Conn  — the closed *ServerConn
    // ctx.Error — the reason for closure
    log.Printf("conn closed: %v", ctx.Error)
}

Session lifecycle

Called when a new RTSP session is created. Sessions are created lazily on the first SETUP or ANNOUNCE request.
func (h *myHandler) OnSessionOpen(ctx *gortsplib.ServerHandlerOnSessionOpenCtx) {
    // ctx.Session — the new *ServerSession
    // ctx.Conn    — the connection that created the session
    log.Printf("session opened")
}
Called when a session is closed. Use this to tear down any resources associated with the session — for example, closing a ServerStream when the publisher disconnects.
func (h *myHandler) OnSessionClose(ctx *gortsplib.ServerHandlerOnSessionCloseCtx) {
    // ctx.Session — the closed *ServerSession
    // ctx.Error   — the reason for closure
    if h.stream != nil && ctx.Session == h.publisher {
        h.stream.Close()
        h.stream = nil
    }
}

Request/response logging

Called for every raw RTSP request received on a connection, before the server processes it. Useful for logging or inspection.
func (h *myHandler) OnRequest(conn *gortsplib.ServerConn, req *base.Request) {
    log.Printf("request: %v", req.Method)
}
Called for every RTSP response sent by the server. Useful for logging or inspection.
func (h *myHandler) OnResponse(conn *gortsplib.ServerConn, res *base.Response) {
    log.Printf("response: %v", res.StatusCode)
}

RTSP methods

Called when a client sends a DESCRIBE request. Readers call this to fetch the stream description (SDP) before calling SETUP.Return the *ServerStream the client should be offered, or nil with StatusNotFound if no stream is available.
func (h *myHandler) OnDescribe(
    ctx *gortsplib.ServerHandlerOnDescribeCtx,
) (*base.Response, *gortsplib.ServerStream, error) {
    // ctx.Conn    — connection sending the request
    // ctx.Request — raw RTSP request
    // ctx.Path    — requested path
    // ctx.Query   — query string
    if h.stream == nil {
        return &base.Response{StatusCode: base.StatusNotFound}, nil, nil
    }
    return &base.Response{StatusCode: base.StatusOK}, h.stream, nil
}
Called when a publisher sends an ANNOUNCE request. This is where you create a ServerStream for the incoming publication.
func (h *myHandler) OnAnnounce(
    ctx *gortsplib.ServerHandlerOnAnnounceCtx,
) (*base.Response, error) {
    // ctx.Session     — the publisher's *ServerSession
    // ctx.Description — the SDP description sent by the publisher
    // ctx.Path        — announced path
    h.stream = &gortsplib.ServerStream{
        Server: h.server,
        Desc:   ctx.Description,
    }
    if err := h.stream.Initialize(); err != nil {
        return &base.Response{StatusCode: base.StatusInternalServerError}, err
    }
    h.publisher = ctx.Session
    return &base.Response{StatusCode: base.StatusOK}, nil
}
Called for every SETUP request. Both readers and publishers send SETUP. Use ctx.Session.State() to distinguish them:
  • ServerSessionStatePreRecord — the session is setting up a track to publish.
  • ServerSessionStateInitial or ServerSessionStatePrePlay — the session is setting up a track to read.
Return the *ServerStream for readers. For publishers, return nil.
func (h *myHandler) OnSetup(
    ctx *gortsplib.ServerHandlerOnSetupCtx,
) (*base.Response, *gortsplib.ServerStream, error) {
    // publisher setup — no stream needed
    if ctx.Session.State() == gortsplib.ServerSessionStatePreRecord {
        return &base.Response{StatusCode: base.StatusOK}, nil, nil
    }

    // reader setup — return the stream to subscribe the session
    if h.stream == nil {
        return &base.Response{StatusCode: base.StatusNotFound}, nil, nil
    }
    return &base.Response{StatusCode: base.StatusOK}, h.stream, nil
}
Called when a reader sends a PLAY request, after all tracks are set up. The server begins delivering packets to the reader.
func (h *myHandler) OnPlay(
    ctx *gortsplib.ServerHandlerOnPlayCtx,
) (*base.Response, error) {
    return &base.Response{StatusCode: base.StatusOK}, nil
}
Called when a publisher sends a RECORD request. Register packet callbacks here using ctx.Session.OnPacketRTPAny or ctx.Session.OnPacketRTP.
func (h *myHandler) OnRecord(
    ctx *gortsplib.ServerHandlerOnRecordCtx,
) (*base.Response, error) {
    ctx.Session.OnPacketRTPAny(func(medi *description.Media, _ format.Format, pkt *rtp.Packet) {
        h.stream.WritePacketRTP(medi, pkt) //nolint:errcheck
    })
    return &base.Response{StatusCode: base.StatusOK}, nil
}
Called when a reader or publisher sends a PAUSE request. Packet delivery is suspended until the next PLAY or RECORD.
func (h *myHandler) OnPause(
    ctx *gortsplib.ServerHandlerOnPauseCtx,
) (*base.Response, error) {
    return &base.Response{StatusCode: base.StatusOK}, nil
}
Called for GET_PARAMETER and SET_PARAMETER requests. These are often used by clients as keepalives.
func (h *myHandler) OnGetParameter(
    ctx *gortsplib.ServerHandlerOnGetParameterCtx,
) (*base.Response, error) {
    return &base.Response{StatusCode: base.StatusOK}, nil
}

func (h *myHandler) OnSetParameter(
    ctx *gortsplib.ServerHandlerOnSetParameterCtx,
) (*base.Response, error) {
    return &base.Response{StatusCode: base.StatusOK}, nil
}

Error and diagnostic callbacks

Called when the server detects lost RTP packets from a publisher, based on RTCP receiver reports.
func (h *myHandler) OnPacketsLost(ctx *gortsplib.ServerHandlerOnPacketsLostCtx) {
    // ctx.Session — the session with losses
    // ctx.Lost    — cumulative count of lost packets
    log.Printf("packets lost: %d", ctx.Lost)
}
Called when a non-fatal decode error occurs while processing incoming RTP packets. The session continues.
func (h *myHandler) OnDecodeError(ctx *gortsplib.ServerHandlerOnDecodeErrorCtx) {
    log.Printf("decode error: %v", ctx.Error)
}
Called when a ServerStream fails to deliver a packet to a particular reader session. The reader is disconnected automatically; this callback is informational.
func (h *myHandler) OnStreamWriteError(ctx *gortsplib.ServerHandlerOnStreamWriteErrorCtx) {
    log.Printf("write error for session: %v", ctx.Error)
}

ServerStream

ServerStream holds the stream description and distributes packets to all readers. You create one in OnAnnounce (for live relay) or before starting the server (for file serving).
// Create and initialize a stream.
stream := &gortsplib.ServerStream{
    Server: s,       // pointer to the running *Server
    Desc:   sdpDesc, // *description.Session — the SDP
}
if err := stream.Initialize(); err != nil {
    return err
}
defer stream.Close()

// Write a packet to all active readers.
stream.WritePacketRTP(media, rtpPacket)

// Write a packet with an explicit NTP timestamp.
stream.WritePacketRTPWithNTP(media, rtpPacket, time.Now())

// Write a raw RTCP packet.
stream.WritePacketRTCP(media, rtcpPacket)
Call stream.Close() when the publisher disconnects. This immediately disconnects all active readers.

Session states

ServerSession.State() returns the current state of a session. The most common use is distinguishing a publisher from a reader inside OnSetup.
StateMeaning
ServerSessionStateInitialSession exists but no tracks are set up yet.
ServerSessionStatePrePlayOne or more tracks set up, waiting for PLAY.
ServerSessionStatePlayActively receiving packets as a reader.
ServerSessionStatePreRecordOne or more tracks set up, waiting for RECORD.
ServerSessionStateRecordActively sending packets as a publisher.

Full example: relay publisher to readers

This example accepts a single publisher and forwards all incoming RTP packets to every connected reader.
server/main.go
// Package main contains an example.
package main

import (
	"log"
	"sync"

	"github.com/pion/rtp"

	"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"
)

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

func (sh *serverHandler) OnConnOpen(_ *gortsplib.ServerHandlerOnConnOpenCtx) {
	log.Printf("conn opened")
}

func (sh *serverHandler) OnConnClose(ctx *gortsplib.ServerHandlerOnConnCloseCtx) {
	log.Printf("conn closed (%v)", ctx.Error)
}

func (sh *serverHandler) OnSessionOpen(_ *gortsplib.ServerHandlerOnSessionOpenCtx) {
	log.Printf("session opened")
}

func (sh *serverHandler) OnSessionClose(ctx *gortsplib.ServerHandlerOnSessionCloseCtx) {
	log.Printf("session closed")

	sh.mutex.Lock()
	defer sh.mutex.Unlock()

	// if the session is the publisher,
	// close the stream and disconnect any reader.
	if sh.stream != nil && ctx.Session == sh.publisher {
		sh.stream.Close()
		sh.stream = nil
	}
}

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

	sh.mutex.RLock()
	defer sh.mutex.RUnlock()

	// no one is publishing yet
	if sh.stream == nil {
		return &base.Response{
			StatusCode: base.StatusNotFound,
		}, nil, nil
	}

	// send medias that are being published to the client
	return &base.Response{
		StatusCode: base.StatusOK,
	}, sh.stream, nil
}

// called when receiving an ANNOUNCE request.
func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) (*base.Response, error) {
	log.Printf("ANNOUNCE request")

	sh.mutex.Lock()
	defer sh.mutex.Unlock()

	// disconnect existing publisher
	if sh.stream != nil {
		sh.stream.Close()
		sh.publisher.Close()
	}

	// create the stream and save the publisher
	sh.stream = &gortsplib.ServerStream{
		Server: sh.server,
		Desc:   ctx.Description,
	}
	err := sh.stream.Initialize()
	if err != nil {
		panic(err)
	}
	sh.publisher = ctx.Session

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

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

	// SETUP is used by both readers and publishers. In case of publishers, just return StatusOK.
	if ctx.Session.State() == gortsplib.ServerSessionStatePreRecord {
		return &base.Response{
			StatusCode: base.StatusOK,
		}, nil, nil
	}

	sh.mutex.RLock()
	defer sh.mutex.RUnlock()

	// no one is publishing yet
	if sh.stream == nil {
		return &base.Response{
			StatusCode: base.StatusNotFound,
		}, nil, nil
	}

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

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

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

// called when receiving a RECORD request.
func (sh *serverHandler) OnRecord(ctx *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) {
	log.Printf("RECORD request")

	// called when receiving a RTP packet
	ctx.Session.OnPacketRTPAny(func(medi *description.Media, _ format.Format, pkt *rtp.Packet) {
		// route the RTP packet to all readers
		err := sh.stream.WritePacketRTP(medi, pkt)
		if err != nil {
			log.Printf("ERR: %v", err)
		}
	})

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

func main() {
	// configure the server
	h := &serverHandler{}
	h.server = &gortsplib.Server{
		Handler:           h,
		RTSPAddress:       ":8554",
		UDPRTPAddress:     ":8000",
		UDPRTCPAddress:    ":8001",
		MulticastIPRange:  "224.1.0.0/16",
		MulticastRTPPort:  8002,
		MulticastRTCPPort: 8003,
	}

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

Example: serve H264 from disk

This example creates a ServerStream before accepting connections and streams a MPEG-TS file to all readers.
server-play-format-h264-from-disk/main.go
// Package main contains an example.
package main

import (
	"log"
	"sync"

	"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"
)

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

func (sh *serverHandler) OnConnOpen(_ *gortsplib.ServerHandlerOnConnOpenCtx) {
	log.Printf("conn opened")
}

func (sh *serverHandler) OnConnClose(ctx *gortsplib.ServerHandlerOnConnCloseCtx) {
	log.Printf("conn closed (%v)", ctx.Error)
}

func (sh *serverHandler) OnSessionOpen(_ *gortsplib.ServerHandlerOnSessionOpenCtx) {
	log.Printf("session opened")
}

func (sh *serverHandler) OnSessionClose(_ *gortsplib.ServerHandlerOnSessionCloseCtx) {
	log.Printf("session closed")
}

func (sh *serverHandler) OnDescribe(
	_ *gortsplib.ServerHandlerOnDescribeCtx,
) (*base.Response, *gortsplib.ServerStream, error) {
	log.Printf("DESCRIBE request")

	sh.mutex.RLock()
	defer sh.mutex.RUnlock()

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

func (sh *serverHandler) OnSetup(
	_ *gortsplib.ServerHandlerOnSetupCtx,
) (*base.Response, *gortsplib.ServerStream, error) {
	log.Printf("SETUP request")

	sh.mutex.RLock()
	defer sh.mutex.RUnlock()

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

func (sh *serverHandler) OnPlay(_ *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {
	log.Printf("PLAY request")

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

func main() {
	h := &serverHandler{}

	// prevent clients from connecting to the server until the stream is properly set up
	h.mutex.Lock()

	// create the server
	h.server = &gortsplib.Server{
		Handler:           h,
		RTSPAddress:       ":8554",
		UDPRTPAddress:     ":8000",
		UDPRTCPAddress:    ":8001",
		MulticastIPRange:  "224.1.0.0/16",
		MulticastRTPPort:  8002,
		MulticastRTCPPort: 8003,
	}

	// start the server
	err := h.server.Start()
	if err != nil {
		panic(err)
	}
	defer h.server.Close()

	// create a RTSP description that contains a H264 format
	desc := &description.Session{
		Medias: []*description.Media{{
			Type: description.MediaTypeVideo,
			Formats: []format.Format{&format.H264{
				PayloadTyp:        96,
				PacketizationMode: 1,
			}},
		}},
	}

	// create a server stream
	h.stream = &gortsplib.ServerStream{
		Server: h.server,
		Desc:   desc,
	}
	err = h.stream.Initialize()
	if err != nil {
		panic(err)
	}
	defer h.stream.Close()

	// create file streamer
	r := &fileStreamer{stream: h.stream}
	err = r.initialize()
	if err != nil {
		panic(err)
	}
	defer r.close()

	// allow clients to connect
	h.mutex.Unlock()

	// wait until a fatal error
	log.Printf("server is ready on %s", h.server.RTSPAddress)
	panic(h.server.Wait())
}
When serving a pre-built stream (not relay), hold the mutex locked during setup so no client can send a DESCRIBE before the stream is ready. Release it once Initialize() succeeds.

Build docs developers (and LLMs) love