wip
This commit is contained in:
335
server/ingest/branch_live.go
Normal file
335
server/ingest/branch_live.go
Normal file
@@ -0,0 +1,335 @@
|
||||
package ingest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-gst/go-gst/gst"
|
||||
"github.com/go-gst/go-gst/gst/app"
|
||||
)
|
||||
|
||||
const (
|
||||
liveSampleTimeout = 200 * time.Millisecond
|
||||
liveSampleBufferSize = 5
|
||||
)
|
||||
|
||||
type subscribeReq struct {
|
||||
BufferSize int
|
||||
Result chan<- subscribeRes
|
||||
}
|
||||
type subscribeRes struct {
|
||||
Id int
|
||||
Stream <-chan *bytes.Reader
|
||||
Err error
|
||||
}
|
||||
|
||||
type unsubscribeReq struct {
|
||||
Id int
|
||||
}
|
||||
|
||||
// addLiveBranch populates the pipeline with:
|
||||
//
|
||||
// [vTee] ---> queue ---> valve ---> mp4mux ---> appsink
|
||||
// [aTee] ---> queue ---> valve -----^
|
||||
func (p *CameraPipeline) addLiveBranch(branchTimeout time.Duration) error {
|
||||
if p.vTee == nil {
|
||||
return fmt.Errorf("video tee not initialized")
|
||||
}
|
||||
if p.aTee == nil {
|
||||
return fmt.Errorf("audio tee not initialized")
|
||||
}
|
||||
vPad := p.vTee.GetRequestPad("src_%u")
|
||||
if vPad == nil {
|
||||
return fmt.Errorf("failed to get request pad from video tee")
|
||||
}
|
||||
aPad := p.aTee.GetRequestPad("src_%u")
|
||||
if aPad == nil {
|
||||
return fmt.Errorf("failed to get request pad from audio tee")
|
||||
}
|
||||
|
||||
vQueue, err := gst.NewElement("queue")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create video queue element: %w", err)
|
||||
}
|
||||
if err := setGlibValueProperty(vQueue, "leaky", 2); err != nil { // downstream
|
||||
return fmt.Errorf("failed to set video queue leaky property: %w", err)
|
||||
}
|
||||
if err := vQueue.SetProperty("max-size-time", uint64(2*time.Second.Nanoseconds())); err != nil {
|
||||
return fmt.Errorf("failed to set video queue max-size-time property: %w", err)
|
||||
}
|
||||
|
||||
vValve, err := gst.NewElement("valve")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create video valve element: %w", err)
|
||||
}
|
||||
if err := vValve.SetProperty("drop", true); err != nil {
|
||||
return fmt.Errorf("failed to set video valve drop property: %w", err)
|
||||
}
|
||||
if err := setGlibValueProperty(vValve, "drop-mode", 1); err != nil { // forward-sticky
|
||||
return fmt.Errorf("failed to set video valve drop-mode property: %w", err)
|
||||
}
|
||||
|
||||
aQueue, err := gst.NewElement("queue")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create audio queue element: %w", err)
|
||||
}
|
||||
if err := setGlibValueProperty(aQueue, "leaky", 2); err != nil { // downstream
|
||||
return fmt.Errorf("failed to set audio queue leaky property: %w", err)
|
||||
}
|
||||
if err := aQueue.SetProperty("max-size-time", uint64(2*time.Second.Nanoseconds())); err != nil {
|
||||
return fmt.Errorf("failed to set audio queue max-size-time property: %w", err)
|
||||
}
|
||||
|
||||
aValve, err := gst.NewElement("valve")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create audio valve element: %w", err)
|
||||
}
|
||||
if err := aValve.SetProperty("drop", true); err != nil {
|
||||
return fmt.Errorf("failed to set audio valve drop property: %w", err)
|
||||
}
|
||||
if err := setGlibValueProperty(aValve, "drop-mode", 1); err != nil { // forward-sticky
|
||||
return fmt.Errorf("failed to set audio valve drop-mode property: %w", err)
|
||||
}
|
||||
|
||||
mp4Mux, err := gst.NewElement("mp4mux")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create mp4mux element: %w", err)
|
||||
}
|
||||
if err := mp4Mux.SetProperty("fragment-duration", uint(100*time.Millisecond.Nanoseconds())); err != nil {
|
||||
return fmt.Errorf("failed to set mp4mux fragment-duration property: %w", err)
|
||||
}
|
||||
if err := setGlibValueProperty(mp4Mux, "fragment-mode", 0); err != nil { // dash-or-mss
|
||||
return fmt.Errorf("failed to set mp4mux fragment-mode property: %w", err)
|
||||
}
|
||||
if err := mp4Mux.SetProperty("interleave-time", uint64(0)); err != nil {
|
||||
return fmt.Errorf("failed to set mp4mux interleave-time property: %w", err)
|
||||
}
|
||||
if err := mp4Mux.SetProperty("latency", uint64(0)); err != nil {
|
||||
return fmt.Errorf("failed to set mp4mux latency property: %w", err)
|
||||
}
|
||||
|
||||
appSink, err := gst.NewElement("appsink")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create appsink element: %w", err)
|
||||
}
|
||||
if err := appSink.SetProperty("max-buffers", uint(5)); err != nil {
|
||||
return fmt.Errorf("failed to set appsink max-buffers property: %w", err)
|
||||
}
|
||||
if err := appSink.SetProperty("sync", false); err != nil {
|
||||
return fmt.Errorf("failed to set appsink sync property: %w", err)
|
||||
}
|
||||
if err := appSink.SetProperty("async", false); err != nil {
|
||||
return fmt.Errorf("failed to set appsink async property: %w", err)
|
||||
}
|
||||
|
||||
if err := p.pipeline.AddMany(vQueue, vValve, aQueue, aValve, mp4Mux, appSink); err != nil {
|
||||
return fmt.Errorf("failed to add live branch elements to pipeline: %w", err)
|
||||
}
|
||||
|
||||
if res := vPad.Link(vQueue.GetStaticPad("sink")); res != gst.PadLinkOK {
|
||||
return fmt.Errorf("failed to link video tee to video queue: %s", res)
|
||||
}
|
||||
if res := aPad.Link(aQueue.GetStaticPad("sink")); res != gst.PadLinkOK {
|
||||
return fmt.Errorf("failed to link audio tee to audio queue: %s", res)
|
||||
}
|
||||
if res := vQueue.GetStaticPad("src").Link(vValve.GetStaticPad("sink")); res != gst.PadLinkOK {
|
||||
return fmt.Errorf("failed to link video queue to video valve: %s", res)
|
||||
}
|
||||
if res := aQueue.GetStaticPad("src").Link(aValve.GetStaticPad("sink")); res != gst.PadLinkOK {
|
||||
return fmt.Errorf("failed to link audio queue to audio valve: %s", res)
|
||||
}
|
||||
if res := vValve.GetStaticPad("src").Link(mp4Mux.GetRequestPad("video_%u")); res != gst.PadLinkOK {
|
||||
return fmt.Errorf("failed to link video valve to mp4mux: %s", res)
|
||||
}
|
||||
if res := aValve.GetStaticPad("src").Link(mp4Mux.GetRequestPad("audio_%u")); res != gst.PadLinkOK {
|
||||
return fmt.Errorf("failed to link audio valve to mp4mux: %s", res)
|
||||
}
|
||||
if res := mp4Mux.GetStaticPad("src").Link(appSink.GetStaticPad("sink")); res != gst.PadLinkOK {
|
||||
return fmt.Errorf("failed to link mp4mux to appsink: %s", res)
|
||||
}
|
||||
|
||||
p.liveVValve = vValve
|
||||
p.liveAValve = aValve
|
||||
p.liveSink = app.SinkFromElement(appSink)
|
||||
p.liveTimeout = branchTimeout
|
||||
p.liveSubscribe = make(chan subscribeReq)
|
||||
p.liveUnsubscribe = make(chan unsubscribeReq)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *CameraPipeline) liveSampler(ctx context.Context, result chan<- []byte) {
|
||||
SamplerLoop:
|
||||
for {
|
||||
sample := p.liveSink.TryPullSample(gst.ClockTime(liveSampleTimeout.Nanoseconds()))
|
||||
if sample == nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
p.log.Debug("Live sampler timed out waiting for sample")
|
||||
continue SamplerLoop
|
||||
}
|
||||
}
|
||||
buf := sample.GetBuffer()
|
||||
if buf == nil {
|
||||
p.log.Warn("Received nil buffer from live sink")
|
||||
continue SamplerLoop
|
||||
}
|
||||
data, err := gstBufferToBytes(buf)
|
||||
if err != nil {
|
||||
p.log.Warn("Failed to convert buffer to bytes")
|
||||
continue SamplerLoop
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case result <- data:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *CameraPipeline) liveManager(ctx context.Context) {
|
||||
active := false
|
||||
var initSeg []byte
|
||||
subscribers := &subscribersManager{m: make(map[int]subscriber), nextID: 0}
|
||||
samples := make(chan []byte, liveSampleBufferSize)
|
||||
samplerCtx, samplerCancel := context.WithCancel(ctx)
|
||||
defer func() {
|
||||
subscribers.Close()
|
||||
samplerCancel()
|
||||
}()
|
||||
|
||||
defer p.log.Debug("Live branch manager exiting")
|
||||
var start time.Time
|
||||
var numSamples int
|
||||
for {
|
||||
var timeout <-chan time.Time
|
||||
if active && subscribers.len() == 0 {
|
||||
timeout = time.After(p.liveTimeout)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
samplerCancel()
|
||||
p.log.Info("Context cancelled", "numSamples", numSamples, "duration", time.Since(start))
|
||||
return
|
||||
case req := <-p.liveSubscribe:
|
||||
if !active {
|
||||
p.log.Info("Activating live branch")
|
||||
if err := p.liveVValve.SetProperty("drop", false); err != nil {
|
||||
req.Result <- subscribeRes{Err: fmt.Errorf("failed to activate video valve: %w", err)}
|
||||
continue
|
||||
}
|
||||
if err := p.liveAValve.SetProperty("drop", false); err != nil {
|
||||
if rollbackErr := p.liveVValve.SetProperty("drop", true); rollbackErr != nil { // try to rollback
|
||||
p.log.Warn("Failed to rollback video valve state after audio valve activation failure", "error", rollbackErr)
|
||||
}
|
||||
req.Result <- subscribeRes{Err: fmt.Errorf("failed to activate audio valve: %w", err)}
|
||||
continue
|
||||
}
|
||||
go p.liveSampler(samplerCtx, samples)
|
||||
active = true
|
||||
start = time.Now()
|
||||
}
|
||||
stream := make(chan *bytes.Reader, req.BufferSize)
|
||||
id := subscribers.AddSubscriber(stream)
|
||||
if initSeg != nil {
|
||||
if !subscribers.SendOrUnsubscribe(id, initSeg) {
|
||||
req.Result <- subscribeRes{Err: fmt.Errorf("subscriber missed init segment")}
|
||||
continue
|
||||
}
|
||||
}
|
||||
req.Result <- subscribeRes{Id: id, Stream: stream}
|
||||
case req := <-p.liveUnsubscribe:
|
||||
subscribers.RemoveSubscriber(req.Id)
|
||||
case data := <-samples:
|
||||
// TODO : init segment
|
||||
p.log.Debug("got video sample")
|
||||
numSamples += 1
|
||||
subscribers.Broadcast(data)
|
||||
case <-timeout:
|
||||
p.log.Info("Deactivating live branch due to inactivity")
|
||||
if err := p.liveVValve.SetProperty("drop", true); err != nil {
|
||||
p.log.Warn("Failed to deactivate video valve: %w", err)
|
||||
}
|
||||
if err := p.liveAValve.SetProperty("drop", true); err != nil {
|
||||
p.log.Warn("Failed to deactivate audio valve: %w", err)
|
||||
}
|
||||
samplerCancel()
|
||||
samplerCtx, samplerCancel = context.WithCancel(ctx)
|
||||
active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *CameraPipeline) LiveSubscribe(timeout time.Duration, bufferSize int) (id int, stream <-chan *bytes.Reader, err error) {
|
||||
result := make(chan subscribeRes)
|
||||
req := subscribeReq{
|
||||
BufferSize: bufferSize,
|
||||
Result: result,
|
||||
}
|
||||
select {
|
||||
case <-time.After(timeout):
|
||||
return 0, nil, fmt.Errorf("live subscribe request timed out")
|
||||
case p.liveSubscribe <- req:
|
||||
}
|
||||
res := <-result
|
||||
return res.Id, res.Stream, res.Err
|
||||
}
|
||||
|
||||
func (p *CameraPipeline) LiveUnsubscribe(id int) {
|
||||
p.liveUnsubscribe <- unsubscribeReq{Id: id}
|
||||
}
|
||||
|
||||
type subscriber struct {
|
||||
Stream chan<- *bytes.Reader
|
||||
}
|
||||
|
||||
type subscribersManager struct {
|
||||
m map[int]subscriber
|
||||
nextID int
|
||||
}
|
||||
|
||||
func (sm subscribersManager) len() int {
|
||||
return len(sm.m)
|
||||
}
|
||||
|
||||
func (sm *subscribersManager) AddSubscriber(stream chan<- *bytes.Reader) int {
|
||||
id := sm.nextID
|
||||
sm.m[id] = subscriber{Stream: stream}
|
||||
sm.nextID++
|
||||
return id
|
||||
}
|
||||
|
||||
func (sm *subscribersManager) RemoveSubscriber(id int) {
|
||||
if sub, ok := sm.m[id]; ok {
|
||||
close(sub.Stream)
|
||||
delete(sm.m, id)
|
||||
}
|
||||
}
|
||||
|
||||
func (sm *subscribersManager) SendOrUnsubscribe(id int, data []byte) bool {
|
||||
if sub, ok := sm.m[id]; ok {
|
||||
select {
|
||||
case sub.Stream <- bytes.NewReader(data):
|
||||
return true
|
||||
default:
|
||||
sm.RemoveSubscriber(id)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (sm *subscribersManager) Close() {
|
||||
for id := range sm.m {
|
||||
sm.RemoveSubscriber(id)
|
||||
}
|
||||
}
|
||||
|
||||
func (sm *subscribersManager) Broadcast(data []byte) {
|
||||
for id := range sm.m {
|
||||
sm.SendOrUnsubscribe(id, data)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user