neko/pkg/types/capture.go

255 lines
5.8 KiB
Go
Raw Normal View History

2020-11-02 04:09:48 +13:00
package types
2021-02-02 11:50:18 +13:00
import (
"context"
2021-08-30 03:09:13 +12:00
"errors"
2021-03-29 11:58:51 +13:00
"fmt"
2021-03-30 11:37:06 +13:00
"math"
"strings"
"time"
2021-03-29 11:58:51 +13:00
"github.com/PaesslerAG/gval"
"github.com/demodesk/neko/pkg/types/codec"
2021-02-02 11:50:18 +13:00
)
2021-08-30 03:09:13 +12:00
var (
ErrCapturePipelineAlreadyExists = errors.New("capture pipeline already exists")
)
type Sample struct {
// buffer with encoded media
Data []byte
Length int
// timing information
Timestamp time.Time
Duration time.Duration
// metadata
DeltaUnit bool // this unit cannot be decoded independently.
}
2020-11-02 04:09:48 +13:00
type SampleListener interface {
WriteSample(Sample)
}
type Receiver interface {
SetStream(stream StreamSinkManager) (changed bool, err error)
RemoveStream()
OnBitrateChange(f func(bitrate int) (changed bool, err error))
OnVideoChange(f func(videoID string) (changed bool, err error))
VideoAuto() bool
SetVideoAuto(videoAuto bool)
}
type BucketsManager interface {
IDs() []string
Codec() codec.RTPCodec
SetReceiver(receiver Receiver)
RemoveReceiver(receiver Receiver) error
DestroyAll()
RecreateAll() error
Shutdown()
}
type BroadcastManager interface {
Start(url string) error
Stop()
2021-02-06 02:03:53 +13:00
Started() bool
Url() string
}
2021-01-23 06:13:32 +13:00
type ScreencastManager interface {
Enabled() bool
2021-01-24 03:17:52 +13:00
Started() bool
Image() ([]byte, error)
2021-01-23 06:13:32 +13:00
}
2021-12-02 08:30:18 +13:00
type StreamSinkManager interface {
ID() string
Codec() codec.RTPCodec
Bitrate() int
AddListener(listener SampleListener) error
RemoveListener(listener SampleListener) error
MoveListenerTo(listener SampleListener, targetStream StreamSinkManager) error
2021-09-27 11:50:49 +13:00
ListenersCount() int
2021-02-06 02:03:53 +13:00
Started() bool
CreatePipeline() error
DestroyPipeline()
}
2021-12-02 10:36:45 +13:00
type StreamSrcManager interface {
Codec() codec.RTPCodec
Start(codec codec.RTPCodec) error
Stop()
Push(bytes []byte)
Started() bool
}
2020-11-02 04:09:48 +13:00
type CaptureManager interface {
Start()
Shutdown() error
GetBitrateFromVideoID(videoID string) (int, error)
Broadcast() BroadcastManager
2021-01-23 06:13:32 +13:00
Screencast() ScreencastManager
2021-12-02 08:30:18 +13:00
Audio() StreamSinkManager
Video() BucketsManager
2021-12-02 10:36:45 +13:00
Webcam() StreamSrcManager
Microphone() StreamSrcManager
2020-11-02 04:09:48 +13:00
}
2021-03-29 11:58:51 +13:00
type VideoConfig struct {
Width string `mapstructure:"width"` // expression
Height string `mapstructure:"height"` // expression
Fps string `mapstructure:"fps"` // expression
Bitrate int `mapstructure:"bitrate"` // pipeline bitrate
GstPrefix string `mapstructure:"gst_prefix"` // pipeline prefix, starts with !
2021-03-30 11:37:06 +13:00
GstEncoder string `mapstructure:"gst_encoder"` // gst encoder name
2021-03-29 11:58:51 +13:00
GstParams map[string]string `mapstructure:"gst_params"` // map of expressions
GstSuffix string `mapstructure:"gst_suffix"` // pipeline suffix, starts with !
GstPipeline string `mapstructure:"gst_pipeline"` // whole pipeline as a string
2021-03-29 11:58:51 +13:00
}
func (config *VideoConfig) GetPipeline(screen ScreenSize) (string, error) {
values := map[string]any{
2021-03-29 11:58:51 +13:00
"width": screen.Width,
"height": screen.Height,
"fps": screen.Rate,
}
language := []gval.Language{
gval.Function("round", func(args ...any) (any, error) {
2021-03-29 11:58:51 +13:00
return (int)(math.Round(args[0].(float64))), nil
}),
}
// get fps pipeline
2021-03-30 09:59:07 +13:00
fpsPipeline := "! video/x-raw ! videoconvert ! queue"
2021-03-29 11:58:51 +13:00
if config.Fps != "" {
eval, err := gval.Full(language...).NewEvaluable(config.Fps)
2021-03-29 11:58:51 +13:00
if err != nil {
return "", err
}
val, err := eval.EvalFloat64(context.Background(), values)
if err != nil {
return "", err
2021-03-29 11:58:51 +13:00
}
2022-09-11 09:24:27 +12:00
fpsPipeline = fmt.Sprintf("! capsfilter caps=video/x-raw,framerate=%d/100 name=framerate ! videoconvert ! queue", int(val*100))
2021-03-29 11:58:51 +13:00
}
// get scale pipeline
scalePipeline := ""
if config.Width != "" && config.Height != "" {
eval, err := gval.Full(language...).NewEvaluable(config.Width)
if err != nil {
return "", err
}
w, err := eval.EvalInt(context.Background(), values)
2021-03-29 11:58:51 +13:00
if err != nil {
return "", err
}
eval, err = gval.Full(language...).NewEvaluable(config.Height)
2021-03-29 11:58:51 +13:00
if err != nil {
return "", err
}
h, err := eval.EvalInt(context.Background(), values)
if err != nil {
return "", err
2021-03-29 11:58:51 +13:00
}
// element videoscale parameter method to 0 meaning nearest neighbor
scalePipeline = fmt.Sprintf("! videoscale method=0 ! capsfilter caps=video/x-raw,width=%d,height=%d name=resolution ! queue", w, h)
2021-03-29 11:58:51 +13:00
}
// get encoder pipeline
2022-09-11 09:24:27 +12:00
encPipeline := fmt.Sprintf("! %s name=encoder", config.GstEncoder)
2021-03-29 11:58:51 +13:00
for key, expr := range config.GstParams {
if expr == "" {
continue
}
val, err := gval.Evaluate(expr, values, language...)
if err != nil {
return "", err
}
if val != nil {
encPipeline += fmt.Sprintf(" %s=%v", key, val)
} else {
encPipeline += fmt.Sprintf(" %s=%s", key, expr)
}
}
// join strings with space
return strings.Join([]string{
fpsPipeline,
scalePipeline,
config.GstPrefix,
encPipeline,
config.GstSuffix,
2021-03-30 11:37:06 +13:00
}[:], " "), nil
2021-03-29 11:58:51 +13:00
}
func (config *VideoConfig) GetBitrateFn(getScreen func() ScreenSize) func() (int, error) {
return func() (int, error) {
if config.Bitrate > 0 {
return config.Bitrate, nil
}
screen := getScreen()
values := map[string]any{
"width": screen.Width,
"height": screen.Height,
"fps": screen.Rate,
}
language := []gval.Language{
gval.Function("round", func(args ...any) (any, error) {
return (int)(math.Round(args[0].(float64))), nil
}),
}
// TOOD: do not read target-bitrate from pipeline, but only from config.
// TODO: This is only for vp8.
expr, ok := config.GstParams["target-bitrate"]
if !ok {
// TODO: This is only for h264.
expr, ok = config.GstParams["bitrate"]
if !ok {
return 0, fmt.Errorf("bitrate not found")
}
}
bitrate, err := gval.Evaluate(expr, values, language...)
if err != nil {
return 0, fmt.Errorf("failed to evaluate bitrate: %w", err)
}
var bitrateInt int
switch val := bitrate.(type) {
case int:
bitrateInt = val
case float64:
bitrateInt = (int)(val)
default:
return 0, fmt.Errorf("bitrate is not int or float64")
}
return bitrateInt, nil
}
}