actioncam/rtsp/RTSPServer.go

252 lines
7.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package rtsp
import (
"context"
"fmt"
"log"
"net"
"sync"
"time"
"github.com/pion/rtp"
"github.com/bluenviron/gortsplib/v4"
"github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/format"
// We keep this import so the CreateServer() signature matches the original
// repo (actioncam.go calls rtsp.CreateServer(ctx, host, port, camera)).
"github.com/jonas-koeritz/actioncam/libipcamera"
)
// This is where your RTP relay should send H.264 packets.
// The original project uses port 5220 for the preview stream.
const defaultRTPInPort = 5220
// Server wraps the gortsplib server + the in-memory stream.
type Server struct {
gs *gortsplib.Server
stream *gortsplib.ServerStream
media *description.Media
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
rtpConn *net.UDPConn
}
// handler implements the gortsplib ServerHandler* interfaces.
type handler struct {
srv *Server
}
// --- RTSP callbacks (multi-client capable) ---
// OnDescribe: clients ask what the stream looks like (SDP).
func (h *handler) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) {
log.Printf("RTSP DESCRIBE from %v path=%s", ctx.Conn.NetConn().RemoteAddr(), ctx.Path)
return &base.Response{
StatusCode: base.StatusOK,
}, h.srv.stream, nil
}
// OnSetup: client wants to SUBSCRIBE to the existing stream.
func (h *handler) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {
log.Printf("RTSP SETUP from %v path=%s", ctx.Conn.NetConn().RemoteAddr(), ctx.Path)
return &base.Response{
StatusCode: base.StatusOK,
}, h.srv.stream, nil
}
// OnPlay: client starts receiving packets.
func (h *handler) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {
log.Printf("RTSP PLAY from %v path=%s", ctx.Conn.NetConn().RemoteAddr(), ctx.Path)
return &base.Response{
StatusCode: base.StatusOK,
}, nil
}
// (Optional) For logging / debugging:
func (h *handler) OnConnOpen(ctx *gortsplib.ServerHandlerOnConnOpenCtx) {
log.Printf("RTSP connection opened: %v", ctx.Conn.NetConn().RemoteAddr())
}
func (h *handler) OnConnClose(ctx *gortsplib.ServerHandlerOnConnCloseCtx) {
log.Printf("RTSP connection closed: %v (err=%v)", ctx.Conn.NetConn().RemoteAddr(), ctx.Error)
}
// --- Public API ---
// CreateServer creates and starts a RTSP server that serves *one* H.264 video
// stream, backed by a single UDP source (127.0.0.1:defaultRTPInPort).
//
// It keeps the original function signature from the repo:
// rtsp.CreateServer(applicationContext, host, port, camera)
// but the camera is NOT used directly here youre expected to start the
// RTP relay separately so that it forwards H.264 RTP packets into
// 127.0.0.1:defaultRTPInPort.
//
// Multiple RTSP clients are automatically supported: all of them read from
// the same gortsplib.ServerStream.
func CreateServer(parentCtx context.Context, host string, port int, _ *libipcamera.Camera) (*Server, error) {
ctx, cancel := context.WithCancel(parentCtx)
// Build an in-memory H.264 stream description suitable for gortsplib.
stream, media, err := newH264Stream()
if err != nil {
cancel()
return nil, fmt.Errorf("create H264 stream: %w", err)
}
// Listen for incoming RTP packets from the RTP relay.
rtpAddr := &net.UDPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: defaultRTPInPort,
}
rtpConn, err := net.ListenUDP("udp", rtpAddr)
if err != nil {
cancel()
return nil, fmt.Errorf("listen udp %v: %w", rtpAddr, err)
}
srv := &Server{
stream: stream,
media: media,
ctx: ctx,
cancel: cancel,
rtpConn: rtpConn,
}
h := &handler{srv: srv}
// Configure gortsplib server.
gs := &gortsplib.Server{
Handler: h,
RTSPAddress: fmt.Sprintf("%s:%d", host, port),
// If you also want UDP transport for clients, set UDPRTPAddress/UDPRTCPAddress here.
// For most use cases, TCP (interleaved over RTSP) is fine.
}
srv.gs = gs
// Start the UDP → RTSP pump.
srv.wg.Add(1)
go srv.rtpPump()
// Start RTSP server in the background.
go func() {
log.Printf("RTSP server listening on rtsp://%s:%d/ (gortsplib)", host, port)
if err := gs.StartAndWait(); err != nil {
log.Printf("RTSP server stopped with error: %v", err)
}
cancel()
}()
return srv, nil
}
// Close shuts down the RTSP server and the RTP pump.
func (s *Server) Close() {
s.cancel()
if s.gs != nil {
s.gs.Close()
}
if s.rtpConn != nil {
_ = s.rtpConn.Close()
}
s.wg.Wait()
if s.stream != nil {
s.stream.Close()
}
}
// --- internals ---
// newH264Stream builds a single-video-track ServerStream with a valid clock rate.
//
// NOTE:
// * SPS / PPS here are "generic valid" values, not tuned to your camera.
// * For best results, you can parse the real SPS/PPS from your camera.sdp and
// drop them in here.
func newH264Stream() (*gortsplib.ServerStream, *description.Media, error) {
// Generic baseline-profile SPS/PPS. They just need to be *valid* so that
// the library knows the clock rate and doesn't panic ("non-positive interval
// for NewTicker") :contentReference[oaicite:1]{index=1}
h264 := &format.H264{
PayloadTyp: 96,
SPS: []byte{
0x67, 0x42, 0xC0, 0x1F, 0x96, 0x54, 0x05, 0x01, 0xED, 0x00, 0xF0, 0x88, 0x45, 0x80,
},
PPS: []byte{
0x68, 0xCE, 0x38, 0x80,
},
PacketizationMode: 1,
}
media := &description.Media{
Type: description.MediaTypeVideo,
// You could also set media.Control if you want a specific track URL.
Formats: []format.Format{h264},
}
desc := &description.Session{
Medias: []*description.Media{media},
}
stream := &gortsplib.ServerStream{
Desc: desc,
}
if err := stream.Initialize(); err != nil {
return nil, nil, err
}
return stream, media, nil
}
// rtpPump reads RTP packets from UDP and pushes them into the gortsplib stream.
// Every connected RTSP client gets the same packets (multi-client fan-out).
func (s *Server) rtpPump() {
defer s.wg.Done()
buf := make([]byte, 2048)
for {
select {
case <-s.ctx.Done():
return
default:
}
// Avoid blocking forever so we can react to shutdown.
_ = s.rtpConn.SetReadDeadline(time.Now().Add(2 * time.Second))
n, _, err := s.rtpConn.ReadFromUDP(buf)
if err != nil {
if ne, ok := err.(net.Error); ok && ne.Timeout() {
continue
}
// If the context is done, exit quietly.
if s.ctx.Err() != nil {
return
}
log.Printf("RTP read error: %v", err)
continue
}
var pkt rtp.Packet
if err := pkt.Unmarshal(buf[:n]); err != nil {
// Ignore malformed packets.
continue
}
if err := s.stream.WritePacketRTP(s.media, &pkt); err != nil {
// This is non-fatal; clients can disconnect at any time.
log.Printf("WritePacketRTP error: %v", err)
}
}
}