Files
cctv/server/ingest/pipeline.go
2026-04-22 23:35:59 +01:00

324 lines
9.0 KiB
Go

package ingest
import (
"context"
"fmt"
"io"
"log/slog"
"net/url"
"reflect"
"sync/atomic"
"time"
"github.com/go-gst/go-glib/glib"
"github.com/go-gst/go-gst/gst"
"github.com/go-gst/go-gst/gst/app"
)
type CameraPipeline struct {
log *slog.Logger
pipeline *gst.Pipeline
vTee *gst.Element
aTee *gst.Element
thumbnailBlockPad *gst.Pad
thumbnailSink *app.Sink
thumbnailBlockProbe uint64
thumbnailBlocked atomic.Bool
thumbnailTimeout time.Duration
thumbnailReq chan thumbnailReq
liveVValve *gst.Element
liveAValve *gst.Element
liveSink *app.Sink
liveTimeout time.Duration
liveSubscribe chan subscribeReq
liveUnsubscribe chan unsubscribeReq
}
type jobResult struct {
Err error
}
func CreatePipeline(log *slog.Logger) (*CameraPipeline, error) {
var err error
p := &CameraPipeline{
log: log,
}
p.pipeline, err = gst.NewPipeline("")
if err != nil {
return nil, fmt.Errorf("failed to create GStreamer pipeline: %w", err)
}
return p, nil
}
func (p *CameraPipeline) Close() {
p.pipeline.SetState(gst.StateNull)
p.thumbnailBlockPad = nil
p.thumbnailSink = nil
p.liveVValve = nil
p.liveAValve = nil
p.liveSink = nil
p.vTee = nil
p.aTee = nil
p.pipeline = nil
}
func (p *CameraPipeline) Run(ctx context.Context) error {
if err := p.pipeline.SetState(gst.StatePlaying); err != nil {
return fmt.Errorf("failed to set pipeline to playing: %w", err)
}
if p.thumbnailSink != nil {
go p.thumbnailManager(ctx)
}
if p.liveSink != nil {
go p.liveManager(ctx)
}
bus := p.pipeline.GetBus()
for {
select {
case <-ctx.Done():
p.Close()
return nil
default:
}
done, err := handleMessage(p.log, bus.TimedPop(gst.ClockTime((500 * time.Millisecond).Nanoseconds())))
if err != nil {
p.Close()
return fmt.Errorf("pipeline error: %w", err)
}
if done {
p.Close()
return nil
}
}
}
func handleMessage(log *slog.Logger, msg *gst.Message) (bool, error) {
if msg == nil {
return false, nil
}
// nolint:exhaustive
switch msg.Type() {
case gst.MessageEOS:
log.Info("Pipeline reached EOS")
return true, nil
case gst.MessageError:
err := msg.ParseError()
return true, fmt.Errorf("pipeline error: %w", err)
default:
log.Debug(fmt.Sprintf("Pipeline message: %s", msg))
return false, nil
}
}
// addRtspSource populates the pipeline with:
//
// rtspsrc
// |---> (dynamic video depayloader + parser) --> vTee
// |---> (dynamic audio depayloader + parser) --> aTee
func (p *CameraPipeline) addRtspSource(rtspURL *url.URL) error {
if p.vTee != nil || p.aTee != nil {
return fmt.Errorf("source already added")
}
src, err := gst.NewElement("rtspsrc")
if err != nil {
return fmt.Errorf("failed to create rtspsrc element: %w", err)
}
if err := src.SetProperty("location", rtspURL.String()); err != nil {
return fmt.Errorf("failed to set rtspsrc location property: %w", err)
}
if err := src.SetProperty("latency", uint(200)); err != nil {
return fmt.Errorf("failed to set rtspsrc latency property: %w", err)
}
p.vTee, err = gst.NewElement("tee")
if err != nil {
return fmt.Errorf("failed to create video tee element: %w", err)
}
if err := p.vTee.SetProperty("name", "video_tee"); err != nil {
return fmt.Errorf("failed to set video tee name property: %w", err)
}
p.aTee, err = gst.NewElement("tee")
if err != nil {
return fmt.Errorf("failed to create audio tee element: %w", err)
}
if err := p.aTee.SetProperty("name", "audio_tee"); err != nil {
return fmt.Errorf("failed to set audio tee name property: %w", err)
}
if err := p.pipeline.AddMany(src, p.vTee, p.aTee); err != nil {
return fmt.Errorf("failed to add elements to pipeline: %w", err)
}
// Dynamically link the appropriate depayloader
src.Connect("pad-added", func(element *gst.Element, pad *gst.Pad) {
log := p.log.With("src", element.GetName(), "pad", pad.GetName())
caps := pad.GetCurrentCaps()
if caps == nil || caps.GetSize() == 0 {
log.Error("Ignoring pad (no or empty caps)")
return
}
if caps.GetSize() > 1 {
// Note: should be impossible
log.Warn("Pad has multiple structures, using the first one", "caps", caps.String())
}
structure := caps.GetStructureAt(0)
if structure == nil || structure.Name() != "application/x-rtp" {
log.Error("Ignoring pad (not RTP)", "caps", caps.String())
return
}
mediaRaw, err := structure.GetValue("media")
media, ok := mediaRaw.(string)
if err != nil || !ok {
log.Error("Ignoring pad (no media field)", "caps", caps.String())
return
}
encodingRaw, err := structure.GetValue("encoding-name")
encoding, ok := encodingRaw.(string)
if err != nil || !ok {
log.Error("Ignoring pad (no encoding-name field)", "caps", caps.String())
return
}
var chain []*gst.Element
switch media {
case "video":
switch encoding {
case "H264":
chain, err = p.addStaticChain(pad, p.vTee.GetStaticPad("sink"), "rtph264depay", "h264parse")
case "H265":
chain, err = p.addStaticChain(pad, p.vTee.GetStaticPad("sink"), "rtph265depay", "h265parse")
default:
log.Error("Ignoring video pad (unsupported encoding)", "caps", caps.String())
return
}
case "audio":
switch encoding {
case "MPEG4-GENERIC":
chain, err = p.addStaticChain(pad, p.aTee.GetStaticPad("sink"), "rtpmp4gdepay", "aacparse")
default:
log.Error("Ignoring audio pad (unsupported encoding)", "caps", caps.String())
return
}
default:
log.Error("Ignoring pad (unsupported media)", "caps", caps.String())
return
}
if err != nil {
log.Error("Failed to add depayloader", "caps", caps.String(), "error", err)
return
}
for _, element := range chain {
element.SyncStateWithParent()
}
})
return nil
}
func (p *CameraPipeline) addStaticChain(src *gst.Pad, sink *gst.Pad, chain ...string) ([]*gst.Element, error) {
factories := make([]elementFactory, len(chain))
for i, name := range chain {
factories[i] = elementFactory{name: name, props: nil}
}
return p.addStaticChainProps(src, sink, factories...)
}
type elementFactory struct {
name string
props map[string]any
}
func (p *CameraPipeline) addStaticChainProps(src *gst.Pad, sink *gst.Pad, chain ...elementFactory) ([]*gst.Element, error) {
chainElements := make([]*gst.Element, len(chain))
last := src
for i, factory := range chain {
elem, err := gst.NewElement(factory.name)
if err != nil {
return nil, fmt.Errorf("failed to create %s: %w", factory.name, err)
}
for propName, propValue := range factory.props {
if err := elem.SetProperty(propName, propValue); err != nil {
// try to set enum property
valueErr := setGlibValueProperty(elem, propName, propValue)
if valueErr != nil {
p.log.Warn("Failed to set property glib value", "name", propName, "value", propValue, "error", valueErr)
return nil, fmt.Errorf("failed to set property %s on %s: %w", propName, factory.name, err)
}
}
}
if err := p.pipeline.Add(elem); err != nil {
return nil, fmt.Errorf("failed to add %s to pipeline: %w", factory.name, err)
}
if last != nil {
if res := last.Link(elem.GetStaticPad("sink")); res != gst.PadLinkOK {
return nil, fmt.Errorf("failed to link %s to src: %s", factory.name, res)
}
}
last = elem.GetStaticPad("src")
chainElements[i] = elem
}
if sink != nil {
if res := last.Link(sink); res != gst.PadLinkOK {
return nil, fmt.Errorf("failed to link depayloader chain to sink: %s", res)
}
}
return chainElements, nil
}
func gstBufferToBytes(buf *gst.Buffer) ([]byte, error) {
mapInfo := buf.Map(gst.MapRead)
if mapInfo == nil {
return nil, fmt.Errorf("failed to map buffer")
}
defer buf.Unmap()
data := make([]byte, mapInfo.Size())
if _, err := io.ReadFull(mapInfo.Reader(), data); err != nil {
return nil, fmt.Errorf("failed to read buffer data: %w", err)
}
return data, nil
}
func setGlibValueProperty(elem *gst.Element, name string, value any) error {
propType, err := elem.GetPropertyType(name)
if err != nil {
return fmt.Errorf("get property type %s: %w", name, err)
}
v, err := glib.ValueInit(propType)
if err != nil {
return fmt.Errorf("init gvalue for %s: %w", name, err)
}
val := reflect.ValueOf(value)
switch {
case propType.IsA(glib.TYPE_ENUM) && val.CanInt():
v.SetEnum(int(val.Int())) // safe because these config values are all done locally and guaranteed to be within int32
case propType.IsA(glib.TYPE_ENUM) && val.CanUint():
v.SetEnum(int(val.Uint())) // safe because these config values are all done locally and guaranteed to be within int32
case propType.IsA(glib.TYPE_FLAGS) && val.CanInt():
v.SetFlags(uint(val.Int())) // safe because these config values are all done locally and guaranteed to be within uint32
case propType.IsA(glib.TYPE_FLAGS) && val.CanUint():
v.SetFlags(uint(val.Uint())) // safe because these config values are all done locally and guaranteed to be within uint32
default:
return fmt.Errorf("unsupported property type for %s: %T (kind = %s), expected %s", name, value, val.Kind(), propType.Name())
}
if err := elem.SetPropertyValue(name, v); err != nil {
return fmt.Errorf("failed to set glib value property %s: %w", name, err)
}
return nil
}