wip
This commit is contained in:
323
server/ingest/pipeline.go
Normal file
323
server/ingest/pipeline.go
Normal file
@@ -0,0 +1,323 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user