feat: add api and ndi device monitor with webrtc preview pipeline
This commit is contained in:
parent
f6be41cb9e
commit
c3a187d580
7 changed files with 768 additions and 9 deletions
232
internal/ndi/device.go
Normal file
232
internal/ndi/device.go
Normal file
|
@ -0,0 +1,232 @@
|
|||
package ndi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/go-gst/go-gst/gst"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
sync.RWMutex
|
||||
|
||||
name string
|
||||
uri string
|
||||
props map[string]interface{}
|
||||
|
||||
available bool
|
||||
|
||||
gstDevice *gst.Device
|
||||
|
||||
pipeline *gst.Pipeline
|
||||
videoTee *gst.Element
|
||||
videoProxy map[string]*gst.Element
|
||||
audioTee *gst.Element
|
||||
audioProxy map[string]*gst.Element
|
||||
}
|
||||
|
||||
func newDevice(d *gst.Device) *Device {
|
||||
name := d.GetDisplayName()
|
||||
uri := d.GetProperties().Values()["url-address"].(string)
|
||||
props := d.GetProperties().Values()
|
||||
|
||||
device := &Device{
|
||||
name: name,
|
||||
uri: uri,
|
||||
props: props,
|
||||
available: true,
|
||||
|
||||
gstDevice: d,
|
||||
|
||||
videoProxy: make(map[string]*gst.Element),
|
||||
audioProxy: make(map[string]*gst.Element),
|
||||
}
|
||||
|
||||
err := device.createPipeline()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create pipeline for device")
|
||||
return nil
|
||||
}
|
||||
|
||||
return device
|
||||
}
|
||||
|
||||
func (d *Device) updateProperties(dgst *gst.Device) {
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
|
||||
d.name = dgst.GetDisplayName()
|
||||
d.uri = dgst.GetProperties().Values()["url-address"].(string)
|
||||
d.props = dgst.GetProperties().Values()
|
||||
d.available = true
|
||||
}
|
||||
|
||||
func (d *Device) setUnavailable() {
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
|
||||
d.available = false
|
||||
}
|
||||
|
||||
func (d *Device) GetDisplayName() string {
|
||||
d.RLock()
|
||||
defer d.RUnlock()
|
||||
|
||||
return d.name
|
||||
}
|
||||
|
||||
func (d *Device) GetDeviceType() string {
|
||||
return deviceType
|
||||
}
|
||||
|
||||
func (d *Device) GetProperties() map[string]interface{} {
|
||||
d.RLock()
|
||||
defer d.RUnlock()
|
||||
|
||||
return d.props
|
||||
}
|
||||
|
||||
// TODO handle case where ndidemux src pads are not present
|
||||
func (d *Device) createPipeline() error {
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
|
||||
pipeline, err := gst.NewPipeline("ndi-" + d.name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating pipeline: %w", err)
|
||||
}
|
||||
pipeline.ForceClock(gst.ObtainSystemClock().Clock)
|
||||
|
||||
input := d.gstDevice.CreateElement("")
|
||||
inputQueue, err := gst.NewElementWithProperties("queue2",
|
||||
map[string]interface{}{
|
||||
"name": "input-queue",
|
||||
"max-size-buffers": 0,
|
||||
"max-size-bytes": 0,
|
||||
"max-size-time": 40000000, // 40ms
|
||||
})
|
||||
if err := input.Link(inputQueue); err != nil {
|
||||
return fmt.Errorf("linking input to input input-queue: %w", err)
|
||||
}
|
||||
|
||||
ndiDemuxer, err := gst.NewElement("ndisrcdemux")
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating ndisrcdemux: %w", err)
|
||||
}
|
||||
|
||||
if err := inputQueue.Link(ndiDemuxer); err != nil {
|
||||
return fmt.Errorf("linking input-queue to ndisrcdemux: %w", err)
|
||||
}
|
||||
|
||||
videoTee, err := gst.NewElementWithName("tee", "video-tee")
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating video-tee: %w", err)
|
||||
}
|
||||
|
||||
audioTee, err := gst.NewElementWithName("tee", "audio-tee")
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating audio-tee: %w", err)
|
||||
}
|
||||
|
||||
if err := pipeline.AddMany(input, ndiDemuxer, videoTee, audioTee); err != nil {
|
||||
return fmt.Errorf("adding elements to pipeline: %w", err)
|
||||
}
|
||||
|
||||
d.pipeline = pipeline
|
||||
d.videoTee = videoTee
|
||||
d.audioTee = audioTee
|
||||
|
||||
if _, err := ndiDemuxer.Connect("pad-added", d.onNdiSrcDemuxerPadAdded); err != nil {
|
||||
return fmt.Errorf("connecting pad-added signal: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Device) onNdiSrcDemuxerPadAdded(element *gst.Element, pad *gst.Pad) {
|
||||
switch pad.GetName() {
|
||||
case "video":
|
||||
if plr := d.videoTee.GetStaticPad("sink").Link(pad); plr != gst.PadLinkOK {
|
||||
log.Error().Str("pad", pad.GetName()).Msgf("linking video pad to video tee: %s", plr)
|
||||
}
|
||||
case "audio":
|
||||
if plr := d.audioTee.GetStaticPad("sink").Link(pad); plr != gst.PadLinkOK {
|
||||
log.Error().Str("pad", pad.GetName()).Msgf("linking audio pad to audio tee: %s", plr)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (d *Device) GetOutput() (string, *gst.Element, *gst.Element, error) {
|
||||
id, videoElement, audioElement, err := func() (string, *gst.Element, *gst.Element, error) {
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
id := uuid.NewString()
|
||||
|
||||
videoProxySink, err := gst.NewElementWithName("proxysink", "video-proxy")
|
||||
if err != nil {
|
||||
return "", nil, nil, fmt.Errorf("creating video-proxy sink: %w", err)
|
||||
}
|
||||
if err := d.pipeline.Add(videoProxySink); err != nil {
|
||||
return "", nil, nil, fmt.Errorf("adding video-proxy sink to pipeline: %w", err)
|
||||
}
|
||||
|
||||
if plr := d.videoTee.GetRequestPad("src_%u").Link(videoProxySink.GetStaticPad("sink")); plr != gst.PadLinkOK {
|
||||
return "", nil, nil, fmt.Errorf("linking video-tee to video-proxy sink: %s", plr)
|
||||
}
|
||||
|
||||
if !videoProxySink.SyncStateWithParent() {
|
||||
log.Warn().Msg("video-proxy sink sync state not syncable")
|
||||
}
|
||||
|
||||
audioProxySink, err := gst.NewElementWithName("proxysink", "audio-proxy")
|
||||
if err != nil {
|
||||
return "", nil, nil, fmt.Errorf("creating audio-proxy sink: %w", err)
|
||||
}
|
||||
|
||||
if err := d.pipeline.Add(audioProxySink); err != nil {
|
||||
return "", nil, nil, fmt.Errorf("adding audio-proxy sink to pipeline: %w", err)
|
||||
}
|
||||
|
||||
if plr := d.audioTee.GetRequestPad("src_%u").Link(audioProxySink.GetStaticPad("sink")); plr != gst.PadLinkOK {
|
||||
return "", nil, nil, fmt.Errorf("linking audio-tee to audio-proxy sink: %s", plr)
|
||||
}
|
||||
|
||||
if !audioProxySink.SyncStateWithParent() {
|
||||
log.Warn().Msg("audio-proxy sink sync state not syncable")
|
||||
}
|
||||
|
||||
d.videoProxy[id] = videoProxySink
|
||||
d.audioProxy[id] = audioProxySink
|
||||
|
||||
return id, videoProxySink, audioProxySink, nil
|
||||
}()
|
||||
if err != nil {
|
||||
return "", nil, nil, fmt.Errorf("creating output: %w", err)
|
||||
}
|
||||
if err := d.setPipelineState(gst.StatePlaying); err != nil {
|
||||
return "", nil, nil, fmt.Errorf("setting pipeline state: %w", err)
|
||||
}
|
||||
|
||||
return id, videoElement, audioElement, nil
|
||||
}
|
||||
|
||||
func (d *Device) setPipelineState(state gst.State) error {
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
|
||||
if d.pipeline == nil {
|
||||
return fmt.Errorf("pipeline is nil")
|
||||
}
|
||||
|
||||
if state == d.pipeline.GetCurrentState() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := d.pipeline.SetState(state); err != nil {
|
||||
return fmt.Errorf("setting pipeline state: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
100
internal/ndi/ndi.go
Normal file
100
internal/ndi/ndi.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
package ndi
|
||||
|
||||
import (
|
||||
"git.entr0py.de/garionion/catie/internal/api"
|
||||
"github.com/go-gst/go-gst/gst"
|
||||
"github.com/rs/zerolog/log"
|
||||
"time"
|
||||
)
|
||||
|
||||
const deviceType = "NDI"
|
||||
|
||||
type NDI struct {
|
||||
deviceMonitor *gst.DeviceMonitor
|
||||
devices map[string]*Device
|
||||
}
|
||||
|
||||
func NewNDI() *NDI {
|
||||
|
||||
log.Debug().Msg("creating NDI device monitor")
|
||||
devMon := gst.NewDeviceMonitor()
|
||||
caps := gst.NewCapsFromString("application/x-ndi")
|
||||
devMon.AddFilter("Source/Network", caps)
|
||||
|
||||
n := &NDI{
|
||||
deviceMonitor: devMon,
|
||||
devices: make(map[string]*Device),
|
||||
}
|
||||
|
||||
bus := devMon.GetBus()
|
||||
bus.AddWatch(func(msg *gst.Message) bool {
|
||||
switch msg.Type() {
|
||||
case gst.MessageDeviceAdded:
|
||||
device := msg.ParseDeviceAdded()
|
||||
n.createOrUpdateDevice(device)
|
||||
log.Debug().Str("message", device.GetDisplayName()).Fields(device.GetProperties().Values()).Msg("NDI Device Added")
|
||||
case gst.MessageDeviceRemoved:
|
||||
device := msg.ParseDeviceRemoved()
|
||||
n.setDeviceUnavailable(device)
|
||||
log.Debug().Str("message", device.GetDisplayName()).Fields(device.GetProperties().Values()).Msg("NDI Device Removed")
|
||||
default:
|
||||
log.Debug().Str("msgType", msg.TypeName()).Str("message", msg.String()).Msg("default")
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
log.Debug().Msg("starting NDI device monitor")
|
||||
devMon.Start()
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *NDI) GetDeviceType() string {
|
||||
return deviceType
|
||||
}
|
||||
|
||||
func (n *NDI) GetDevices() []api.Device {
|
||||
devices := make([]api.Device, 0, len(n.devices))
|
||||
for _, d := range n.devices {
|
||||
devices = append(devices, d)
|
||||
}
|
||||
|
||||
return devices
|
||||
}
|
||||
|
||||
func (n *NDI) GetDevice(name string) api.Device {
|
||||
if device, ok := n.devices[name]; ok {
|
||||
return device
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NDI) createOrUpdateDevice(d *gst.Device) {
|
||||
name := d.GetDisplayName()
|
||||
if device, ok := n.devices[name]; ok {
|
||||
device.updateProperties(d)
|
||||
return
|
||||
}
|
||||
|
||||
n.devices[name] = newDevice(d)
|
||||
|
||||
}
|
||||
|
||||
func (n *NDI) setDeviceUnavailable(d *gst.Device) {
|
||||
if device, ok := n.devices[d.GetDisplayName()]; ok {
|
||||
device.setUnavailable()
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NDI) DiscoverSources() {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
|
||||
for range ticker.C {
|
||||
devices := n.deviceMonitor.GetDevices()
|
||||
for _, device := range devices {
|
||||
log.Debug().Fields(device.GetProperties().Values()).Msg("discovering sources")
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue