mirror of
https://github.com/xaionaro-go/streamctl.git
synced 2025-10-31 02:46:27 +08:00
Initial commit, pt. 46
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
"github.com/facebookincubator/go-belt/tool/logger/implementation/logrus"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/kraken-hpc/go-fork"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/xaionaro-go/streamctl/pkg/observability"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol"
|
||||
@@ -26,6 +27,15 @@ import (
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func init() {
|
||||
fork.RegisterFunc("streamd", streamd)
|
||||
fork.Init()
|
||||
}
|
||||
|
||||
func streamd(remoteAddr string) {
|
||||
|
||||
}
|
||||
|
||||
const forceNetPProfOnAndroid = true
|
||||
|
||||
func main() {
|
||||
@@ -39,8 +49,13 @@ func main() {
|
||||
heapProfile := pflag.String("go-profile-heap", "", "file to write memory profile to")
|
||||
sentryDSN := pflag.String("sentry-dsn", "", "DSN of a Sentry instance to send error reports")
|
||||
page := pflag.String("page", string(consts.PageControl), "DSN of a Sentry instance to send error reports")
|
||||
splitProcess := pflag.Bool("split-process", !isMobile(), "split the process into multiple processes for better stability")
|
||||
pflag.Parse()
|
||||
|
||||
l := logrus.Default().WithLevel(loggerLevel)
|
||||
logger.Default = func() logger.Logger {
|
||||
return l
|
||||
}
|
||||
|
||||
if *cpuProfile != "" {
|
||||
f, err := os.Create(*cpuProfile)
|
||||
@@ -81,17 +96,29 @@ func main() {
|
||||
runtime.GOMAXPROCS(16)
|
||||
}
|
||||
|
||||
if *splitProcess && *listenAddr == "" {
|
||||
listenAddr = ptr("localhost:0")
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", *listenAddr)
|
||||
if err != nil {
|
||||
l.Fatalf("failed to listen: %v", err)
|
||||
}
|
||||
|
||||
if *splitProcess {
|
||||
fork.Fork("streamd", listener.Addr().String())
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
listener.Close()
|
||||
}()
|
||||
|
||||
if *splitProcess && *remoteAddr == "" {
|
||||
remoteAddr = ptr(listener.Addr().String())
|
||||
}
|
||||
|
||||
var opts []streampanel.Option
|
||||
if *remoteAddr != "" {
|
||||
opts = append(opts, streampanel.OptionRemoteStreamDAddr(*remoteAddr))
|
||||
@@ -101,7 +128,7 @@ func main() {
|
||||
}
|
||||
panel, panelErr := streampanel.New(*configPath, opts...)
|
||||
|
||||
if panel.Config.SentryDSN != "" {
|
||||
if panel != nil && panel.Config.SentryDSN != "" {
|
||||
l.Infof("setting up Sentry at DSN '%s'", panel.Config.SentryDSN)
|
||||
sentryClient, err := sentry.NewClient(sentry.ClientOptions{
|
||||
Dsn: panel.Config.SentryDSN,
|
||||
|
||||
3
go.mod
3
go.mod
@@ -210,6 +210,7 @@ require (
|
||||
github.com/andreykaipov/goobs v1.4.1
|
||||
github.com/anthonynsimon/bild v0.14.0
|
||||
github.com/chai2010/webp v1.1.1
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/getsentry/sentry-go v0.28.1
|
||||
github.com/go-git/go-git/v5 v5.12.0
|
||||
github.com/go-ng/xmath v0.0.0-20230704233441-028f5ea62335
|
||||
@@ -218,12 +219,14 @@ require (
|
||||
github.com/hyprspace/hyprspace v0.10.1
|
||||
github.com/immune-gmbh/attestation-sdk v0.0.0-20230711173209-f44e4502aeca
|
||||
github.com/kbinani/screenshot v0.0.0-20230812210009-b87d31814237
|
||||
github.com/kraken-hpc/go-fork v0.1.1
|
||||
github.com/libp2p/go-libp2p v0.33.2
|
||||
github.com/libp2p/go-libp2p-kad-dht v0.25.2
|
||||
github.com/multiformats/go-multiaddr v0.12.3
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||
github.com/prometheus/client_golang v1.18.0
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/sethvargo/go-password v0.3.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/xaionaro-go/datacounter v1.0.4
|
||||
github.com/xaionaro-go/unsafetools v0.0.0-20210722164218-75ba48cf7b3c
|
||||
|
||||
6
go.sum
6
go.sum
@@ -139,6 +139,8 @@ github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
|
||||
github.com/elastic/gosigar v0.14.2 h1:Dg80n8cr90OZ7x+bAax/QjoW/XqTI11RmA79ZwIm9/4=
|
||||
github.com/elastic/gosigar v0.14.2/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
|
||||
@@ -452,6 +454,8 @@ github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kraken-hpc/go-fork v0.1.1 h1:O3X/ynoNy/eS7UIcZYef8ndFq2RXEIOue9kZqyzF0Sk=
|
||||
github.com/kraken-hpc/go-fork v0.1.1/go.mod h1:uu0e5h+V4ONH5Qk/xuVlyNXJXy/swhqGIEMK7w+9dNc=
|
||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
|
||||
@@ -650,6 +654,8 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU=
|
||||
github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs=
|
||||
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
||||
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
|
||||
|
||||
119
pkg/mainprocess/client.go
Normal file
119
pkg/mainprocess/client.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package mainprocess
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Conn net.Conn
|
||||
Password string
|
||||
OnReceivedMessage OnReceivedMessageFunc
|
||||
}
|
||||
|
||||
func NewClient(
|
||||
myName string,
|
||||
addr string,
|
||||
password string,
|
||||
onReceivedMessage OnReceivedMessageFunc,
|
||||
) (*Client, error) {
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to connect to '%s': %w", addr, err)
|
||||
}
|
||||
logger.Default().Tracef("connected to '%s' as '%s'", conn.RemoteAddr(), conn.LocalAddr())
|
||||
|
||||
msg := RegistrationMessage{
|
||||
Password: password,
|
||||
Source: myName,
|
||||
}
|
||||
encoder := gob.NewEncoder(conn)
|
||||
if err := encoder.Encode(msg); err != nil {
|
||||
return nil, fmt.Errorf("unable to encode&send the registration message %#+v: %w", msg, err)
|
||||
}
|
||||
|
||||
var regResult RegistrationResult
|
||||
decoder := gob.NewDecoder(conn)
|
||||
if err := decoder.Decode(®Result); err != nil {
|
||||
return nil, fmt.Errorf("unable to decode&receive the registration result: %w", err)
|
||||
}
|
||||
if regResult.Error != "" {
|
||||
return nil, fmt.Errorf("registration error: %s", regResult.Error)
|
||||
}
|
||||
logger.Default().Tracef("successfully registered the process '%s'", myName)
|
||||
|
||||
return &Client{
|
||||
Conn: conn,
|
||||
Password: password,
|
||||
OnReceivedMessage: onReceivedMessage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) SendMessage(
|
||||
ctx context.Context,
|
||||
dst string,
|
||||
content any,
|
||||
) error {
|
||||
encoder := gob.NewEncoder(c.Conn)
|
||||
msg := MessageToMain{
|
||||
Password: c.Password,
|
||||
Destination: dst,
|
||||
Content: content,
|
||||
}
|
||||
err := encoder.Encode(msg)
|
||||
logger.Tracef(ctx, "sending message %#+v: %v", msg, err)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to encode&send message %#+v: %w", msg, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
func (c *Client) Serve(ctx context.Context) error {
|
||||
ctx, cancelFn := context.WithCancel(ctx)
|
||||
defer cancelFn()
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
err := c.Close()
|
||||
if err != nil {
|
||||
logger.Error(ctx, err)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
var msg MessageFromMain
|
||||
decoder := gob.NewDecoder(c.Conn)
|
||||
err := decoder.Decode(&msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to receive&decode message: %w", err)
|
||||
}
|
||||
|
||||
if err := c.onReceivedMessage(ctx, msg); err != nil {
|
||||
logger.Error(ctx, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) onReceivedMessage(
|
||||
ctx context.Context,
|
||||
msg MessageFromMain,
|
||||
) error {
|
||||
if c.OnReceivedMessage == nil {
|
||||
return fmt.Errorf("OnReceivedMessage function is not set")
|
||||
}
|
||||
|
||||
return c.OnReceivedMessage(ctx, msg.Source, msg.Content)
|
||||
}
|
||||
382
pkg/mainprocess/main_process.go
Normal file
382
pkg/mainprocess/main_process.go
Normal file
@@ -0,0 +1,382 @@
|
||||
package mainprocess
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/facebookincubator/go-belt"
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/sethvargo/go-password/password"
|
||||
)
|
||||
|
||||
type OnReceivedMessageFunc func(
|
||||
ctx context.Context,
|
||||
source string,
|
||||
content any,
|
||||
) error
|
||||
|
||||
type Manager struct {
|
||||
listener net.Listener
|
||||
password string
|
||||
|
||||
connsLocker sync.Mutex
|
||||
conns map[string]net.Conn
|
||||
connsChanged chan struct{}
|
||||
|
||||
allClientProcesses []string
|
||||
|
||||
OnReceivedMessage OnReceivedMessageFunc
|
||||
}
|
||||
|
||||
func NewManager(
|
||||
onReceivedMessage OnReceivedMessageFunc,
|
||||
expectedClients ...string,
|
||||
) (*Manager, error) {
|
||||
listener, err := net.Listen("tcp", "localhost:0")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to listen: %w", err)
|
||||
}
|
||||
|
||||
password, err := password.Generate(16, 4, 4, false, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to generate a password: %w", err)
|
||||
}
|
||||
|
||||
return &Manager{
|
||||
OnReceivedMessage: onReceivedMessage,
|
||||
|
||||
listener: listener,
|
||||
password: password,
|
||||
|
||||
conns: map[string]net.Conn{},
|
||||
connsChanged: make(chan struct{}),
|
||||
|
||||
allClientProcesses: expectedClients,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Password() string {
|
||||
return m.password
|
||||
}
|
||||
|
||||
func (m *Manager) Addr() net.Addr {
|
||||
return m.listener.Addr()
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
return m.listener.Close()
|
||||
}
|
||||
|
||||
func (m *Manager) Serve(ctx context.Context) error {
|
||||
logger.Tracef(ctx, "serving listener at %s", m.listener.Addr())
|
||||
defer logger.Tracef(ctx, "/serving listener at %s", m.listener.Addr())
|
||||
|
||||
ctx, cancelFn := context.WithCancel(ctx)
|
||||
defer cancelFn()
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
err := m.Close()
|
||||
if err != nil {
|
||||
logger.Error(ctx, err)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
conn, err := m.listener.Accept()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to accept connection: %w", err)
|
||||
}
|
||||
logger.Tracef(ctx, "accepted a connection from '%s'", conn.RemoteAddr())
|
||||
|
||||
m.addNewConnection(ctx, conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) addNewConnection(
|
||||
ctx context.Context,
|
||||
conn net.Conn,
|
||||
) {
|
||||
go func() {
|
||||
m.handleConnection(ctx, conn)
|
||||
}()
|
||||
}
|
||||
|
||||
func (m *Manager) handleConnection(
|
||||
ctx context.Context,
|
||||
conn net.Conn,
|
||||
) {
|
||||
var regMessage RegistrationMessage
|
||||
logger.Tracef(ctx, "handleConnection from %s", conn.RemoteAddr())
|
||||
defer func() { logger.Tracef(ctx, "/handleConnection from %s (%s)", conn.RemoteAddr(), regMessage.Source) }()
|
||||
|
||||
ctx, cancelFn := context.WithCancel(ctx)
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
conn.Close()
|
||||
}()
|
||||
defer cancelFn()
|
||||
|
||||
encoder := gob.NewEncoder(conn)
|
||||
|
||||
decoder := gob.NewDecoder(conn)
|
||||
err := decoder.Decode(®Message)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("unable to decode registration message: %w", err)
|
||||
encoder.Encode(RegistrationResult{Error: err.Error()})
|
||||
logger.Debug(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debugf(ctx, "received registration message: %#+v", regMessage)
|
||||
if err := m.checkPassword(regMessage.Password); err != nil {
|
||||
regMessage = RegistrationMessage{}
|
||||
err = fmt.Errorf("invalid password: %w", err)
|
||||
encoder.Encode(RegistrationResult{Error: err.Error()})
|
||||
logger.Warn(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := m.registerConnection(regMessage.Source, conn); err != nil {
|
||||
err = fmt.Errorf("unable to register process '%s': %w", regMessage.Source, err)
|
||||
encoder.Encode(RegistrationResult{Error: err.Error()})
|
||||
logger.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
defer func(sourceName string) {
|
||||
m.unregisterConnection(sourceName)
|
||||
}(regMessage.Source)
|
||||
if err := encoder.Encode(RegistrationResult{}); err != nil {
|
||||
err = fmt.Errorf("unable to encode&send the registration result to '%s': %w", regMessage.Source, err)
|
||||
logger.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
ctx = belt.WithField(ctx, "client", regMessage.Source)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
logger.Tracef(ctx, "context was closed")
|
||||
return
|
||||
default:
|
||||
}
|
||||
var message MessageToMain
|
||||
logger.Tracef(ctx, "waiting for a message from '%s'", regMessage.Source)
|
||||
decoder := gob.NewDecoder(conn)
|
||||
err := decoder.Decode(&message)
|
||||
logger.Tracef(ctx, "getting a message from '%s': %#+v %#+v", regMessage.Source, message, err)
|
||||
if err != nil {
|
||||
err = fmt.Errorf(
|
||||
"unable to parse the message from %s (%s): %w",
|
||||
regMessage.Source,
|
||||
conn.RemoteAddr().String(),
|
||||
err,
|
||||
)
|
||||
logger.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.processMessage(ctx, regMessage.Source, message); err != nil {
|
||||
logger.Errorf(
|
||||
ctx,
|
||||
"unable to process the message %#+v from %s (%s): %w",
|
||||
message, regMessage.Source, conn.RemoteAddr().String(), err,
|
||||
)
|
||||
}
|
||||
logger.Tracef(ctx, "next iteration")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) processMessage(
|
||||
ctx context.Context,
|
||||
source string,
|
||||
message MessageToMain,
|
||||
) (_ret error) {
|
||||
logger.Tracef(ctx, "processing message from '%s': %#+v", source, message)
|
||||
defer func() { logger.Tracef(ctx, "/processing message from '%s': %#+v: %v", source, message, _ret) }()
|
||||
|
||||
switch message.Destination {
|
||||
case "":
|
||||
logger.Tracef(ctx, "a broadcast message from '%s': %#+v", source, message.Content)
|
||||
var wg sync.WaitGroup
|
||||
var err *multierror.Error
|
||||
err = multierror.Append(err, m.onReceivedMessage(ctx, source, message.Content))
|
||||
|
||||
errCh := make(chan error)
|
||||
go func() {
|
||||
for e := range errCh {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
}()
|
||||
for _, dst := range m.allClientProcesses {
|
||||
if dst == source {
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(dst string) {
|
||||
defer wg.Done()
|
||||
errCh <- m.sendMessage(ctx, source, dst, message.Content)
|
||||
}(dst)
|
||||
}
|
||||
wg.Wait()
|
||||
close(errCh)
|
||||
return err.ErrorOrNil()
|
||||
case "main":
|
||||
logger.Tracef(ctx, "a message to the main process from '%s': %#+v", source, message.Content)
|
||||
return m.onReceivedMessage(ctx, source, message.Content)
|
||||
default:
|
||||
logger.Tracef(ctx, "a message to '%s' from '%s': %#+v", message.Destination, source, message.Content)
|
||||
return m.sendMessage(ctx, source, message.Destination, message.Content)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) onReceivedMessage(
|
||||
ctx context.Context,
|
||||
source string,
|
||||
content any,
|
||||
) error {
|
||||
if m.OnReceivedMessage == nil {
|
||||
err := fmt.Errorf("OnReceivedMessage is not set")
|
||||
logger.Tracef(ctx, "%v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Tracef(ctx, "calling the OnReceivedMessage function")
|
||||
return m.OnReceivedMessage(ctx, source, content)
|
||||
}
|
||||
|
||||
type MessageFromMain struct {
|
||||
Source string
|
||||
Password string
|
||||
Destination string
|
||||
Content any
|
||||
}
|
||||
|
||||
func (m *Manager) isExpectedProcess(
|
||||
name string,
|
||||
) bool {
|
||||
for _, p := range m.allClientProcesses {
|
||||
if name == p {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Manager) sendMessage(
|
||||
ctx context.Context,
|
||||
source string,
|
||||
destination string,
|
||||
content any,
|
||||
) (_ret error) {
|
||||
logger.Tracef(ctx, "sending message message %#+v from '%s' to '%s'", content, source, destination)
|
||||
defer func() {
|
||||
logger.Tracef(ctx, "/sending message message %#+v from '%s' to '%s': %v", content, source, destination, _ret)
|
||||
}()
|
||||
|
||||
if !m.isExpectedProcess(destination) {
|
||||
return fmt.Errorf("process '%s' is not ever expected", destination)
|
||||
}
|
||||
|
||||
message := MessageFromMain{
|
||||
Source: source,
|
||||
Password: m.password,
|
||||
Destination: destination,
|
||||
Content: content,
|
||||
}
|
||||
|
||||
conn, err := m.waitForProcess(destination)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to wait for process '%s': %w", destination, err)
|
||||
}
|
||||
|
||||
encoder := gob.NewEncoder(conn)
|
||||
err = encoder.Encode(message)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to encode&send message: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) waitForProcess(
|
||||
name string,
|
||||
) (net.Conn, error) {
|
||||
if !m.isExpectedProcess(name) {
|
||||
return nil, fmt.Errorf("process '%s' is not ever expected", name)
|
||||
}
|
||||
|
||||
for {
|
||||
m.connsLocker.Lock()
|
||||
conn := m.conns[name]
|
||||
ch := m.connsChanged
|
||||
m.connsLocker.Unlock()
|
||||
|
||||
if conn != nil {
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
<-ch
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) checkPassword(
|
||||
password string,
|
||||
) error {
|
||||
return checkPassword(m.password, password)
|
||||
}
|
||||
|
||||
func (m *Manager) registerConnection(
|
||||
sourceName string,
|
||||
conn net.Conn,
|
||||
) error {
|
||||
if !m.isExpectedProcess(sourceName) {
|
||||
return fmt.Errorf("process '%s' is not ever expected", sourceName)
|
||||
}
|
||||
|
||||
m.connsLocker.Lock()
|
||||
defer m.connsLocker.Unlock()
|
||||
if conn, ok := m.conns[sourceName]; ok {
|
||||
return fmt.Errorf("process '%s' is already registered at %s", sourceName, conn.RemoteAddr().String())
|
||||
}
|
||||
m.conns[sourceName] = conn
|
||||
var oldCh chan struct{}
|
||||
oldCh, m.connsChanged = m.connsChanged, make(chan struct{})
|
||||
close(oldCh)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) unregisterConnection(
|
||||
sourceName string,
|
||||
) {
|
||||
m.connsLocker.Lock()
|
||||
defer m.connsLocker.Unlock()
|
||||
delete(m.conns, sourceName)
|
||||
var oldCh chan struct{}
|
||||
oldCh, m.connsChanged = m.connsChanged, make(chan struct{})
|
||||
close(oldCh)
|
||||
}
|
||||
|
||||
type RegistrationMessage struct {
|
||||
Password string
|
||||
Source string
|
||||
}
|
||||
|
||||
type RegistrationResult struct {
|
||||
Error string
|
||||
}
|
||||
|
||||
type MessageToMain struct {
|
||||
Password string
|
||||
Destination string
|
||||
Content any
|
||||
}
|
||||
27
pkg/mainprocess/password.go
Normal file
27
pkg/mainprocess/password.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package mainprocess
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func checkPassword(
|
||||
a, b string,
|
||||
) error {
|
||||
// naive mostly-timing-attack-resistant comparison algo
|
||||
|
||||
h0 := sha1.Sum([]byte(a))
|
||||
h1 := sha1.Sum([]byte(b))
|
||||
|
||||
match := true
|
||||
for idx := range h0 {
|
||||
charMatches := h0[idx] == h1[idx]
|
||||
match = match && charMatches
|
||||
}
|
||||
|
||||
if !match {
|
||||
return fmt.Errorf("the password does not match")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
104
pkg/mainprocess/unit_test.go
Normal file
104
pkg/mainprocess/unit_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package mainprocess
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/facebookincubator/go-belt"
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
"github.com/facebookincubator/go-belt/tool/logger/implementation/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
l := logrus.Default().WithLevel(logger.LevelTrace)
|
||||
logger.Default = func() logger.Logger {
|
||||
return l
|
||||
}
|
||||
ctx := logger.CtxWithLogger(context.Background(), l)
|
||||
defer belt.Flush(ctx)
|
||||
ctx, cancelFunc := context.WithCancel(ctx)
|
||||
defer cancelFunc()
|
||||
|
||||
type messageContent struct {
|
||||
Integer int
|
||||
String string
|
||||
}
|
||||
gob.Register(messageContent{})
|
||||
|
||||
handleCallHappened := map[string]chan struct{}{
|
||||
"main": make(chan struct{}),
|
||||
"child0": make(chan struct{}),
|
||||
"child1": make(chan struct{}),
|
||||
}
|
||||
callCount := map[string]int{}
|
||||
|
||||
handleCall := func(procName string, content any) {
|
||||
logger.Tracef(ctx, "handleCall('%s', %#+v)", procName, content)
|
||||
count := callCount[procName]
|
||||
count++
|
||||
callCount[procName] = count
|
||||
msg := content.(messageContent)
|
||||
assert.Equal(t, count, msg.Integer, procName)
|
||||
assert.Equal(t, fmt.Sprint(count), msg.String, procName)
|
||||
var oldCh chan struct{}
|
||||
oldCh, handleCallHappened[procName] = handleCallHappened[procName], make(chan struct{})
|
||||
close(oldCh)
|
||||
}
|
||||
|
||||
m, err := NewManager(
|
||||
func(ctx context.Context, source string, content any) error {
|
||||
handleCall("main", content)
|
||||
return nil
|
||||
},
|
||||
"child0", "child1",
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer m.Close()
|
||||
go m.Serve(belt.WithField(ctx, "process", "main"))
|
||||
|
||||
c0, err := NewClient("child0", m.Addr().String(), m.Password(), func(ctx context.Context, source string, content any) error {
|
||||
handleCall("child0", content)
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer c0.Close()
|
||||
go c0.Serve(belt.WithField(ctx, "process", "child0"))
|
||||
|
||||
c1, err := NewClient("child1", m.Addr().String(), m.Password(), func(ctx context.Context, source string, content any) error {
|
||||
handleCall("child1", content)
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer c1.Close()
|
||||
go c1.Serve(belt.WithField(ctx, "process", "child1"))
|
||||
|
||||
_, err = NewClient("child2", m.Addr().String(), m.Password(), func(ctx context.Context, source string, content any) error {
|
||||
return nil
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
waitCh0 := handleCallHappened["main"]
|
||||
waitCh1 := handleCallHappened["child1"]
|
||||
err = c0.SendMessage(ctx, "", messageContent{Integer: 1, String: "1"})
|
||||
require.NoError(t, err)
|
||||
<-waitCh0
|
||||
<-waitCh1
|
||||
|
||||
waitCh0 = handleCallHappened["main"]
|
||||
err = c1.SendMessage(ctx, "main", messageContent{Integer: 2, String: "2"})
|
||||
require.NoError(t, err)
|
||||
<-waitCh0
|
||||
|
||||
waitCh0 = handleCallHappened["child0"]
|
||||
err = c1.SendMessage(ctx, "child0", messageContent{Integer: 1, String: "1"})
|
||||
require.NoError(t, err)
|
||||
<-waitCh0
|
||||
|
||||
require.Equal(t, 2, callCount["main"])
|
||||
require.Equal(t, 1, callCount["child0"])
|
||||
require.Equal(t, 1, callCount["child1"])
|
||||
}
|
||||
@@ -79,6 +79,14 @@ type StreamD interface {
|
||||
ctx context.Context,
|
||||
listenAddr string,
|
||||
) error
|
||||
AddIncomingStream(
|
||||
ctx context.Context,
|
||||
streamID StreamID,
|
||||
) error
|
||||
RemoveIncomingStream(
|
||||
ctx context.Context,
|
||||
streamID StreamID,
|
||||
) error
|
||||
ListIncomingStreams(
|
||||
ctx context.Context,
|
||||
) ([]IncomingStream, error)
|
||||
@@ -99,8 +107,15 @@ type StreamD interface {
|
||||
) ([]StreamForward, error)
|
||||
AddStreamForward(
|
||||
ctx context.Context,
|
||||
streamIDSrc StreamID,
|
||||
streamID StreamID,
|
||||
destinationID DestinationID,
|
||||
enabled bool,
|
||||
) error
|
||||
UpdateStreamForward(
|
||||
ctx context.Context,
|
||||
streamID StreamID,
|
||||
destinationID DestinationID,
|
||||
enabled bool,
|
||||
) error
|
||||
RemoveStreamForward(
|
||||
ctx context.Context,
|
||||
@@ -153,6 +168,9 @@ func ParseStreamServerType(in string) StreamServerType {
|
||||
type StreamServer struct {
|
||||
Type StreamServerType
|
||||
ListenAddr string
|
||||
|
||||
NumBytesConsumerWrote uint64
|
||||
NumBytesProducerRead uint64
|
||||
}
|
||||
|
||||
type StreamDestination struct {
|
||||
@@ -161,8 +179,11 @@ type StreamDestination struct {
|
||||
}
|
||||
|
||||
type StreamForward struct {
|
||||
Enabled bool
|
||||
StreamID StreamID
|
||||
DestinationID DestinationID
|
||||
NumBytesWrote uint64
|
||||
NumBytesRead uint64
|
||||
}
|
||||
|
||||
type IncomingStream struct {
|
||||
|
||||
@@ -645,13 +645,15 @@ func (c *Client) ListStreamServers(
|
||||
}
|
||||
var result []api.StreamServer
|
||||
for _, server := range reply.GetStreamServers() {
|
||||
t, err := grpcconv.StreamServerTypeGRPC2Go(server.GetServerType())
|
||||
t, err := grpcconv.StreamServerTypeGRPC2Go(server.Config.GetServerType())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to convert the server type value: %w", err)
|
||||
}
|
||||
result = append(result, api.StreamServer{
|
||||
Type: t,
|
||||
ListenAddr: server.GetListenAddr(),
|
||||
Type: t,
|
||||
ListenAddr: server.Config.GetListenAddr(),
|
||||
NumBytesConsumerWrote: uint64(server.GetStatistics().GetNumBytesConsumerWrote()),
|
||||
NumBytesProducerRead: uint64(server.GetStatistics().GetNumBytesProducerRead()),
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
@@ -703,6 +705,44 @@ func (c *Client) StopStreamServer(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) AddIncomingStream(
|
||||
ctx context.Context,
|
||||
streamID api.StreamID,
|
||||
) error {
|
||||
client, conn, err := c.grpcClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = client.AddIncomingStream(ctx, &streamd_grpc.AddIncomingStreamRequest{
|
||||
StreamID: string(streamID),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to request to add the incoming stream: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) RemoveIncomingStream(
|
||||
ctx context.Context,
|
||||
streamID api.StreamID,
|
||||
) error {
|
||||
client, conn, err := c.grpcClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = client.RemoveIncomingStream(ctx, &streamd_grpc.RemoveIncomingStreamRequest{
|
||||
StreamID: string(streamID),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to request to remove the incoming stream: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) ListIncomingStreams(
|
||||
ctx context.Context,
|
||||
) ([]api.IncomingStream, error) {
|
||||
@@ -809,8 +849,11 @@ func (c *Client) ListStreamForwards(
|
||||
var result []api.StreamForward
|
||||
for _, forward := range reply.GetStreamForwards() {
|
||||
result = append(result, api.StreamForward{
|
||||
StreamID: api.StreamID(forward.GetStreamID()),
|
||||
DestinationID: api.DestinationID(forward.GetDestinationID()),
|
||||
Enabled: forward.Config.Enabled,
|
||||
StreamID: api.StreamID(forward.Config.GetStreamID()),
|
||||
DestinationID: api.DestinationID(forward.Config.GetDestinationID()),
|
||||
NumBytesWrote: uint64(forward.Statistics.NumBytesWrote),
|
||||
NumBytesRead: uint64(forward.Statistics.NumBytesRead),
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
@@ -820,6 +863,7 @@ func (c *Client) AddStreamForward(
|
||||
ctx context.Context,
|
||||
streamID api.StreamID,
|
||||
destinationID api.DestinationID,
|
||||
enabled bool,
|
||||
) error {
|
||||
client, conn, err := c.grpcClient()
|
||||
if err != nil {
|
||||
@@ -831,6 +875,32 @@ func (c *Client) AddStreamForward(
|
||||
Config: &streamd_grpc.StreamForward{
|
||||
StreamID: string(streamID),
|
||||
DestinationID: string(destinationID),
|
||||
Enabled: enabled,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to request to add the stream forward: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) UpdateStreamForward(
|
||||
ctx context.Context,
|
||||
streamID api.StreamID,
|
||||
destinationID api.DestinationID,
|
||||
enabled bool,
|
||||
) error {
|
||||
client, conn, err := c.grpcClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = client.UpdateStreamForward(ctx, &streamd_grpc.UpdateStreamForwardRequest{
|
||||
Config: &streamd_grpc.StreamForward{
|
||||
StreamID: string(streamID),
|
||||
DestinationID: string(destinationID),
|
||||
Enabled: enabled,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -19,12 +19,6 @@ type ProfileMetadata struct {
|
||||
MaxOrder int
|
||||
}
|
||||
|
||||
type GitRepoConfig struct {
|
||||
Enable *bool
|
||||
URL string `yaml:"url,omitempty"`
|
||||
PrivateKey string `yaml:"private_key,omitempty"`
|
||||
LatestSyncCommit string `yaml:"latest_sync_commit,omitempty"` // TODO: deprecate this field, it's just a non-needed mechanism (better to check against git history).
|
||||
}
|
||||
|
||||
type config struct {
|
||||
CachePath *string `yaml:"cache_path"`
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/obs"
|
||||
@@ -22,11 +24,32 @@ func (cfg *Config) Read(
|
||||
return len(b), cfg.UnmarshalYAML(b)
|
||||
}
|
||||
|
||||
func (cfg *Config) traceDump() {
|
||||
l := logger.Default()
|
||||
if l.Level() < logger.LevelTrace {
|
||||
return
|
||||
}
|
||||
if cfg == nil {
|
||||
l.Tracef("streamd config == nil")
|
||||
return
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err := cfg.WriteTo(&buf)
|
||||
if err != nil {
|
||||
l.Error(err)
|
||||
return
|
||||
}
|
||||
l.Tracef("streamd config == %#+v: %s", *cfg, buf.String())
|
||||
}
|
||||
|
||||
func (cfg *Config) UnmarshalYAML(b []byte) error {
|
||||
logger.Default().Tracef("unparsed streamd config == %s", b)
|
||||
err := yaml.Unmarshal(b, (*config)(cfg))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to unserialize data: %w", err)
|
||||
}
|
||||
cfg.traceDump()
|
||||
|
||||
if cfg.Backends == nil {
|
||||
cfg.Backends = streamcontrol.Config{}
|
||||
|
||||
@@ -45,12 +45,12 @@ func (cfg Config) MarshalYAML() ([]byte, error) {
|
||||
m := map[string]any{}
|
||||
err = goyaml.Unmarshal(b, &m)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to unserialize data %#+v: %w", cfg, err)
|
||||
return nil, fmt.Errorf("unable to unserialize data %s: %w", b, err)
|
||||
}
|
||||
|
||||
b, err = goyaml.Marshal(m)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to re-serialize data %#+v: %w", cfg, err)
|
||||
return nil, fmt.Errorf("unable to re-serialize data %#+v: %w", m, err)
|
||||
}
|
||||
|
||||
return b, nil
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -53,9 +53,12 @@ type StreamDClient interface {
|
||||
ListStreamDestinations(ctx context.Context, in *ListStreamDestinationsRequest, opts ...grpc.CallOption) (*ListStreamDestinationsReply, error)
|
||||
AddStreamDestination(ctx context.Context, in *AddStreamDestinationRequest, opts ...grpc.CallOption) (*AddStreamDestinationReply, error)
|
||||
RemoveStreamDestination(ctx context.Context, in *RemoveStreamDestinationRequest, opts ...grpc.CallOption) (*RemoveStreamDestinationReply, error)
|
||||
AddIncomingStream(ctx context.Context, in *AddIncomingStreamRequest, opts ...grpc.CallOption) (*AddIncomingStreamReply, error)
|
||||
RemoveIncomingStream(ctx context.Context, in *RemoveIncomingStreamRequest, opts ...grpc.CallOption) (*RemoveIncomingStreamReply, error)
|
||||
ListIncomingStreams(ctx context.Context, in *ListIncomingStreamsRequest, opts ...grpc.CallOption) (*ListIncomingStreamsReply, error)
|
||||
ListStreamForwards(ctx context.Context, in *ListStreamForwardsRequest, opts ...grpc.CallOption) (*ListStreamForwardsReply, error)
|
||||
AddStreamForward(ctx context.Context, in *AddStreamForwardRequest, opts ...grpc.CallOption) (*AddStreamForwardReply, error)
|
||||
UpdateStreamForward(ctx context.Context, in *UpdateStreamForwardRequest, opts ...grpc.CallOption) (*UpdateStreamForwardReply, error)
|
||||
RemoveStreamForward(ctx context.Context, in *RemoveStreamForwardRequest, opts ...grpc.CallOption) (*RemoveStreamForwardReply, error)
|
||||
}
|
||||
|
||||
@@ -369,6 +372,24 @@ func (c *streamDClient) RemoveStreamDestination(ctx context.Context, in *RemoveS
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *streamDClient) AddIncomingStream(ctx context.Context, in *AddIncomingStreamRequest, opts ...grpc.CallOption) (*AddIncomingStreamReply, error) {
|
||||
out := new(AddIncomingStreamReply)
|
||||
err := c.cc.Invoke(ctx, "/StreamD/AddIncomingStream", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *streamDClient) RemoveIncomingStream(ctx context.Context, in *RemoveIncomingStreamRequest, opts ...grpc.CallOption) (*RemoveIncomingStreamReply, error) {
|
||||
out := new(RemoveIncomingStreamReply)
|
||||
err := c.cc.Invoke(ctx, "/StreamD/RemoveIncomingStream", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *streamDClient) ListIncomingStreams(ctx context.Context, in *ListIncomingStreamsRequest, opts ...grpc.CallOption) (*ListIncomingStreamsReply, error) {
|
||||
out := new(ListIncomingStreamsReply)
|
||||
err := c.cc.Invoke(ctx, "/StreamD/ListIncomingStreams", in, out, opts...)
|
||||
@@ -396,6 +417,15 @@ func (c *streamDClient) AddStreamForward(ctx context.Context, in *AddStreamForwa
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *streamDClient) UpdateStreamForward(ctx context.Context, in *UpdateStreamForwardRequest, opts ...grpc.CallOption) (*UpdateStreamForwardReply, error) {
|
||||
out := new(UpdateStreamForwardReply)
|
||||
err := c.cc.Invoke(ctx, "/StreamD/UpdateStreamForward", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *streamDClient) RemoveStreamForward(ctx context.Context, in *RemoveStreamForwardRequest, opts ...grpc.CallOption) (*RemoveStreamForwardReply, error) {
|
||||
out := new(RemoveStreamForwardReply)
|
||||
err := c.cc.Invoke(ctx, "/StreamD/RemoveStreamForward", in, out, opts...)
|
||||
@@ -440,9 +470,12 @@ type StreamDServer interface {
|
||||
ListStreamDestinations(context.Context, *ListStreamDestinationsRequest) (*ListStreamDestinationsReply, error)
|
||||
AddStreamDestination(context.Context, *AddStreamDestinationRequest) (*AddStreamDestinationReply, error)
|
||||
RemoveStreamDestination(context.Context, *RemoveStreamDestinationRequest) (*RemoveStreamDestinationReply, error)
|
||||
AddIncomingStream(context.Context, *AddIncomingStreamRequest) (*AddIncomingStreamReply, error)
|
||||
RemoveIncomingStream(context.Context, *RemoveIncomingStreamRequest) (*RemoveIncomingStreamReply, error)
|
||||
ListIncomingStreams(context.Context, *ListIncomingStreamsRequest) (*ListIncomingStreamsReply, error)
|
||||
ListStreamForwards(context.Context, *ListStreamForwardsRequest) (*ListStreamForwardsReply, error)
|
||||
AddStreamForward(context.Context, *AddStreamForwardRequest) (*AddStreamForwardReply, error)
|
||||
UpdateStreamForward(context.Context, *UpdateStreamForwardRequest) (*UpdateStreamForwardReply, error)
|
||||
RemoveStreamForward(context.Context, *RemoveStreamForwardRequest) (*RemoveStreamForwardReply, error)
|
||||
mustEmbedUnimplementedStreamDServer()
|
||||
}
|
||||
@@ -544,6 +577,12 @@ func (UnimplementedStreamDServer) AddStreamDestination(context.Context, *AddStre
|
||||
func (UnimplementedStreamDServer) RemoveStreamDestination(context.Context, *RemoveStreamDestinationRequest) (*RemoveStreamDestinationReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RemoveStreamDestination not implemented")
|
||||
}
|
||||
func (UnimplementedStreamDServer) AddIncomingStream(context.Context, *AddIncomingStreamRequest) (*AddIncomingStreamReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AddIncomingStream not implemented")
|
||||
}
|
||||
func (UnimplementedStreamDServer) RemoveIncomingStream(context.Context, *RemoveIncomingStreamRequest) (*RemoveIncomingStreamReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RemoveIncomingStream not implemented")
|
||||
}
|
||||
func (UnimplementedStreamDServer) ListIncomingStreams(context.Context, *ListIncomingStreamsRequest) (*ListIncomingStreamsReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListIncomingStreams not implemented")
|
||||
}
|
||||
@@ -553,6 +592,9 @@ func (UnimplementedStreamDServer) ListStreamForwards(context.Context, *ListStrea
|
||||
func (UnimplementedStreamDServer) AddStreamForward(context.Context, *AddStreamForwardRequest) (*AddStreamForwardReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AddStreamForward not implemented")
|
||||
}
|
||||
func (UnimplementedStreamDServer) UpdateStreamForward(context.Context, *UpdateStreamForwardRequest) (*UpdateStreamForwardReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateStreamForward not implemented")
|
||||
}
|
||||
func (UnimplementedStreamDServer) RemoveStreamForward(context.Context, *RemoveStreamForwardRequest) (*RemoveStreamForwardReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RemoveStreamForward not implemented")
|
||||
}
|
||||
@@ -1130,6 +1172,42 @@ func _StreamD_RemoveStreamDestination_Handler(srv interface{}, ctx context.Conte
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StreamD_AddIncomingStream_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AddIncomingStreamRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(StreamDServer).AddIncomingStream(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/StreamD/AddIncomingStream",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(StreamDServer).AddIncomingStream(ctx, req.(*AddIncomingStreamRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StreamD_RemoveIncomingStream_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RemoveIncomingStreamRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(StreamDServer).RemoveIncomingStream(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/StreamD/RemoveIncomingStream",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(StreamDServer).RemoveIncomingStream(ctx, req.(*RemoveIncomingStreamRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StreamD_ListIncomingStreams_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListIncomingStreamsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -1184,6 +1262,24 @@ func _StreamD_AddStreamForward_Handler(srv interface{}, ctx context.Context, dec
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StreamD_UpdateStreamForward_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(UpdateStreamForwardRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(StreamDServer).UpdateStreamForward(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/StreamD/UpdateStreamForward",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(StreamDServer).UpdateStreamForward(ctx, req.(*UpdateStreamForwardRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StreamD_RemoveStreamForward_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RemoveStreamForwardRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -1329,6 +1425,14 @@ var StreamD_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "RemoveStreamDestination",
|
||||
Handler: _StreamD_RemoveStreamDestination_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AddIncomingStream",
|
||||
Handler: _StreamD_AddIncomingStream_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "RemoveIncomingStream",
|
||||
Handler: _StreamD_RemoveIncomingStream_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListIncomingStreams",
|
||||
Handler: _StreamD_ListIncomingStreams_Handler,
|
||||
@@ -1341,6 +1445,10 @@ var StreamD_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "AddStreamForward",
|
||||
Handler: _StreamD_AddStreamForward_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "UpdateStreamForward",
|
||||
Handler: _StreamD_UpdateStreamForward_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "RemoveStreamForward",
|
||||
Handler: _StreamD_RemoveStreamForward_Handler,
|
||||
|
||||
@@ -38,9 +38,12 @@ service StreamD {
|
||||
rpc ListStreamDestinations(ListStreamDestinationsRequest) returns (ListStreamDestinationsReply) {}
|
||||
rpc AddStreamDestination(AddStreamDestinationRequest) returns (AddStreamDestinationReply) {}
|
||||
rpc RemoveStreamDestination(RemoveStreamDestinationRequest) returns (RemoveStreamDestinationReply) {}
|
||||
rpc AddIncomingStream(AddIncomingStreamRequest) returns (AddIncomingStreamReply) {}
|
||||
rpc RemoveIncomingStream(RemoveIncomingStreamRequest) returns (RemoveIncomingStreamReply) {}
|
||||
rpc ListIncomingStreams(ListIncomingStreamsRequest) returns (ListIncomingStreamsReply) {}
|
||||
rpc ListStreamForwards(ListStreamForwardsRequest) returns (ListStreamForwardsReply) {}
|
||||
rpc AddStreamForward(AddStreamForwardRequest) returns (AddStreamForwardReply) {}
|
||||
rpc UpdateStreamForward(UpdateStreamForwardRequest) returns (UpdateStreamForwardReply) {}
|
||||
rpc RemoveStreamForward(RemoveStreamForwardRequest) returns (RemoveStreamForwardReply) {}
|
||||
}
|
||||
|
||||
@@ -197,9 +200,19 @@ message StreamServer {
|
||||
string listenAddr = 2;
|
||||
}
|
||||
|
||||
message StreamServerStatistics {
|
||||
int64 NumBytesConsumerWrote = 1;
|
||||
int64 NumBytesProducerRead = 2;
|
||||
}
|
||||
|
||||
message StreamServerWithStatistics {
|
||||
StreamServer config = 1;
|
||||
StreamServerStatistics statistics = 2;
|
||||
}
|
||||
|
||||
message ListStreamServersRequest {}
|
||||
message ListStreamServersReply {
|
||||
repeated StreamServer streamServers = 1;
|
||||
repeated StreamServerWithStatistics streamServers = 1;
|
||||
}
|
||||
|
||||
message StartStreamServerRequest {
|
||||
@@ -237,6 +250,16 @@ message IncomingStream {
|
||||
string streamID = 1;
|
||||
}
|
||||
|
||||
message AddIncomingStreamRequest {
|
||||
string streamID = 1;
|
||||
}
|
||||
message AddIncomingStreamReply {}
|
||||
|
||||
message RemoveIncomingStreamRequest {
|
||||
string streamID = 1;
|
||||
}
|
||||
message RemoveIncomingStreamReply {}
|
||||
|
||||
message ListIncomingStreamsRequest {}
|
||||
message ListIncomingStreamsReply {
|
||||
repeated IncomingStream incomingStreams = 1;
|
||||
@@ -245,11 +268,22 @@ message ListIncomingStreamsReply {
|
||||
message StreamForward {
|
||||
string streamID = 1;
|
||||
string destinationID = 2;
|
||||
bool enabled = 3;
|
||||
}
|
||||
|
||||
message StreamForwardStatistics {
|
||||
int64 numBytesWrote = 1;
|
||||
int64 numBytesRead = 2;
|
||||
}
|
||||
|
||||
message StreamForwardWithStatistics {
|
||||
StreamForward config = 1;
|
||||
StreamForwardStatistics statistics = 2;
|
||||
}
|
||||
|
||||
message ListStreamForwardsRequest {}
|
||||
message ListStreamForwardsReply {
|
||||
repeated StreamForward streamForwards = 1;
|
||||
repeated StreamForwardWithStatistics streamForwards = 1;
|
||||
}
|
||||
|
||||
message AddStreamForwardRequest {
|
||||
@@ -257,6 +291,11 @@ message AddStreamForwardRequest {
|
||||
}
|
||||
message AddStreamForwardReply {}
|
||||
|
||||
message UpdateStreamForwardRequest {
|
||||
StreamForward config = 1;
|
||||
}
|
||||
message UpdateStreamForwardReply {}
|
||||
|
||||
message RemoveStreamForwardRequest {
|
||||
StreamForward config = 1;
|
||||
}
|
||||
|
||||
@@ -706,16 +706,22 @@ func (grpc *GRPCServer) ListStreamServers(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []*streamd_grpc.StreamServer
|
||||
var result []*streamd_grpc.StreamServerWithStatistics
|
||||
for _, srv := range servers {
|
||||
t, err := grpcconv.StreamServerTypeGo2GRPC(srv.Type)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to convert the server type value: %w", err)
|
||||
}
|
||||
|
||||
result = append(result, &streamd_grpc.StreamServer{
|
||||
ServerType: t,
|
||||
ListenAddr: srv.ListenAddr,
|
||||
result = append(result, &streamd_grpc.StreamServerWithStatistics{
|
||||
Config: &streamd_grpc.StreamServer{
|
||||
ServerType: t,
|
||||
ListenAddr: srv.ListenAddr,
|
||||
},
|
||||
Statistics: &streamd_grpc.StreamServerStatistics{
|
||||
NumBytesConsumerWrote: int64(srv.NumBytesConsumerWrote),
|
||||
NumBytesProducerRead: int64(srv.NumBytesProducerRead),
|
||||
},
|
||||
})
|
||||
}
|
||||
return &streamd_grpc.ListStreamServersReply{
|
||||
@@ -809,6 +815,28 @@ func (grpc *GRPCServer) RemoveStreamDestination(
|
||||
return &streamd_grpc.RemoveStreamDestinationReply{}, nil
|
||||
}
|
||||
|
||||
func (grpc *GRPCServer) AddIncomingStream(
|
||||
ctx context.Context,
|
||||
req *streamd_grpc.AddIncomingStreamRequest,
|
||||
) (*streamd_grpc.AddIncomingStreamReply, error) {
|
||||
err := grpc.StreamD.AddIncomingStream(ctx, api.StreamID(req.GetStreamID()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &streamd_grpc.AddIncomingStreamReply{}, nil
|
||||
}
|
||||
|
||||
func (grpc *GRPCServer) RemoveIncomingStream(
|
||||
ctx context.Context,
|
||||
req *streamd_grpc.RemoveIncomingStreamRequest,
|
||||
) (*streamd_grpc.RemoveIncomingStreamReply, error) {
|
||||
err := grpc.StreamD.RemoveIncomingStream(ctx, api.StreamID(req.GetStreamID()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &streamd_grpc.RemoveIncomingStreamReply{}, nil
|
||||
}
|
||||
|
||||
func (grpc *GRPCServer) ListIncomingStreams(
|
||||
ctx context.Context,
|
||||
req *streamd_grpc.ListIncomingStreamsRequest,
|
||||
@@ -840,11 +868,18 @@ func (grpc *GRPCServer) ListStreamForwards(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []*streamd_grpc.StreamForward
|
||||
var result []*streamd_grpc.StreamForwardWithStatistics
|
||||
for _, s := range streamFwds {
|
||||
result = append(result, &streamd_grpc.StreamForward{
|
||||
StreamID: string(s.StreamID),
|
||||
DestinationID: string(s.DestinationID),
|
||||
result = append(result, &streamd_grpc.StreamForwardWithStatistics{
|
||||
Config: &streamd_grpc.StreamForward{
|
||||
StreamID: string(s.StreamID),
|
||||
DestinationID: string(s.DestinationID),
|
||||
Enabled: s.Enabled,
|
||||
},
|
||||
Statistics: &streamd_grpc.StreamForwardStatistics{
|
||||
NumBytesWrote: int64(s.NumBytesWrote),
|
||||
NumBytesRead: int64(s.NumBytesRead),
|
||||
},
|
||||
})
|
||||
}
|
||||
return &streamd_grpc.ListStreamForwardsReply{
|
||||
@@ -860,6 +895,7 @@ func (grpc *GRPCServer) AddStreamForward(
|
||||
ctx,
|
||||
api.StreamID(req.GetConfig().GetStreamID()),
|
||||
api.DestinationID(req.GetConfig().GetDestinationID()),
|
||||
req.Config.Enabled,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -867,6 +903,22 @@ func (grpc *GRPCServer) AddStreamForward(
|
||||
return &streamd_grpc.AddStreamForwardReply{}, nil
|
||||
}
|
||||
|
||||
func (grpc *GRPCServer) UpdateStreamForward(
|
||||
ctx context.Context,
|
||||
req *streamd_grpc.UpdateStreamForwardRequest,
|
||||
) (*streamd_grpc.UpdateStreamForwardReply, error) {
|
||||
err := grpc.StreamD.UpdateStreamForward(
|
||||
ctx,
|
||||
api.StreamID(req.GetConfig().GetStreamID()),
|
||||
api.DestinationID(req.GetConfig().GetDestinationID()),
|
||||
req.Config.Enabled,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &streamd_grpc.UpdateStreamForwardReply{}, nil
|
||||
}
|
||||
|
||||
func (grpc *GRPCServer) RemoveStreamForward(
|
||||
ctx context.Context,
|
||||
req *streamd_grpc.RemoveStreamForwardRequest,
|
||||
|
||||
@@ -2,6 +2,7 @@ package streamd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -34,7 +35,7 @@ func (d *StreamD) EXPERIMENTAL_ReinitStreamControllers(ctx context.Context) erro
|
||||
case strings.ToLower(string(youtube.ID)):
|
||||
err = d.initYouTubeBackend(ctx)
|
||||
}
|
||||
if err == ErrSkipBackend {
|
||||
if errors.Is(err, ErrSkipBackend) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
@@ -88,7 +88,10 @@ func New(
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *StreamD) Run(ctx context.Context) error {
|
||||
func (d *StreamD) Run(ctx context.Context) (_ret error) {
|
||||
logger.Debugf(ctx, "StreamD.Run()")
|
||||
defer func() { logger.Debugf(ctx, "/StreamD.Run(): %v", _ret) }()
|
||||
|
||||
d.UI.SetStatus("Initializing remote GIT storage...")
|
||||
err := d.FetchConfig(ctx)
|
||||
if err != nil {
|
||||
@@ -117,6 +120,7 @@ func (d *StreamD) Run(ctx context.Context) error {
|
||||
|
||||
func (d *StreamD) InitStreamServer(ctx context.Context) error {
|
||||
d.StreamServer = streamserver.New(&d.Config.StreamServer)
|
||||
assert(d.StreamServer != nil)
|
||||
return d.StreamServer.Init(ctx)
|
||||
}
|
||||
|
||||
@@ -715,6 +719,8 @@ func (d *StreamD) ListStreamServers(
|
||||
d.StreamServerLocker.Lock()
|
||||
defer d.StreamServerLocker.Unlock()
|
||||
|
||||
assert(d.StreamServer != nil)
|
||||
|
||||
servers := d.StreamServer.ListServers(ctx)
|
||||
|
||||
var result []api.StreamServer
|
||||
@@ -722,6 +728,9 @@ func (d *StreamD) ListStreamServers(
|
||||
result = append(result, api.StreamServer{
|
||||
Type: api.ServerTypeServer2API(src.Type()),
|
||||
ListenAddr: src.ListenAddr(),
|
||||
|
||||
NumBytesConsumerWrote: src.NumBytesConsumerWrote(),
|
||||
NumBytesProducerRead: src.NumBytesProducerRead(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -797,6 +806,52 @@ func (d *StreamD) StopStreamServer(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *StreamD) AddIncomingStream(
|
||||
ctx context.Context,
|
||||
streamID api.StreamID,
|
||||
) error {
|
||||
logger.Debugf(ctx, "AddIncomingStream")
|
||||
defer logger.Debugf(ctx, "/AddIncomingStream")
|
||||
|
||||
d.StreamServerLocker.Lock()
|
||||
defer d.StreamServerLocker.Unlock()
|
||||
|
||||
err := d.StreamServer.AddIncomingStream(ctx, types.StreamID(streamID))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to add an incoming stream: %w", err)
|
||||
}
|
||||
|
||||
err = d.SaveConfig(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save the config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *StreamD) RemoveIncomingStream(
|
||||
ctx context.Context,
|
||||
streamID api.StreamID,
|
||||
) error {
|
||||
logger.Debugf(ctx, "RemoveIncomingStream")
|
||||
defer logger.Debugf(ctx, "/RemoveIncomingStream")
|
||||
|
||||
d.StreamServerLocker.Lock()
|
||||
defer d.StreamServerLocker.Unlock()
|
||||
|
||||
err := d.StreamServer.RemoveIncomingStream(ctx, types.StreamID(streamID))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to remove an incoming stream: %w", err)
|
||||
}
|
||||
|
||||
err = d.SaveConfig(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save the config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *StreamD) ListIncomingStreams(
|
||||
ctx context.Context,
|
||||
) ([]api.IncomingStream, error) {
|
||||
@@ -899,8 +954,11 @@ func (d *StreamD) listStreamForwards(
|
||||
}
|
||||
for _, streamFwd := range streamForwards {
|
||||
result = append(result, api.StreamForward{
|
||||
Enabled: streamFwd.Enabled,
|
||||
StreamID: api.StreamID(streamFwd.StreamID),
|
||||
DestinationID: api.DestinationID(streamFwd.DestinationID),
|
||||
NumBytesWrote: streamFwd.NumBytesWrote,
|
||||
NumBytesRead: streamFwd.NumBytesRead,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
@@ -922,6 +980,7 @@ func (d *StreamD) AddStreamForward(
|
||||
ctx context.Context,
|
||||
streamID api.StreamID,
|
||||
destinationID api.DestinationID,
|
||||
enabled bool,
|
||||
) error {
|
||||
logger.Debugf(ctx, "AddStreamForward")
|
||||
defer logger.Debugf(ctx, "/AddStreamForward")
|
||||
@@ -933,6 +992,37 @@ func (d *StreamD) AddStreamForward(
|
||||
resetContextCancellers(ctx),
|
||||
types.StreamID(streamID),
|
||||
types.DestinationID(destinationID),
|
||||
enabled,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to add the stream forwarding: %w", err)
|
||||
}
|
||||
|
||||
err = d.SaveConfig(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save the config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *StreamD) UpdateStreamForward(
|
||||
ctx context.Context,
|
||||
streamID api.StreamID,
|
||||
destinationID api.DestinationID,
|
||||
enabled bool,
|
||||
) error {
|
||||
logger.Debugf(ctx, "AddStreamForward")
|
||||
defer logger.Debugf(ctx, "/AddStreamForward")
|
||||
|
||||
d.StreamServerLocker.Lock()
|
||||
defer d.StreamServerLocker.Unlock()
|
||||
|
||||
err := d.StreamServer.UpdateStreamForward(
|
||||
resetContextCancellers(ctx),
|
||||
types.StreamID(streamID),
|
||||
types.DestinationID(destinationID),
|
||||
enabled,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to add the stream forwarding: %w", err)
|
||||
|
||||
@@ -3,3 +3,9 @@ package streamd
|
||||
func ptr[T any](in T) *T {
|
||||
return &in
|
||||
}
|
||||
|
||||
func assert(b bool) {
|
||||
if !b {
|
||||
panic("assertion failed")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -37,7 +38,16 @@ func ReadConfigFromPath[CFG Config](
|
||||
return fmt.Errorf("unable to read file '%s': %w", cfgPath, err)
|
||||
}
|
||||
|
||||
logger.Default().Debugf("unparsed config == %s", b)
|
||||
_, err = cfg.Read(b)
|
||||
|
||||
var cfgSerialized bytes.Buffer
|
||||
if _, _err := cfg.WriteTo(&cfgSerialized); _err != nil {
|
||||
logger.Default().Error(_err)
|
||||
} else {
|
||||
logger.Default().Debugf("parsed config == %s", cfgSerialized.String())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,11 @@ var _ io.ReaderFrom = (*Config)(nil)
|
||||
func (cfg *Config) Read(
|
||||
b []byte,
|
||||
) (int, error) {
|
||||
return len(b), yaml.Unmarshal(b, cfg)
|
||||
n := len(b)
|
||||
if err := yaml.Unmarshal(b, cfg); err != nil {
|
||||
return n, fmt.Errorf("unable to unmarshal the config: %w", err)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (cfg *Config) ReadFrom(
|
||||
|
||||
@@ -2,8 +2,10 @@ package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
goyaml "github.com/go-yaml/yaml"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/xaionaro-go/datacounter"
|
||||
)
|
||||
@@ -19,9 +21,36 @@ func (cfg Config) Write(b []byte) (int, error) {
|
||||
func (cfg Config) WriteTo(
|
||||
w io.Writer,
|
||||
) (int64, error) {
|
||||
// There is bug in github.com/goccy/go-yaml that makes wrong intention
|
||||
// in cfg.BuiltinStreamD.GitRepo.PrivateKey makes the whole value unparsable
|
||||
//
|
||||
// Working this around...
|
||||
key := cfg.BuiltinStreamD.GitRepo.PrivateKey
|
||||
cfg.BuiltinStreamD.GitRepo.PrivateKey = ""
|
||||
|
||||
b, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, fmt.Errorf("unable to serialize data %#+v: %w", cfg, err)
|
||||
}
|
||||
|
||||
m := map[any]any{}
|
||||
err = goyaml.Unmarshal(b, &m)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("unable to unserialize data %s: %w", b, err)
|
||||
}
|
||||
if v, ok := m["streamd_builtin"]; ok {
|
||||
if m2, ok := v.(map[any]any); ok {
|
||||
if v, ok := m2["gitrepo"]; ok {
|
||||
if m3, ok := v.(map[any]any); ok {
|
||||
m3["private_key"] = key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b, err = goyaml.Marshal(m)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("unable to re-serialize data %#+v: %w", m, err)
|
||||
}
|
||||
|
||||
counter := datacounter.NewWriterCounter(w)
|
||||
|
||||
@@ -41,7 +41,7 @@ func (p *Panel) InputGitUserData(
|
||||
gitRepo.SetPlaceHolder("git@github.com:myname/myrepo.git")
|
||||
|
||||
gitPrivateKey := widget.NewMultiLineEntry()
|
||||
gitPrivateKey.SetText(cfg.GitRepo.PrivateKey)
|
||||
gitPrivateKey.SetText(string(cfg.GitRepo.PrivateKey))
|
||||
gitPrivateKey.SetMinRowsVisible(10)
|
||||
gitPrivateKey.TextStyle.Monospace = true
|
||||
gitPrivateKey.SetPlaceHolder(`-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
|
||||
@@ -119,6 +119,10 @@ type Panel struct {
|
||||
streamsWidget *fyne.Container
|
||||
destinationsWidget *fyne.Container
|
||||
restreamsWidget *fyne.Container
|
||||
|
||||
previousNumBytesLocker sync.Mutex
|
||||
previousNumBytes map[any][4]uint64
|
||||
previousNumBytesTS map[any]time.Time
|
||||
}
|
||||
|
||||
func New(
|
||||
@@ -136,13 +140,16 @@ func New(
|
||||
return nil, fmt.Errorf("unable to read the config from path '%s': %w", configPath, err)
|
||||
}
|
||||
|
||||
return &Panel{
|
||||
p := &Panel{
|
||||
configPath: configPath,
|
||||
Config: Options(opts).ApplyOverrides(cfg),
|
||||
Screenshoter: screenshoter.New(screenshot.Implementation{}),
|
||||
imageLastDownloaded: map[consts.ImageID][]byte{},
|
||||
streamStatus: map[streamcontrol.PlatformName]*widget.Label{},
|
||||
}, nil
|
||||
previousNumBytes: map[any][4]uint64{},
|
||||
previousNumBytesTS: map[any]time.Time{},
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *Panel) SetStatus(msg string) {
|
||||
@@ -178,15 +185,30 @@ func (opt LoopOptionStartingPage) apply(cfg *loopConfig) {
|
||||
cfg.StartingPage = consts.Page(opt)
|
||||
}
|
||||
|
||||
func (p *Panel) dumpConfig(ctx context.Context) {
|
||||
if logger.FromCtx(ctx).Level() < logger.LevelTrace {
|
||||
return
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err := p.Config.WriteTo(&buf)
|
||||
if err != nil {
|
||||
logger.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Tracef(ctx, "the current config is: %s", buf.String())
|
||||
}
|
||||
|
||||
func (p *Panel) Loop(ctx context.Context, opts ...LoopOption) error {
|
||||
if p.defaultContext != nil {
|
||||
return fmt.Errorf("Loop was already used, and cannot be used the second time")
|
||||
}
|
||||
p.dumpConfig(ctx)
|
||||
|
||||
initCfg := loopOptions(opts).Config()
|
||||
|
||||
p.defaultContext = ctx
|
||||
logger.Debug(ctx, "config", p.Config)
|
||||
|
||||
if p.Config.RemoteStreamDAddr != "" {
|
||||
if err := p.initRemoteStreamD(ctx); err != nil {
|
||||
@@ -230,6 +252,9 @@ func (p *Panel) Loop(ctx context.Context, opts ...LoopOption) error {
|
||||
p.DisplayError(fmt.Errorf("unable to initialize the streaming controllers: %w", err))
|
||||
}
|
||||
p.setStatusFunc = nil
|
||||
if streamD, ok := p.StreamD.(*streamd.StreamD); ok {
|
||||
assert(streamD.StreamServer != nil)
|
||||
}
|
||||
|
||||
p.reinitScreenshoter(ctx)
|
||||
|
||||
@@ -241,7 +266,7 @@ func (p *Panel) Loop(ctx context.Context, opts ...LoopOption) error {
|
||||
|
||||
if p.Config.RemoteStreamDAddr == "" {
|
||||
logger.Tracef(ctx, "hiding the loading window")
|
||||
loadingWindow.Hide()
|
||||
hideWindow(loadingWindow)
|
||||
}
|
||||
|
||||
logger.Tracef(ctx, "ended stream controllers initialization")
|
||||
@@ -1531,11 +1556,17 @@ func (p *Panel) initMainWindow(
|
||||
p.openAddStreamServerWindow(ctx)
|
||||
})
|
||||
p.streamsWidget = container.NewVBox()
|
||||
addStreamButton := widget.NewButtonWithIcon("Add stream", theme.ContentAddIcon(), p.openAddStreamWindow)
|
||||
addStreamButton := widget.NewButtonWithIcon("Add stream", theme.ContentAddIcon(), func() {
|
||||
p.openAddStreamWindow(ctx)
|
||||
})
|
||||
p.destinationsWidget = container.NewVBox()
|
||||
addDestination := widget.NewButtonWithIcon("Add destination", theme.ContentAddIcon(), p.openAddDestinationWindow)
|
||||
addDestination := widget.NewButtonWithIcon("Add destination", theme.ContentAddIcon(), func() {
|
||||
p.openAddDestinationWindow(ctx)
|
||||
})
|
||||
p.restreamsWidget = container.NewVBox()
|
||||
addRestream := widget.NewButtonWithIcon("Add restream", theme.ContentAddIcon(), p.openAddRestreamWindow)
|
||||
addRestream := widget.NewButtonWithIcon("Add restream", theme.ContentAddIcon(), func() {
|
||||
p.openAddRestreamWindow(ctx)
|
||||
})
|
||||
restreamPage := container.NewBorder(
|
||||
nil,
|
||||
nil,
|
||||
@@ -2517,7 +2548,9 @@ func (p *Panel) DisplayError(err error) {
|
||||
|
||||
func (p *Panel) waitForResponse(callback func()) {
|
||||
p.showWaitWindow()
|
||||
defer p.hideWaitWindow()
|
||||
defer func() {
|
||||
p.hideWaitWindow()
|
||||
}()
|
||||
callback()
|
||||
}
|
||||
|
||||
@@ -2540,6 +2573,8 @@ func (p *Panel) showWaitWindow() {
|
||||
func (p *Panel) hideWaitWindow() {
|
||||
p.waitWindowLocker.Lock()
|
||||
defer p.waitWindowLocker.Unlock()
|
||||
p.waitWindow.Hide()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
p.waitWindow.Close()
|
||||
p.waitWindow = nil
|
||||
}
|
||||
|
||||
@@ -3,14 +3,18 @@ package streampanel
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/api"
|
||||
"github.com/xaionaro-go/streamctl/pkg/xfyne"
|
||||
@@ -28,7 +32,7 @@ func (p *Panel) startRestreamPage(
|
||||
ctx, cancelFn := context.WithCancel(ctx)
|
||||
p.restreamPageUpdaterCancel = cancelFn
|
||||
|
||||
p.initRestartPage(ctx)
|
||||
p.initRestreamPage(ctx)
|
||||
|
||||
go func(ctx context.Context) {
|
||||
p.updateRestreamPage(ctx)
|
||||
@@ -46,11 +50,11 @@ func (p *Panel) startRestreamPage(
|
||||
}(ctx)
|
||||
}
|
||||
|
||||
func (p *Panel) initRestartPage(
|
||||
func (p *Panel) initRestreamPage(
|
||||
ctx context.Context,
|
||||
) {
|
||||
logger.Debugf(ctx, "initRestartPage")
|
||||
defer logger.Debugf(ctx, "/initRestartPage")
|
||||
logger.Debugf(ctx, "initRestreamPage")
|
||||
defer logger.Debugf(ctx, "/initRestreamPage")
|
||||
|
||||
streamServers, err := p.StreamD.ListStreamServers(ctx)
|
||||
if err != nil {
|
||||
@@ -116,12 +120,15 @@ func (p *Panel) openAddStreamServerWindow(ctx context.Context) {
|
||||
|
||||
saveButton := widget.NewButtonWithIcon("Save", theme.DocumentSaveIcon(), func() {
|
||||
listenHost := listenHostEntry.Text
|
||||
err := p.addStreamServer(ctx, currentProtocol, listenHost, listenPort)
|
||||
if err != nil {
|
||||
p.DisplayError(err)
|
||||
}
|
||||
|
||||
w.Close()
|
||||
p.waitForResponse(func() {
|
||||
err := p.addStreamServer(ctx, currentProtocol, listenHost, listenPort)
|
||||
if err != nil {
|
||||
p.DisplayError(err)
|
||||
return
|
||||
}
|
||||
w.Close()
|
||||
p.initRestreamPage(ctx)
|
||||
})
|
||||
})
|
||||
|
||||
w.SetContent(container.NewBorder(
|
||||
@@ -160,25 +167,123 @@ func (p *Panel) displayStreamServers(
|
||||
logger.Debugf(ctx, "displayStreamServers")
|
||||
defer logger.Debugf(ctx, "/displayStreamServers")
|
||||
|
||||
c := widget.NewList(
|
||||
func() int {
|
||||
return len(streamServers)
|
||||
},
|
||||
func() fyne.CanvasObject {
|
||||
return widget.NewLabel("")
|
||||
},
|
||||
func(idx widget.ListItemID, co fyne.CanvasObject) {
|
||||
o := co.(*widget.Label)
|
||||
srv := streamServers[idx]
|
||||
o.SetText(fmt.Sprintf("%s://%s", srv.Type, srv.ListenAddr))
|
||||
},
|
||||
)
|
||||
|
||||
p.streamServersWidget.RemoveAll()
|
||||
p.streamServersWidget.Add(c)
|
||||
for idx, srv := range streamServers {
|
||||
logger.Tracef(ctx, "streamServer[%3d] == %#+v", idx, srv)
|
||||
c := container.NewHBox()
|
||||
button := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
|
||||
w := dialog.NewConfirm(
|
||||
fmt.Sprintf("Delete Stream Server %s://%s ?", srv.Type, srv.ListenAddr),
|
||||
"",
|
||||
func(b bool) {
|
||||
if !b {
|
||||
return
|
||||
}
|
||||
logger.Debugf(ctx, "remove stream server")
|
||||
defer logger.Debugf(ctx, "/remove stream server")
|
||||
p.waitForResponse(func() {
|
||||
err := p.StreamD.StopStreamServer(ctx, srv.ListenAddr)
|
||||
if err != nil {
|
||||
p.DisplayError(err)
|
||||
return
|
||||
}
|
||||
})
|
||||
p.initRestreamPage(ctx)
|
||||
},
|
||||
p.mainWindow,
|
||||
)
|
||||
w.Show()
|
||||
})
|
||||
label := widget.NewLabel(fmt.Sprintf("%s://%s", srv.Type, srv.ListenAddr))
|
||||
c.RemoveAll()
|
||||
c.Add(button)
|
||||
c.Add(label)
|
||||
c.Add(widget.NewSeparator())
|
||||
|
||||
type numBytesID struct {
|
||||
ID string
|
||||
}
|
||||
key := numBytesID{ID: srv.ListenAddr}
|
||||
p.previousNumBytesLocker.Lock()
|
||||
prevNumBytes := p.previousNumBytes[key]
|
||||
now := time.Now()
|
||||
bwText := widget.NewRichTextWithText(bwString(srv.NumBytesProducerRead, prevNumBytes[0], srv.NumBytesConsumerWrote, prevNumBytes[1], now, p.previousNumBytesTS[key]))
|
||||
p.previousNumBytes[key] = [4]uint64{srv.NumBytesProducerRead, srv.NumBytesConsumerWrote}
|
||||
p.previousNumBytesTS[key] = now
|
||||
p.previousNumBytesLocker.Unlock()
|
||||
|
||||
c.Add(bwText)
|
||||
p.streamServersWidget.Add(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Panel) openAddStreamWindow() {}
|
||||
func bwString(
|
||||
nRead, nReadPrev uint64,
|
||||
nWrote, nWrotePrev uint64,
|
||||
ts, tsPrev time.Time,
|
||||
) string {
|
||||
var nReadStr, nWroteStr string
|
||||
|
||||
duration := ts.Sub(tsPrev)
|
||||
|
||||
if nRead != math.MaxUint64 {
|
||||
n := 8 * (nRead - nReadPrev)
|
||||
nReadStr = humanize.Bytes(uint64(float64(n) * float64(time.Second) / float64(duration)))
|
||||
nReadStr = nReadStr[:len(nReadStr)-1] + "bps"
|
||||
}
|
||||
|
||||
if nWrote != math.MaxUint64 {
|
||||
n := 8 * (nWrote - nWrotePrev)
|
||||
nWroteStr = humanize.Bytes(uint64(float64(n) * float64(time.Second) / float64(duration)))
|
||||
nWroteStr = nWroteStr[:len(nWroteStr)-1] + "bps"
|
||||
}
|
||||
|
||||
if nReadStr == "" && nWroteStr == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s | %s", nReadStr, nWroteStr)
|
||||
}
|
||||
|
||||
func (p *Panel) openAddStreamWindow(ctx context.Context) {
|
||||
w := p.app.NewWindow(appName + ": Add incoming stream")
|
||||
resizeWindow(w, fyne.NewSize(400, 300))
|
||||
|
||||
streamIDEntry := widget.NewEntry()
|
||||
streamIDEntry.SetPlaceHolder("stream name")
|
||||
|
||||
saveButton := widget.NewButtonWithIcon("Save", theme.DocumentSaveIcon(), func() {
|
||||
p.waitForResponse(func() {
|
||||
err := p.addIncomingStream(ctx, api.StreamID(streamIDEntry.Text))
|
||||
if err != nil {
|
||||
p.DisplayError(err)
|
||||
return
|
||||
}
|
||||
w.Close()
|
||||
p.initRestreamPage(ctx)
|
||||
})
|
||||
})
|
||||
|
||||
w.SetContent(container.NewBorder(
|
||||
nil,
|
||||
container.NewHBox(saveButton),
|
||||
nil,
|
||||
nil,
|
||||
container.NewVBox(
|
||||
streamIDEntry,
|
||||
),
|
||||
))
|
||||
w.Show()
|
||||
}
|
||||
|
||||
func (p *Panel) addIncomingStream(
|
||||
ctx context.Context,
|
||||
streamID api.StreamID,
|
||||
) error {
|
||||
logger.Debugf(ctx, "addIncomingStream")
|
||||
defer logger.Debugf(ctx, "/addIncomingStream")
|
||||
return p.StreamD.AddIncomingStream(ctx, streamID)
|
||||
}
|
||||
|
||||
func (p *Panel) displayIncomingServers(
|
||||
ctx context.Context,
|
||||
@@ -186,10 +291,91 @@ func (p *Panel) displayIncomingServers(
|
||||
) {
|
||||
logger.Debugf(ctx, "displayIncomingServers")
|
||||
defer logger.Debugf(ctx, "/displayIncomingServers")
|
||||
sort.Slice(inStreams, func(i, j int) bool {
|
||||
return inStreams[i].StreamID < inStreams[j].StreamID
|
||||
})
|
||||
|
||||
p.streamsWidget.RemoveAll()
|
||||
for idx, stream := range inStreams {
|
||||
logger.Tracef(ctx, "inStream[%3d] == %#+v", idx, stream)
|
||||
c := container.NewHBox()
|
||||
button := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
|
||||
w := dialog.NewConfirm(
|
||||
fmt.Sprintf("Delete incoming server %s ?", stream.StreamID),
|
||||
"",
|
||||
func(b bool) {
|
||||
if !b {
|
||||
return
|
||||
}
|
||||
logger.Debugf(ctx, "remove incoming stream")
|
||||
defer logger.Debugf(ctx, "/remove incoming stream")
|
||||
p.waitForResponse(func() {
|
||||
err := p.StreamD.RemoveIncomingStream(ctx, stream.StreamID)
|
||||
if err != nil {
|
||||
p.DisplayError(err)
|
||||
return
|
||||
}
|
||||
})
|
||||
p.initRestreamPage(ctx)
|
||||
},
|
||||
p.mainWindow,
|
||||
)
|
||||
w.Show()
|
||||
p.initRestreamPage(ctx)
|
||||
})
|
||||
label := widget.NewLabel(string(stream.StreamID))
|
||||
c.RemoveAll()
|
||||
c.Add(button)
|
||||
c.Add(label)
|
||||
p.streamsWidget.Add(c)
|
||||
}
|
||||
p.streamsWidget.Refresh()
|
||||
}
|
||||
|
||||
func (p *Panel) openAddDestinationWindow() {}
|
||||
func (p *Panel) openAddDestinationWindow(ctx context.Context) {
|
||||
w := p.app.NewWindow(appName + ": Add stream destination")
|
||||
resizeWindow(w, fyne.NewSize(400, 300))
|
||||
|
||||
destinationIDEntry := widget.NewEntry()
|
||||
destinationIDEntry.SetPlaceHolder("destination ID")
|
||||
|
||||
urlEntry := widget.NewEntry()
|
||||
urlEntry.SetPlaceHolder("URL")
|
||||
|
||||
saveButton := widget.NewButtonWithIcon("Save", theme.DocumentSaveIcon(), func() {
|
||||
p.waitForResponse(func() {
|
||||
err := p.addStreamDestination(ctx, api.DestinationID(destinationIDEntry.Text), urlEntry.Text)
|
||||
if err != nil {
|
||||
p.DisplayError(err)
|
||||
return
|
||||
}
|
||||
w.Close()
|
||||
p.initRestreamPage(ctx)
|
||||
})
|
||||
})
|
||||
|
||||
w.SetContent(container.NewBorder(
|
||||
nil,
|
||||
container.NewHBox(saveButton),
|
||||
nil,
|
||||
nil,
|
||||
container.NewVBox(
|
||||
destinationIDEntry,
|
||||
urlEntry,
|
||||
),
|
||||
))
|
||||
w.Show()
|
||||
}
|
||||
|
||||
func (p *Panel) addStreamDestination(
|
||||
ctx context.Context,
|
||||
destinationID api.DestinationID,
|
||||
url string,
|
||||
) error {
|
||||
logger.Debugf(ctx, "addStreamDestination")
|
||||
defer logger.Debugf(ctx, "/addStreamDestination")
|
||||
return p.StreamD.AddStreamDestination(ctx, destinationID, url)
|
||||
}
|
||||
|
||||
func (p *Panel) displayStreamDestinations(
|
||||
ctx context.Context,
|
||||
@@ -198,17 +384,220 @@ func (p *Panel) displayStreamDestinations(
|
||||
logger.Debugf(ctx, "displayStreamDestinations")
|
||||
defer logger.Debugf(ctx, "/displayStreamDestinations")
|
||||
|
||||
p.destinationsWidget.RemoveAll()
|
||||
for idx, dst := range dsts {
|
||||
logger.Tracef(ctx, "dsts[%3d] == %#+v", idx, dst)
|
||||
c := container.NewHBox()
|
||||
deleteButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
|
||||
w := dialog.NewConfirm(
|
||||
fmt.Sprintf("Delete destination %s ?", dst.ID),
|
||||
"",
|
||||
func(b bool) {
|
||||
if !b {
|
||||
return
|
||||
}
|
||||
logger.Debugf(ctx, "remove destination")
|
||||
defer logger.Debugf(ctx, "/remove destination")
|
||||
p.waitForResponse(func() {
|
||||
err := p.StreamD.RemoveStreamDestination(ctx, dst.ID)
|
||||
if err != nil {
|
||||
p.DisplayError(err)
|
||||
return
|
||||
}
|
||||
})
|
||||
p.initRestreamPage(ctx)
|
||||
},
|
||||
p.mainWindow,
|
||||
)
|
||||
w.Show()
|
||||
p.initRestreamPage(ctx)
|
||||
})
|
||||
label := widget.NewLabel(string(dst.ID) + ": " + string(dst.URL))
|
||||
c.RemoveAll()
|
||||
c.Add(deleteButton)
|
||||
c.Add(label)
|
||||
p.destinationsWidget.Add(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Panel) openAddRestreamWindow() {}
|
||||
func (p *Panel) openAddRestreamWindow(ctx context.Context) {
|
||||
w := p.app.NewWindow(appName + ": Add restreaming (stream forwarding)")
|
||||
resizeWindow(w, fyne.NewSize(400, 300))
|
||||
|
||||
enabledCheck := widget.NewCheck("Enable", func(b bool) {})
|
||||
|
||||
inStreams, err := p.StreamD.ListIncomingStreams(ctx)
|
||||
if err != nil {
|
||||
p.DisplayError(err)
|
||||
return
|
||||
}
|
||||
|
||||
dsts, err := p.StreamD.ListStreamDestinations(ctx)
|
||||
if err != nil {
|
||||
p.DisplayError(err)
|
||||
return
|
||||
}
|
||||
|
||||
var inStreamStrs []string
|
||||
for _, inStream := range inStreams {
|
||||
inStreamStrs = append(inStreamStrs, string(inStream.StreamID))
|
||||
}
|
||||
inStreamsSelect := widget.NewSelect(inStreamStrs, func(s string) {})
|
||||
|
||||
var dstStrs []string
|
||||
dstMap := map[string]api.DestinationID{}
|
||||
for _, dst := range dsts {
|
||||
k := string(dst.ID) + ": " + dst.URL
|
||||
dstStrs = append(dstStrs, k)
|
||||
dstMap[k] = dst.ID
|
||||
}
|
||||
dstSelect := widget.NewSelect(dstStrs, func(s string) {})
|
||||
|
||||
saveButton := widget.NewButtonWithIcon("Save", theme.DocumentSaveIcon(), func() {
|
||||
p.waitForResponse(func() {
|
||||
err := p.addStreamForward(
|
||||
ctx,
|
||||
api.StreamID(inStreamsSelect.Selected),
|
||||
dstMap[dstSelect.Selected],
|
||||
enabledCheck.Checked,
|
||||
)
|
||||
if err != nil {
|
||||
p.DisplayError(err)
|
||||
return
|
||||
}
|
||||
w.Close()
|
||||
p.initRestreamPage(ctx)
|
||||
})
|
||||
})
|
||||
|
||||
w.SetContent(container.NewBorder(
|
||||
nil,
|
||||
container.NewHBox(saveButton),
|
||||
nil,
|
||||
nil,
|
||||
container.NewVBox(
|
||||
widget.NewLabel("From:"),
|
||||
inStreamsSelect,
|
||||
widget.NewLabel("To:"),
|
||||
dstSelect,
|
||||
),
|
||||
))
|
||||
w.Show()
|
||||
}
|
||||
|
||||
func (p *Panel) addStreamForward(
|
||||
ctx context.Context,
|
||||
streamID api.StreamID,
|
||||
dstID api.DestinationID,
|
||||
enabled bool,
|
||||
) error {
|
||||
logger.Debugf(ctx, "addStreamForward")
|
||||
defer logger.Debugf(ctx, "/addStreamForward")
|
||||
return p.StreamD.AddStreamForward(
|
||||
ctx,
|
||||
streamID,
|
||||
dstID,
|
||||
enabled,
|
||||
)
|
||||
}
|
||||
|
||||
func (p *Panel) displayStreamForwards(
|
||||
ctx context.Context,
|
||||
dsts []api.StreamForward,
|
||||
fwds []api.StreamForward,
|
||||
) {
|
||||
logger.Debugf(ctx, "displayStreamForwards")
|
||||
defer logger.Debugf(ctx, "/displayStreamForwards")
|
||||
|
||||
p.restreamsWidget.RemoveAll()
|
||||
for idx, fwd := range fwds {
|
||||
logger.Tracef(ctx, "fwds[%3d] == %#+v", idx, fwd)
|
||||
c := container.NewHBox()
|
||||
deleteButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
|
||||
w := dialog.NewConfirm(
|
||||
fmt.Sprintf("Delete restreaming (stream forwarding) %s -> %s ?", fwd.StreamID, fwd.DestinationID),
|
||||
"",
|
||||
func(b bool) {
|
||||
if !b {
|
||||
return
|
||||
}
|
||||
logger.Debugf(ctx, "remove restreaming (stream forwarding)")
|
||||
defer logger.Debugf(ctx, "/remove restreaming (stream forwarding)")
|
||||
p.waitForResponse(func() {
|
||||
err := p.StreamD.RemoveStreamForward(ctx, fwd.StreamID, fwd.DestinationID)
|
||||
if err != nil {
|
||||
p.DisplayError(err)
|
||||
return
|
||||
}
|
||||
})
|
||||
p.initRestreamPage(ctx)
|
||||
},
|
||||
p.mainWindow,
|
||||
)
|
||||
w.Show()
|
||||
p.initRestreamPage(ctx)
|
||||
})
|
||||
icon := theme.MediaPauseIcon()
|
||||
label := "Pause"
|
||||
title := fmt.Sprintf("Pause forwarding %s -> %s ?", fwd.StreamID, fwd.DestinationID)
|
||||
if !fwd.Enabled {
|
||||
icon = theme.MediaPlayIcon()
|
||||
label = "Unpause"
|
||||
title = fmt.Sprintf("Unpause forwarding %s -> %s ?", fwd.StreamID, fwd.DestinationID)
|
||||
}
|
||||
playPauseButton := widget.NewButtonWithIcon(label, icon, func() {
|
||||
w := dialog.NewConfirm(
|
||||
title,
|
||||
"",
|
||||
func(b bool) {
|
||||
if !b {
|
||||
return
|
||||
}
|
||||
logger.Debugf(ctx, "pause/unpause restreaming (stream forwarding): disabled:%v->%v", fwd.Enabled, !fwd.Enabled)
|
||||
defer logger.Debugf(ctx, "/pause/unpause restreaming (stream forwarding): disabled:%v->%v", !fwd.Enabled, fwd.Enabled)
|
||||
p.waitForResponse(func() {
|
||||
err := p.StreamD.UpdateStreamForward(
|
||||
ctx,
|
||||
fwd.StreamID,
|
||||
fwd.DestinationID,
|
||||
!fwd.Enabled,
|
||||
)
|
||||
if err != nil {
|
||||
p.DisplayError(err)
|
||||
return
|
||||
}
|
||||
})
|
||||
p.initRestreamPage(ctx)
|
||||
},
|
||||
p.mainWindow,
|
||||
)
|
||||
w.Show()
|
||||
p.initRestreamPage(ctx)
|
||||
})
|
||||
caption := widget.NewLabel(string(fwd.StreamID) + " -> " + string(fwd.DestinationID))
|
||||
c.RemoveAll()
|
||||
c.Add(deleteButton)
|
||||
c.Add(playPauseButton)
|
||||
c.Add(caption)
|
||||
if fwd.Enabled {
|
||||
c.Add(widget.NewSeparator())
|
||||
|
||||
type numBytesID struct {
|
||||
StrID api.StreamID
|
||||
DstID api.DestinationID
|
||||
}
|
||||
key := numBytesID{StrID: fwd.StreamID, DstID: fwd.DestinationID}
|
||||
now := time.Now()
|
||||
p.previousNumBytesLocker.Lock()
|
||||
prevNumBytes := p.previousNumBytes[key]
|
||||
bwText := widget.NewRichTextWithText(bwString(fwd.NumBytesRead, prevNumBytes[0], fwd.NumBytesWrote, prevNumBytes[1], now, p.previousNumBytesTS[key]))
|
||||
p.previousNumBytes[key] = [4]uint64{fwd.NumBytesRead, fwd.NumBytesWrote}
|
||||
p.previousNumBytesTS[key] = now
|
||||
p.previousNumBytesLocker.Unlock()
|
||||
|
||||
c.Add(bwText)
|
||||
}
|
||||
p.restreamsWidget.Add(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Panel) stopRestreamPage(
|
||||
@@ -239,6 +628,7 @@ func (p *Panel) updateRestreamPage(
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
p.initRestreamPage(ctx)
|
||||
// whatever
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
@@ -14,16 +14,19 @@ import (
|
||||
"github.com/facebookincubator/go-belt/tool/experimental/errmon"
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/xaionaro-go/datacounter"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamserver/consts"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamserver/server"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamserver/streams"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamserver/types"
|
||||
)
|
||||
|
||||
type RTMPServer struct {
|
||||
Config Config
|
||||
StreamHandler *streams.StreamHandler
|
||||
Listener net.Listener
|
||||
CancelFn context.CancelFunc
|
||||
Config Config
|
||||
StreamHandler *streams.StreamHandler
|
||||
Listener net.Listener
|
||||
CancelFn context.CancelFunc
|
||||
TrafficCounter server.TrafficCounter
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
@@ -123,7 +126,12 @@ func (s *RTMPServer) tcpHandle(netConn net.Conn) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = cons.WriteTo(rtmpConn)
|
||||
wc := datacounter.NewWriterCounter(rtmpConn)
|
||||
s.TrafficCounter.Lock()
|
||||
s.TrafficCounter.WriterCounter = wc
|
||||
s.TrafficCounter.Unlock()
|
||||
|
||||
_, _ = cons.WriteTo(wc)
|
||||
|
||||
return nil
|
||||
|
||||
@@ -146,7 +154,15 @@ func (s *RTMPServer) tcpHandle(netConn net.Conn) error {
|
||||
|
||||
defer stream.RemoveProducer(prod)
|
||||
|
||||
_ = prod.Start()
|
||||
rc := server.NewIntPtrCounter(&prod.Recv)
|
||||
s.TrafficCounter.Lock()
|
||||
s.TrafficCounter.ReaderCounter = rc
|
||||
s.TrafficCounter.Unlock()
|
||||
|
||||
err = prod.Start()
|
||||
if err != nil {
|
||||
logger.Default().Error(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -154,18 +170,31 @@ func (s *RTMPServer) tcpHandle(netConn net.Conn) error {
|
||||
return errors.New("rtmp: unknown command: " + rtmpConn.Intent)
|
||||
}
|
||||
|
||||
func (s *RTMPServer) NumBytesConsumerWrote() uint64 {
|
||||
return s.TrafficCounter.NumBytesWrote()
|
||||
}
|
||||
func (s *RTMPServer) NumBytesProducerRead() uint64 {
|
||||
return s.TrafficCounter.NumBytesRead()
|
||||
}
|
||||
|
||||
func StreamsHandle(url string) (core.Producer, error) {
|
||||
return rtmp.DialPlay(url)
|
||||
}
|
||||
|
||||
func StreamsConsumerHandle(url string) (core.Consumer, func(context.Context) error, error) {
|
||||
func StreamsConsumerHandle(url string) (core.Consumer, server.NumBytesReaderWroter, func(context.Context) error, error) {
|
||||
cons := flv.NewConsumer()
|
||||
trafficCounter := &server.TrafficCounter{}
|
||||
run := func(ctx context.Context) error {
|
||||
wr, err := rtmp.DialPublish(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to connect to '%s': %w", url, err)
|
||||
}
|
||||
|
||||
wrc := datacounter.NewWriterCounter(wr)
|
||||
trafficCounter.Lock()
|
||||
trafficCounter.WriterCounter = wrc
|
||||
trafficCounter.Unlock()
|
||||
|
||||
ctx, cancelFn := context.WithCancel(ctx)
|
||||
defer cancelFn()
|
||||
go func() {
|
||||
@@ -175,14 +204,14 @@ func StreamsConsumerHandle(url string) (core.Consumer, func(context.Context) err
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
}()
|
||||
|
||||
_, err = cons.WriteTo(wr)
|
||||
_, err = cons.WriteTo(wrc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to write: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return cons, run, nil
|
||||
return cons, trafficCounter, run, nil
|
||||
}
|
||||
|
||||
func (s *RTMPServer) apiHandle(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -14,17 +14,19 @@ import (
|
||||
"github.com/facebookincubator/go-belt/tool/experimental/errmon"
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamserver/consts"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamserver/server"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamserver/streams"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamserver/types"
|
||||
)
|
||||
|
||||
type RTSPServer struct {
|
||||
Config Config
|
||||
Listener net.Listener
|
||||
DefaultMedias []*core.Media
|
||||
StreamHandler *streams.StreamHandler
|
||||
Handlers []HandlerFunc
|
||||
CancelFn context.CancelFunc
|
||||
Config Config
|
||||
Listener net.Listener
|
||||
DefaultMedias []*core.Media
|
||||
StreamHandler *streams.StreamHandler
|
||||
Handlers []HandlerFunc
|
||||
CancelFn context.CancelFunc
|
||||
TrafficCounter server.TrafficCounter
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
@@ -280,6 +282,13 @@ func (s *RTSPServer) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RTSPServer) NumBytesConsumerWrote() uint64 {
|
||||
return s.TrafficCounter.NumBytesWrote()
|
||||
}
|
||||
func (s *RTSPServer) NumBytesProducerRead() uint64 {
|
||||
return s.TrafficCounter.NumBytesRead()
|
||||
}
|
||||
|
||||
func ParseQuery(query map[string][]string) []*core.Media {
|
||||
if v := query["mp4"]; v != nil {
|
||||
return []*core.Media{
|
||||
|
||||
@@ -3,8 +3,10 @@ package streamserver
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
rtmpserver "github.com/xaionaro-go/streamctl/pkg/streamserver/server/rtmp"
|
||||
rtspserver "github.com/xaionaro-go/streamctl/pkg/streamserver/server/rtsp"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamserver/streams"
|
||||
@@ -20,9 +22,9 @@ type StreamServer struct {
|
||||
}
|
||||
|
||||
func New(cfg *types.Config) *StreamServer {
|
||||
if cfg == nil {
|
||||
cfg = &types.Config{}
|
||||
}
|
||||
assert(cfg != nil)
|
||||
logger.Default().Debugf("config == %#+v", *cfg)
|
||||
|
||||
if cfg.Streams == nil {
|
||||
cfg.Streams = map[types.StreamID]*types.StreamConfig{}
|
||||
}
|
||||
@@ -52,6 +54,7 @@ func (s *StreamServer) Init(ctx context.Context) error {
|
||||
defer s.Unlock()
|
||||
|
||||
cfg := s.Config
|
||||
logger.Debugf(ctx, "config == %#+v", *cfg)
|
||||
|
||||
for _, srv := range cfg.Servers {
|
||||
err := s.startServer(ctx, srv.Type, srv.Listen)
|
||||
@@ -73,10 +76,12 @@ func (s *StreamServer) Init(ctx context.Context) error {
|
||||
return fmt.Errorf("unable to initialize stream '%s': %w", streamID, err)
|
||||
}
|
||||
|
||||
for _, fwd := range streamCfg.Forwardings {
|
||||
err := s.addStreamForward(ctx, streamID, fwd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to launch stream forward from '%s' to '%s': %w", streamID, fwd, err)
|
||||
for dstID, fwd := range streamCfg.Forwardings {
|
||||
if !fwd.Disabled {
|
||||
err := s.addStreamForward(ctx, streamID, dstID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to launch stream forward from '%s' to '%s': %w", streamID, dstID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,7 +205,7 @@ func (s *StreamServer) addIncomingStream(
|
||||
if s.StreamHandler.Get(string(streamID)) != nil {
|
||||
return fmt.Errorf("stream '%s' already exists", streamID)
|
||||
}
|
||||
_, err := s.StreamHandler.New(string(streamID), "")
|
||||
_, err := s.StreamHandler.New(string(streamID), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create the stream '%s': %w", streamID, err)
|
||||
}
|
||||
@@ -209,6 +214,9 @@ func (s *StreamServer) addIncomingStream(
|
||||
|
||||
type IncomingStream struct {
|
||||
StreamID types.StreamID
|
||||
|
||||
NumBytesWrote uint64
|
||||
NumBytesRead uint64
|
||||
}
|
||||
|
||||
func (s *StreamServer) ListIncomingStreams(
|
||||
@@ -258,21 +266,33 @@ func (s *StreamServer) removeIncomingStream(
|
||||
type StreamForward struct {
|
||||
StreamID types.StreamID
|
||||
DestinationID types.DestinationID
|
||||
Enabled bool
|
||||
NumBytesWrote uint64
|
||||
NumBytesRead uint64
|
||||
}
|
||||
|
||||
func (s *StreamServer) AddStreamForward(
|
||||
ctx context.Context,
|
||||
streamID types.StreamID,
|
||||
destinationID types.DestinationID,
|
||||
enabled bool,
|
||||
) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
err := s.addStreamForward(ctx, streamID, destinationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
streamConfig := s.Config.Streams[streamID]
|
||||
streamConfig.Forwardings = append(streamConfig.Forwardings, destinationID)
|
||||
if _, ok := streamConfig.Forwardings[destinationID]; ok {
|
||||
return fmt.Errorf("the forwarding %s->%s already exists", streamID, destinationID)
|
||||
}
|
||||
|
||||
if enabled {
|
||||
err := s.addStreamForward(ctx, streamID, destinationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
streamConfig.Forwardings[destinationID] = types.ForwardingConfig{
|
||||
Disabled: !enabled,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -282,8 +302,8 @@ func (s *StreamServer) addStreamForward(
|
||||
destinationID types.DestinationID,
|
||||
) error {
|
||||
streamSrc := s.StreamHandler.Get(string(streamID))
|
||||
if streamSrc != nil {
|
||||
return fmt.Errorf("unable to find stream ID '%s'", streamID)
|
||||
if streamSrc == nil {
|
||||
return fmt.Errorf("unable to find stream ID '%s', available stream IDs: %s", streamID, strings.Join(s.StreamHandler.GetAll(), ", "))
|
||||
}
|
||||
dst, err := s.findStreamDestinationByID(ctx, destinationID)
|
||||
if err != nil {
|
||||
@@ -296,12 +316,82 @@ func (s *StreamServer) addStreamForward(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StreamServer) UpdateStreamForward(
|
||||
ctx context.Context,
|
||||
streamID types.StreamID,
|
||||
destinationID types.DestinationID,
|
||||
enabled bool,
|
||||
) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
streamConfig := s.Config.Streams[streamID]
|
||||
fwdCfg, ok := streamConfig.Forwardings[destinationID]
|
||||
if !ok {
|
||||
return fmt.Errorf("the forwarding %s->%s does not exist", streamID, destinationID)
|
||||
}
|
||||
|
||||
if fwdCfg.Disabled && enabled {
|
||||
err := s.addStreamForward(ctx, streamID, destinationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !fwdCfg.Disabled && !enabled {
|
||||
err := s.removeStreamForward(ctx, streamID, destinationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
streamConfig.Forwardings[destinationID] = types.ForwardingConfig{
|
||||
Disabled: !enabled,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StreamServer) ListStreamForwards(
|
||||
ctx context.Context,
|
||||
) ([]StreamForward, error) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
return s.listStreamForwards(ctx)
|
||||
|
||||
activeStreamForwards, err := s.listStreamForwards(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get the list of active stream forwardings: %w", err)
|
||||
}
|
||||
|
||||
type fwdID struct {
|
||||
StreamID types.StreamID
|
||||
DestID types.DestinationID
|
||||
}
|
||||
m := map[fwdID]*StreamForward{}
|
||||
for idx := range activeStreamForwards {
|
||||
fwd := &activeStreamForwards[idx]
|
||||
m[fwdID{
|
||||
StreamID: fwd.StreamID,
|
||||
DestID: fwd.DestinationID,
|
||||
}] = fwd
|
||||
}
|
||||
|
||||
var result []StreamForward
|
||||
for streamID, stream := range s.Config.Streams {
|
||||
for dstID, cfg := range stream.Forwardings {
|
||||
item := StreamForward{
|
||||
StreamID: streamID,
|
||||
DestinationID: dstID,
|
||||
Enabled: !cfg.Disabled,
|
||||
}
|
||||
if activeFwd, ok := m[fwdID{
|
||||
StreamID: streamID,
|
||||
DestID: dstID,
|
||||
}]; ok {
|
||||
item.NumBytesWrote = activeFwd.NumBytesWrote
|
||||
item.NumBytesRead = activeFwd.NumBytesRead
|
||||
}
|
||||
logger.Tracef(ctx, "stream forwarding '%s->%s': %#+v", streamID, dstID, cfg)
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *StreamServer) listStreamForwards(
|
||||
@@ -322,6 +412,9 @@ func (s *StreamServer) listStreamForwards(
|
||||
result = append(result, StreamForward{
|
||||
StreamID: streamIDSrc,
|
||||
DestinationID: streamDst.ID,
|
||||
Enabled: true,
|
||||
NumBytesWrote: fwd.TrafficCounter.NumBytesWrote(),
|
||||
NumBytesRead: fwd.TrafficCounter.NumBytesRead(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -336,13 +429,10 @@ func (s *StreamServer) RemoveStreamForward(
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
streamCfg := s.Config.Streams[streamID]
|
||||
for idx, _dstID := range streamCfg.Forwardings {
|
||||
if _dstID != dstID {
|
||||
continue
|
||||
}
|
||||
streamCfg.Forwardings = append(streamCfg.Forwardings[:idx], streamCfg.Forwardings[idx+1:]...)
|
||||
break
|
||||
if _, ok := streamCfg.Forwardings[dstID]; !ok {
|
||||
return fmt.Errorf("the forwarding %s->%s does not exist", streamID, dstID)
|
||||
}
|
||||
delete(streamCfg.Forwardings, dstID)
|
||||
return s.removeStreamForward(ctx, streamID, dstID)
|
||||
}
|
||||
|
||||
@@ -424,13 +514,7 @@ func (s *StreamServer) RemoveStreamDestination(
|
||||
s.Mutex.Lock()
|
||||
defer s.Mutex.Unlock()
|
||||
for _, streamCfg := range s.Config.Streams {
|
||||
for fIdx, destID := range streamCfg.Forwardings {
|
||||
if destID != destinationID {
|
||||
continue
|
||||
}
|
||||
streamCfg.Forwardings = append(streamCfg.Forwardings[:fIdx], streamCfg.Forwardings[fIdx+1:]...)
|
||||
break
|
||||
}
|
||||
delete(streamCfg.Forwardings, destinationID)
|
||||
}
|
||||
delete(s.Config.Destinations, destinationID)
|
||||
return s.removeStreamDestination(ctx, destinationID)
|
||||
|
||||
@@ -2,6 +2,7 @@ package streams
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
@@ -12,6 +13,10 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
||||
// support for multiple simultaneous pending from different consumers
|
||||
consN := s.pending.Add(1) - 1
|
||||
|
||||
if len(s.producers) == 0 {
|
||||
return ErrNoProducer{}
|
||||
}
|
||||
|
||||
var prodErrors = make([]error, len(s.producers))
|
||||
var prodMedias []*core.Media
|
||||
var prodStarts []*Producer
|
||||
@@ -29,8 +34,8 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
||||
}
|
||||
|
||||
if err = prod.Dial(); err != nil {
|
||||
logger.Default().WithField("error", err).Tracef("[streams] dial cons=%d prod=%d", consN, prodN)
|
||||
prodErrors[prodN] = err
|
||||
logger.Default().Tracef("[streams] dial cons=%d prod=%d err=%v", consN, prodN, err)
|
||||
prodErrors[prodN] = fmt.Errorf("unable to Dial(): %w", err)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -53,13 +58,13 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
||||
|
||||
// Step 4. Get recvonly track from producer
|
||||
if track, err = prod.GetTrack(prodMedia, prodCodec); err != nil {
|
||||
logger.Default().WithField("error", err).Info("[streams] can't get track")
|
||||
prodErrors[prodN] = err
|
||||
logger.Default().Info("[streams] can't get track; err=%v", err)
|
||||
prodErrors[prodN] = fmt.Errorf("unable to GetTrack(): %w", err)
|
||||
continue
|
||||
}
|
||||
// Step 5. Add track to consumer
|
||||
if err = cons.AddTrack(consMedia, consCodec, track); err != nil {
|
||||
logger.Default().WithField("error", err).Info("[streams] can't add track")
|
||||
logger.Default().Info("[streams] can't add track; err=%v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -68,13 +73,13 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
||||
|
||||
// Step 4. Get recvonly track from consumer (backchannel)
|
||||
if track, err = cons.(core.Producer).GetTrack(consMedia, consCodec); err != nil {
|
||||
logger.Default().WithField("error", err).Info("[streams] can't get track")
|
||||
logger.Default().Info("[streams] can't get track; err=%v", err)
|
||||
continue
|
||||
}
|
||||
// Step 5. Add track to producer
|
||||
if err = prod.AddTrack(prodMedia, prodCodec, track); err != nil {
|
||||
logger.Default().WithField("error", err).Info("[streams] can't add track")
|
||||
prodErrors[prodN] = err
|
||||
logger.Default().Info("[streams] can't add track; err=%v", err)
|
||||
prodErrors[prodN] = fmt.Errorf("unable to AddTrack(): %w", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ package streams
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamserver/server"
|
||||
)
|
||||
|
||||
type Handler func(source string) (core.Producer, error)
|
||||
@@ -30,26 +32,34 @@ func (s *StreamHandler) HasProducer(url string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type ErrNoProducer struct{}
|
||||
|
||||
func (err ErrNoProducer) Error() string {
|
||||
return "no producers"
|
||||
}
|
||||
|
||||
func (s *StreamHandler) GetProducer(url string) (core.Producer, error) {
|
||||
if i := strings.IndexByte(url, ':'); i > 0 {
|
||||
scheme := url[:i]
|
||||
i := strings.IndexByte(url, ':')
|
||||
if i <= 0 {
|
||||
return nil, fmt.Errorf("streams: empty scheme in URL: '%s'", url)
|
||||
}
|
||||
scheme := url[:i]
|
||||
|
||||
if redirect, ok := s.redirects[scheme]; ok {
|
||||
location, err := redirect(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if location != "" {
|
||||
return s.GetProducer(location)
|
||||
}
|
||||
if redirect, ok := s.redirects[scheme]; ok {
|
||||
location, err := redirect(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if handler, ok := s.handlers[scheme]; ok {
|
||||
return handler(url)
|
||||
if location != "" {
|
||||
return s.GetProducer(location)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("streams: unsupported scheme: " + url)
|
||||
if handler, ok := s.handlers[scheme]; ok {
|
||||
return handler(url)
|
||||
}
|
||||
|
||||
return nil, errors.New("streams: unsupported scheme in URL: " + url)
|
||||
}
|
||||
|
||||
// Redirect can return: location URL or error or empty URL and error
|
||||
@@ -73,13 +83,13 @@ func (s *StreamHandler) Location(url string) (string, error) {
|
||||
|
||||
// TODO: rework
|
||||
|
||||
type ConsumerHandler func(url string) (core.Consumer, func(context.Context) error, error)
|
||||
type ConsumerHandler func(url string) (core.Consumer, server.NumBytesReaderWroter, func(context.Context) error, error)
|
||||
|
||||
func (s *StreamHandler) HandleConsumerFunc(scheme string, handler ConsumerHandler) {
|
||||
s.consumerHandlers[scheme] = handler
|
||||
}
|
||||
|
||||
func (s *StreamHandler) GetConsumer(url string) (core.Consumer, func(context.Context) error, error) {
|
||||
func (s *StreamHandler) GetConsumer(url string) (core.Consumer, server.NumBytesReaderWroter, func(context.Context) error, error) {
|
||||
if i := strings.IndexByte(url, ':'); i > 0 {
|
||||
scheme := url[:i]
|
||||
|
||||
@@ -88,5 +98,5 @@ func (s *StreamHandler) GetConsumer(url string) (core.Consumer, func(context.Con
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil, errors.New("streams: unsupported scheme: " + url)
|
||||
return nil, nil, nil, errors.New("streams: unsupported scheme: " + url)
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ func (s *Stream) Play(source string) error {
|
||||
|
||||
for _, producer := range s.producers {
|
||||
// start new client
|
||||
dst, err := s.streamHandler.GetProducer(producer.url)
|
||||
dst, err := s.streamHandler.GetProducer(producer.urlFunc())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ const (
|
||||
type Producer struct {
|
||||
core.Listener
|
||||
|
||||
url string
|
||||
urlFunc func() string
|
||||
template string
|
||||
|
||||
conn core.Producer
|
||||
@@ -41,20 +41,19 @@ type Producer struct {
|
||||
|
||||
const SourceTemplate = "{input}"
|
||||
|
||||
func (s *StreamHandler) NewProducer(source string) *Producer {
|
||||
if strings.Contains(source, SourceTemplate) {
|
||||
return &Producer{streamHandler: s, template: source}
|
||||
func (s *StreamHandler) NewProducer(source func() string) *Producer {
|
||||
if strings.Contains(source(), SourceTemplate) {
|
||||
return &Producer{streamHandler: s, template: source()}
|
||||
}
|
||||
|
||||
return &Producer{streamHandler: s, url: source}
|
||||
return &Producer{streamHandler: s, urlFunc: source}
|
||||
}
|
||||
|
||||
func (p *Producer) SetSource(s string) {
|
||||
if p.template == "" {
|
||||
p.url = s
|
||||
} else {
|
||||
p.url = strings.Replace(p.template, SourceTemplate, s, 1)
|
||||
if p.template != "" {
|
||||
s = strings.Replace(p.template, SourceTemplate, s, 1)
|
||||
}
|
||||
p.urlFunc = func() string { return s }
|
||||
}
|
||||
|
||||
func (p *Producer) Dial() error {
|
||||
@@ -62,7 +61,7 @@ func (p *Producer) Dial() error {
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.state == stateNone {
|
||||
conn, err := p.streamHandler.GetProducer(p.url)
|
||||
conn, err := p.streamHandler.GetProducer(p.urlFunc())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -138,7 +137,7 @@ func (p *Producer) MarshalJSON() ([]byte, error) {
|
||||
if conn := p.conn; conn != nil {
|
||||
return json.Marshal(conn)
|
||||
}
|
||||
info := map[string]string{"url": p.url}
|
||||
info := map[string]string{"url": p.urlFunc()}
|
||||
return json.Marshal(info)
|
||||
}
|
||||
|
||||
@@ -150,7 +149,7 @@ func (p *Producer) start() {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Default().Debugf("[streams] start producer url=%s", p.url)
|
||||
logger.Default().Debugf("[streams] start producer url=%s", p.urlFunc)
|
||||
|
||||
p.state = stateStart
|
||||
p.workerID++
|
||||
@@ -168,7 +167,7 @@ func (p *Producer) worker(conn core.Producer, workerID int) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Default().Warn(struct{ URL string }{URL: p.url}, err)
|
||||
logger.Default().Warn(struct{ URL string }{URL: p.urlFunc()}, err)
|
||||
}
|
||||
|
||||
p.reconnect(workerID, 0)
|
||||
@@ -179,13 +178,13 @@ func (p *Producer) reconnect(workerID, retry int) {
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.workerID != workerID {
|
||||
logger.Default().Tracef("[streams] stop reconnect url=%s", p.url)
|
||||
logger.Default().Tracef("[streams] stop reconnect url=%s", p.urlFunc)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Default().Debugf("[streams] retry=%d to url=%s", retry, p.url)
|
||||
logger.Default().Debugf("[streams] retry=%d to url=%s", retry, p.urlFunc)
|
||||
|
||||
conn, err := p.streamHandler.GetProducer(p.url)
|
||||
conn, err := p.streamHandler.GetProducer(p.urlFunc())
|
||||
if err != nil {
|
||||
logger.Default().Debugf("[streams] producer=%s", err)
|
||||
|
||||
@@ -258,7 +257,7 @@ func (p *Producer) stop() {
|
||||
p.workerID++
|
||||
}
|
||||
|
||||
logger.Default().Tracef("[streams] stop producer url=%s", p.url)
|
||||
logger.Default().Tracef("[streams] stop producer url=%s", p.urlFunc)
|
||||
|
||||
if p.conn != nil {
|
||||
_ = p.conn.Stop()
|
||||
|
||||
@@ -22,6 +22,11 @@ type Stream struct {
|
||||
func (s *StreamHandler) NewStream(source any) *Stream {
|
||||
switch source := source.(type) {
|
||||
case string:
|
||||
return &Stream{
|
||||
producers: []*Producer{s.NewProducer(func() string { return source })},
|
||||
streamHandler: s,
|
||||
}
|
||||
case func() string:
|
||||
return &Stream{
|
||||
producers: []*Producer{s.NewProducer(source)},
|
||||
streamHandler: s,
|
||||
@@ -35,15 +40,15 @@ func (s *StreamHandler) NewStream(source any) *Stream {
|
||||
logger.Default().Errorf("[stream] NewStream: Expected string, got %v", src)
|
||||
continue
|
||||
}
|
||||
stream.producers = append(stream.producers, s.NewProducer(str))
|
||||
stream.producers = append(stream.producers, s.NewProducer(func() string { return str }))
|
||||
}
|
||||
return stream
|
||||
case map[string]any:
|
||||
return s.NewStream(source["url"])
|
||||
case nil:
|
||||
stream := new(Stream)
|
||||
stream.streamHandler = s
|
||||
return stream
|
||||
return &Stream{
|
||||
streamHandler: s,
|
||||
}
|
||||
default:
|
||||
panic(core.Caller())
|
||||
}
|
||||
@@ -51,7 +56,7 @@ func (s *StreamHandler) NewStream(source any) *Stream {
|
||||
|
||||
func (s *Stream) Sources() (sources []string) {
|
||||
for _, prod := range s.producers {
|
||||
sources = append(sources, prod.url)
|
||||
sources = append(sources, prod.urlFunc())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,22 +2,26 @@ package streams
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/facebookincubator/go-belt/tool/experimental/errmon"
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamserver/server"
|
||||
)
|
||||
|
||||
type StreamForwarding struct {
|
||||
sync.Mutex
|
||||
Stream *Stream
|
||||
Consumer core.Consumer
|
||||
StreamHandler *StreamHandler
|
||||
CancelFunc context.CancelFunc
|
||||
URL string
|
||||
Stream *Stream
|
||||
Consumer core.Consumer
|
||||
StreamHandler *StreamHandler
|
||||
CancelFunc context.CancelFunc
|
||||
URL string
|
||||
TrafficCounter server.NumBytesReaderWroter
|
||||
}
|
||||
|
||||
func NewStreamForwarding(streamHandler *StreamHandler) *StreamForwarding {
|
||||
@@ -32,21 +36,39 @@ func (sf *StreamForwarding) Start(
|
||||
sf.Lock()
|
||||
defer sf.Unlock()
|
||||
|
||||
cons, run, err := sf.StreamHandler.GetConsumer(url)
|
||||
cons, trafficCounter, run, err := sf.StreamHandler.GetConsumer(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to initialize consumer of '%s': %w", url, err)
|
||||
}
|
||||
sf.Stream = s
|
||||
sf.URL = url
|
||||
sf.Consumer = cons
|
||||
sf.TrafficCounter = trafficCounter
|
||||
|
||||
if err = s.AddConsumer(cons); err != nil {
|
||||
return fmt.Errorf("unable to add consumer: %w", err)
|
||||
}
|
||||
ctx, cancelFn := context.WithCancel(ctx)
|
||||
sf.CancelFunc = cancelFn
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
err = s.AddConsumer(cons)
|
||||
if errors.Is(err, ErrNoProducer{}) {
|
||||
logger.Debugf(ctx, "waiting for a producer")
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "unable to add consumer of '%s': %v", sf.URL, err)
|
||||
time.Sleep(time.Second * 5)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
err := run(ctx)
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
s.RemoveConsumer(cons)
|
||||
|
||||
@@ -22,11 +22,7 @@ func (s *StreamHandler) Validate(source string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StreamHandler) New(name string, source string) (*Stream, error) {
|
||||
if err := s.Validate(source); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (s *StreamHandler) New(name string, source any) (*Stream, error) {
|
||||
stream := s.NewStream(source)
|
||||
s.streams[name] = stream
|
||||
return stream, nil
|
||||
|
||||
@@ -5,8 +5,12 @@ type Server struct {
|
||||
Listen string `yaml:"listen"`
|
||||
}
|
||||
|
||||
type ForwardingConfig struct {
|
||||
Disabled bool `yaml:"disabled,omitempty"`
|
||||
}
|
||||
|
||||
type StreamConfig struct {
|
||||
Forwardings []DestinationID `yaml:"forwardings"`
|
||||
Forwardings map[DestinationID]ForwardingConfig `yaml:"forwardings"`
|
||||
}
|
||||
|
||||
type DestinationConfig struct {
|
||||
|
||||
@@ -83,6 +83,9 @@ type ServerHandler interface {
|
||||
|
||||
Type() ServerType
|
||||
ListenAddr() string
|
||||
|
||||
NumBytesConsumerWrote() uint64
|
||||
NumBytesProducerRead() uint64
|
||||
}
|
||||
|
||||
type StreamDestination struct {
|
||||
|
||||
Reference in New Issue
Block a user