2020-11-02 04:09:48 +13:00
|
|
|
package types
|
|
|
|
|
2021-02-02 11:50:18 +13:00
|
|
|
import (
|
2021-03-30 11:36:13 +13:00
|
|
|
"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"
|
2023-02-15 09:19:02 +13:00
|
|
|
"time"
|
2021-03-29 11:58:51 +13:00
|
|
|
|
|
|
|
"github.com/PaesslerAG/gval"
|
2022-07-14 10:58:22 +12:00
|
|
|
"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")
|
|
|
|
)
|
|
|
|
|
2023-02-15 09:19:02 +13:00
|
|
|
type Sample struct {
|
2023-03-07 12:08:53 +13:00
|
|
|
// buffer with encoded media
|
|
|
|
Data []byte
|
|
|
|
Length int
|
|
|
|
// timing information
|
|
|
|
Timestamp time.Time
|
2023-02-15 09:19:02 +13:00
|
|
|
Duration time.Duration
|
2023-03-07 12:08:53 +13:00
|
|
|
// metadata
|
2023-02-15 09:19:02 +13:00
|
|
|
DeltaUnit bool // this unit cannot be decoded independently.
|
|
|
|
}
|
2020-11-02 04:09:48 +13:00
|
|
|
|
2023-03-07 12:08:53 +13:00
|
|
|
type SampleListener interface {
|
|
|
|
WriteSample(Sample)
|
|
|
|
}
|
|
|
|
|
2021-01-23 02:09:47 +13:00
|
|
|
type BroadcastManager interface {
|
|
|
|
Start(url string) error
|
|
|
|
Stop()
|
2021-02-06 02:03:53 +13:00
|
|
|
Started() bool
|
2021-01-23 02:09:47 +13:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2023-05-16 05:29:39 +12:00
|
|
|
type StreamSelectorType int
|
|
|
|
|
|
|
|
const (
|
|
|
|
// select exact stream
|
|
|
|
StreamSelectorTypeExact StreamSelectorType = iota
|
|
|
|
// select nearest stream (in either direction) if exact stream is not available
|
|
|
|
StreamSelectorTypeNearest
|
|
|
|
// if exact stream is found select the next lower stream, otherwise select the nearest lower stream
|
|
|
|
StreamSelectorTypeLower
|
|
|
|
// if exact stream is found select the next higher stream, otherwise select the nearest higher stream
|
|
|
|
StreamSelectorTypeHigher
|
|
|
|
)
|
|
|
|
|
|
|
|
func (s StreamSelectorType) String() string {
|
|
|
|
switch s {
|
|
|
|
case StreamSelectorTypeExact:
|
|
|
|
return "exact"
|
|
|
|
case StreamSelectorTypeNearest:
|
|
|
|
return "nearest"
|
|
|
|
case StreamSelectorTypeLower:
|
|
|
|
return "lower"
|
|
|
|
case StreamSelectorTypeHigher:
|
|
|
|
return "higher"
|
|
|
|
default:
|
|
|
|
return fmt.Sprintf("%d", int(s))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *StreamSelectorType) UnmarshalText(text []byte) error {
|
|
|
|
switch strings.ToLower(string(text)) {
|
|
|
|
case "exact", "":
|
|
|
|
*s = StreamSelectorTypeExact
|
|
|
|
case "nearest":
|
|
|
|
*s = StreamSelectorTypeNearest
|
|
|
|
case "lower":
|
|
|
|
*s = StreamSelectorTypeLower
|
|
|
|
case "higher":
|
|
|
|
*s = StreamSelectorTypeHigher
|
|
|
|
default:
|
|
|
|
return fmt.Errorf("invalid stream selector type: %s", string(text))
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s StreamSelectorType) MarshalText() ([]byte, error) {
|
|
|
|
return []byte(s.String()), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type StreamSelector struct {
|
|
|
|
// type of stream selector
|
2023-06-27 07:27:14 +12:00
|
|
|
Type StreamSelectorType `json:"type"`
|
2023-05-16 05:29:39 +12:00
|
|
|
// select stream by its ID
|
2023-06-27 07:27:14 +12:00
|
|
|
ID string `json:"id"`
|
2023-05-16 05:29:39 +12:00
|
|
|
// select stream by its bitrate
|
2023-06-27 07:27:14 +12:00
|
|
|
Bitrate uint64 `json:"bitrate"`
|
2023-05-16 05:29:39 +12:00
|
|
|
}
|
|
|
|
|
|
|
|
type StreamSelectorManager interface {
|
|
|
|
IDs() []string
|
|
|
|
Codec() codec.RTPCodec
|
|
|
|
|
|
|
|
GetStream(selector StreamSelector) (StreamSinkManager, bool)
|
|
|
|
}
|
|
|
|
|
2021-12-02 08:30:18 +13:00
|
|
|
type StreamSinkManager interface {
|
2022-10-26 07:25:00 +13:00
|
|
|
ID() string
|
2021-02-05 09:39:48 +13:00
|
|
|
Codec() codec.RTPCodec
|
2023-05-16 05:29:39 +12:00
|
|
|
Bitrate() uint64
|
2021-02-07 06:16:24 +13:00
|
|
|
|
2023-03-07 12:08:53 +13:00
|
|
|
AddListener(listener SampleListener) error
|
|
|
|
RemoveListener(listener SampleListener) error
|
|
|
|
MoveListenerTo(listener SampleListener, targetStream StreamSinkManager) error
|
2021-02-05 09:39:48 +13:00
|
|
|
|
2021-09-27 11:50:49 +13:00
|
|
|
ListenersCount() int
|
2021-02-06 02:03:53 +13:00
|
|
|
Started() bool
|
2023-02-07 07:45:51 +13:00
|
|
|
|
|
|
|
CreatePipeline() error
|
|
|
|
DestroyPipeline()
|
2021-02-05 09:39:48 +13:00
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
2021-01-23 02:09:47 +13:00
|
|
|
Broadcast() BroadcastManager
|
2021-01-23 06:13:32 +13:00
|
|
|
Screencast() ScreencastManager
|
2021-12-02 08:30:18 +13:00
|
|
|
Audio() StreamSinkManager
|
2023-05-16 05:29:39 +12:00
|
|
|
Video() StreamSelectorManager
|
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
|
2022-10-26 07:25:00 +13:00
|
|
|
Bitrate int `mapstructure:"bitrate"` // pipeline bitrate
|
2021-03-30 11:36:13 +13:00
|
|
|
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
|
2021-03-30 11:36:13 +13:00
|
|
|
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) {
|
2022-07-28 22:20:20 +12:00
|
|
|
values := map[string]any{
|
2021-03-29 11:58:51 +13:00
|
|
|
"width": screen.Width,
|
|
|
|
"height": screen.Height,
|
|
|
|
"fps": screen.Rate,
|
|
|
|
}
|
|
|
|
|
|
|
|
language := []gval.Language{
|
2022-07-28 22:20:20 +12:00
|
|
|
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 != "" {
|
2021-03-30 11:36:13 +13:00
|
|
|
eval, err := gval.Full(language...).NewEvaluable(config.Fps)
|
2021-03-29 11:58:51 +13:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2021-03-30 11:36:13 +13:00
|
|
|
|
|
|
|
val, err := eval.EvalFloat64(context.Background(), values)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
2021-03-29 11:58:51 +13:00
|
|
|
}
|
2021-03-30 11:36:13 +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 != "" {
|
2021-03-30 11:36:13 +13:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-03-30 11:36:13 +13:00
|
|
|
eval, err = gval.Full(language...).NewEvaluable(config.Height)
|
2021-03-29 11:58:51 +13:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
2021-03-30 11:36:13 +13:00
|
|
|
h, err := eval.EvalInt(context.Background(), values)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
2021-03-29 11:58:51 +13:00
|
|
|
}
|
2021-03-30 11:36:13 +13:00
|
|
|
|
2023-02-27 09:57:02 +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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-30 11:36:13 +13:00
|
|
|
// 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
|
|
|
}
|