mirror of
https://github.com/xaionaro-go/streamctl.git
synced 2025-10-28 01:41:34 +08:00
Initial commit, pt. 42
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@ qamel-*
|
||||
rcc*.go
|
||||
rcc*.cpp
|
||||
*.qrc
|
||||
.vscode
|
||||
|
||||
8
Makefile
8
Makefile
@@ -22,11 +22,17 @@ streampanel-ios: builddir
|
||||
streampanel-windows: builddir
|
||||
CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc GOOS=windows go build -ldflags "-H windowsgui" -o build/streampanel.exe ./cmd/streampanel/
|
||||
|
||||
streampanel-windows-debug: builddir
|
||||
CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc GOOS=windows go build -o build/streampanel-debug.exe ./cmd/streampanel/
|
||||
|
||||
streamd-linux-amd64: builddir
|
||||
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o build/streamd-linux-amd64 ./cmd/streamd
|
||||
|
||||
streamcli-linux-amd64: builddir
|
||||
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o build/streamcli-linux-amd64 ./cmd/streamcli
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o build/streamcli-linux-amd64 ./cmd/streamcli
|
||||
|
||||
streamcli-linux-arm64: builddir
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o build/streamcli-linux-arm64 ./cmd/streamcli
|
||||
|
||||
builddir:
|
||||
mkdir -p build
|
||||
|
||||
@@ -3,6 +3,7 @@ package commands
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -62,6 +63,12 @@ var (
|
||||
Run: variablesGet,
|
||||
}
|
||||
|
||||
VariablesGetHash = &cobra.Command{
|
||||
Use: "get_hash",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: variablesGetHash,
|
||||
}
|
||||
|
||||
VariablesSet = &cobra.Command{
|
||||
Use: "set",
|
||||
Args: cobra.ExactArgs(1),
|
||||
@@ -78,6 +85,7 @@ func init() {
|
||||
|
||||
Root.AddCommand(Variables)
|
||||
Variables.AddCommand(VariablesGet)
|
||||
Variables.AddCommand(VariablesGetHash)
|
||||
Variables.AddCommand(VariablesSet)
|
||||
|
||||
Root.PersistentFlags().Var(&LoggerLevel, "log-level", "")
|
||||
@@ -176,6 +184,20 @@ func variablesGet(cmd *cobra.Command, args []string) {
|
||||
assertNoError(ctx, err)
|
||||
}
|
||||
|
||||
func variablesGetHash(cmd *cobra.Command, args []string) {
|
||||
variableKey := args[0]
|
||||
ctx := cmd.Context()
|
||||
|
||||
remoteAddr, err := cmd.Flags().GetString("remote-addr")
|
||||
assertNoError(ctx, err)
|
||||
streamD := client.New(remoteAddr)
|
||||
|
||||
b, err := streamD.GetVariableHash(ctx, consts.VarKey(variableKey), crypto.SHA1)
|
||||
assertNoError(ctx, err)
|
||||
|
||||
fmt.Printf("%X\n", b)
|
||||
}
|
||||
|
||||
func variablesSet(cmd *cobra.Command, args []string) {
|
||||
variableKey := args[0]
|
||||
ctx := cmd.Context()
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/xaionaro-go/streamctl/cmd/streamd/ui"
|
||||
"github.com/xaionaro-go/streamctl/pkg/observability"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/config"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/grpc/go/streamd_grpc"
|
||||
@@ -154,9 +155,11 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err = streamD.Run(ctx); err != nil {
|
||||
l.Errorf("streamd returned an error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
listener, err := net.Listen("tcp", *listenAddr)
|
||||
if err != nil {
|
||||
@@ -172,7 +175,10 @@ func main() {
|
||||
streamdGRPC = server.NewGRPCServer(streamD)
|
||||
streamd_grpc.RegisterStreamDServer(grpcServer, streamdGRPC)
|
||||
l.Infof("started server at %s", *listenAddr)
|
||||
|
||||
streamdGRPCLocker.Unlock()
|
||||
err = grpcServer.Serve(listener)
|
||||
streamdGRPCLocker.Lock()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -180,10 +186,18 @@ func main() {
|
||||
|
||||
_ui = ui.NewUI(
|
||||
ctx,
|
||||
func(authURL string) {
|
||||
func(listenPort uint16, platID streamcontrol.PlatformName, authURL string) bool {
|
||||
logger.Tracef(ctx, "streamd.UI.OpenOAuthURL(%d, %s, '%s')", listenPort, platID, authURL)
|
||||
defer logger.Tracef(ctx, "/streamd.UI.OpenOAuthURL(%d, %s, '%s')", listenPort, platID, authURL)
|
||||
|
||||
streamdGRPCLocker.Lock()
|
||||
logger.Tracef(ctx, "streamdGRPCLocker.Lock()-ed")
|
||||
defer logger.Tracef(ctx, "streamdGRPCLocker.Lock()-ed")
|
||||
defer streamdGRPCLocker.Unlock()
|
||||
streamdGRPC.OpenOAuthURL(authURL)
|
||||
|
||||
err := streamdGRPC.OpenOAuthURL(ctx, listenPort, platID, authURL)
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
return err == nil
|
||||
},
|
||||
func(ctx context.Context, s string) {
|
||||
restart()
|
||||
|
||||
@@ -2,6 +2,9 @@ package ui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/facebookincubator/go-belt"
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
@@ -15,22 +18,25 @@ import (
|
||||
)
|
||||
|
||||
type UI struct {
|
||||
OAuthURLOpenFn func(authURL string)
|
||||
OAuthURLOpenFn func(listenPort uint16, platID streamcontrol.PlatformName, authURL string) bool
|
||||
Belt *belt.Belt
|
||||
RestartFn func(context.Context, string)
|
||||
CodeChMap map[streamcontrol.PlatformName]chan string
|
||||
CodeChMapLocker sync.Mutex
|
||||
}
|
||||
|
||||
var _ ui.UI = (*UI)(nil)
|
||||
|
||||
func NewUI(
|
||||
ctx context.Context,
|
||||
oauthURLOpener func(authURL string),
|
||||
oauthURLOpener func(listenPort uint16, platID streamcontrol.PlatformName, authURL string) bool,
|
||||
restartFn func(context.Context, string),
|
||||
) *UI {
|
||||
return &UI{
|
||||
OAuthURLOpenFn: oauthURLOpener,
|
||||
Belt: belt.CtxBelt(ctx),
|
||||
RestartFn: restartFn,
|
||||
CodeChMap: map[streamcontrol.PlatformName]chan string{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,33 +58,99 @@ func (*UI) InputGitUserData(
|
||||
return false, "", nil, nil
|
||||
}
|
||||
|
||||
func (ui *UI) oauth2Handler(
|
||||
func (ui *UI) newOAuthCodeReceiver(
|
||||
_ context.Context,
|
||||
arg oauthhandler.OAuthHandlerArgument,
|
||||
) error {
|
||||
codeCh, err := oauthhandler.NewCodeReceiver(arg.RedirectURL)
|
||||
if err != nil {
|
||||
return err
|
||||
platID streamcontrol.PlatformName,
|
||||
) (<-chan string, context.CancelFunc) {
|
||||
ui.CodeChMapLocker.Lock()
|
||||
defer ui.CodeChMapLocker.Unlock()
|
||||
|
||||
if oldCh, ok := ui.CodeChMap[platID]; ok {
|
||||
return oldCh, nil
|
||||
}
|
||||
|
||||
ui.OAuthURLOpenFn(arg.AuthURL)
|
||||
ch := make(chan string)
|
||||
ui.CodeChMap[platID] = ch
|
||||
|
||||
code := <-codeCh
|
||||
return ch, func() {
|
||||
ui.CodeChMapLocker.Lock()
|
||||
defer ui.CodeChMapLocker.Unlock()
|
||||
delete(ui.CodeChMap, platID)
|
||||
}
|
||||
}
|
||||
|
||||
func (ui *UI) getOAuthCodeReceiver(
|
||||
_ context.Context,
|
||||
platID streamcontrol.PlatformName,
|
||||
) chan<- string {
|
||||
ui.CodeChMapLocker.Lock()
|
||||
defer ui.CodeChMapLocker.Unlock()
|
||||
|
||||
return ui.CodeChMap[platID]
|
||||
}
|
||||
|
||||
func (ui *UI) oauth2Handler(
|
||||
ctx context.Context,
|
||||
platID streamcontrol.PlatformName,
|
||||
arg oauthhandler.OAuthHandlerArgument,
|
||||
) error {
|
||||
ctx, cancelFn := context.WithCancel(ctx)
|
||||
defer cancelFn()
|
||||
|
||||
codeCh, removeReceiver := ui.newOAuthCodeReceiver(ctx, platID)
|
||||
if codeCh == nil {
|
||||
return fmt.Errorf("there is already another oauth handler for this platform running")
|
||||
}
|
||||
if removeReceiver != nil {
|
||||
defer removeReceiver()
|
||||
}
|
||||
|
||||
logger.Debugf(ctx, "asking to open the URL: %s", arg.AuthURL)
|
||||
ui.OAuthURLOpenFn(arg.ListenPort, platID, arg.AuthURL)
|
||||
|
||||
t := time.NewTicker(time.Hour)
|
||||
defer t.Stop()
|
||||
for {
|
||||
logger.Debugf(ctx, "waiting for an auth code")
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case code := <-codeCh:
|
||||
return arg.ExchangeFn(code)
|
||||
case <-t.C:
|
||||
logger.Debugf(ctx, "re-asking to open the URL: %s", arg.AuthURL)
|
||||
ui.OAuthURLOpenFn(arg.ListenPort, platID, arg.AuthURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ui *UI) OnSubmittedOAuthCode(
|
||||
ctx context.Context,
|
||||
platID streamcontrol.PlatformName,
|
||||
code string,
|
||||
) error {
|
||||
codeCh := ui.getOAuthCodeReceiver(ctx, platID)
|
||||
if codeCh == nil {
|
||||
logger.Debugf(ctx, "no code receiver for '%s'", platID)
|
||||
return nil
|
||||
}
|
||||
|
||||
codeCh <- code
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ui *UI) OAuthHandlerTwitch(
|
||||
ctx context.Context,
|
||||
arg oauthhandler.OAuthHandlerArgument,
|
||||
) error {
|
||||
return ui.oauth2Handler(ctx, arg)
|
||||
return ui.oauth2Handler(ctx, twitch.ID, arg)
|
||||
}
|
||||
|
||||
func (ui *UI) OAuthHandlerYouTube(
|
||||
ctx context.Context,
|
||||
arg oauthhandler.OAuthHandlerArgument,
|
||||
) error {
|
||||
return ui.oauth2Handler(ctx, arg)
|
||||
return ui.oauth2Handler(ctx, youtube.ID, arg)
|
||||
}
|
||||
|
||||
func (*UI) InputTwitchUserInfo(
|
||||
|
||||
@@ -5,4 +5,4 @@ Website = "https://github.com/xaionaro/streamctl"
|
||||
Name = "streampanel"
|
||||
ID = "center.dx.streampanel"
|
||||
Version = "0.1.0"
|
||||
Build = 31
|
||||
Build = 37
|
||||
|
||||
@@ -17,10 +17,12 @@ import (
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/xaionaro-go/streamctl/pkg/observability"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/grpc/go/streamd_grpc"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/server"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streampanel"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streampanel/consts"
|
||||
_ "github.com/xaionaro-go/streamctl/pkg/streamserver"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
@@ -125,10 +127,25 @@ func main() {
|
||||
}
|
||||
|
||||
if *listenAddr != "" {
|
||||
go func() {
|
||||
grpcServer := grpc.NewServer()
|
||||
streamdGRPC := server.NewGRPCServer(panel.StreamD)
|
||||
streamd_grpc.RegisterStreamDServer(grpcServer, streamdGRPC)
|
||||
|
||||
// to erase an oauth request answered locally from "UnansweredOAuthRequests" in the GRPC server:
|
||||
panel.OnInternallySubmittedOAuthCode = func(
|
||||
ctx context.Context,
|
||||
platID streamcontrol.PlatformName,
|
||||
code string,
|
||||
) error {
|
||||
_, err := streamdGRPC.SubmitOAuthCode(ctx, &streamd_grpc.SubmitOAuthCodeRequest{
|
||||
PlatID: string(platID),
|
||||
Code: code,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// start the server:
|
||||
go func() {
|
||||
l.Infof("started server at %s", *listenAddr)
|
||||
err = grpcServer.Serve(listener)
|
||||
if err != nil {
|
||||
|
||||
23
go.mod
23
go.mod
@@ -72,7 +72,6 @@ require (
|
||||
github.com/google/gopacket v1.1.19 // indirect
|
||||
github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.2 // indirect
|
||||
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||
@@ -117,7 +116,7 @@ require (
|
||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.58 // indirect
|
||||
github.com/miekg/dns v1.1.59 // indirect
|
||||
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
|
||||
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
@@ -139,6 +138,10 @@ require (
|
||||
github.com/opencontainers/runtime-spec v1.2.0 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.14 // indirect
|
||||
github.com/pion/rtp v1.8.6 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.9 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
@@ -153,6 +156,8 @@ require (
|
||||
github.com/rymdport/portal v0.2.2 // indirect
|
||||
github.com/samber/lo v1.36.0 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 // indirect
|
||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/skeema/knownhosts v1.2.2 // indirect
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 // indirect
|
||||
@@ -178,13 +183,13 @@ require (
|
||||
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect
|
||||
golang.org/x/image v0.18.0 // indirect
|
||||
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
||||
@@ -200,15 +205,16 @@ require (
|
||||
|
||||
require (
|
||||
fyne.io/fyne/v2 v2.4.6-0.20240630191754-79694263c06a
|
||||
github.com/AlexxIT/go2rtc v1.9.4
|
||||
github.com/DataDog/gostackparse v0.6.0
|
||||
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-broadcast v0.0.0-20211018055107-71439988bd91
|
||||
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
|
||||
github.com/go-yaml/yaml v2.1.0+incompatible
|
||||
github.com/google/uuid v1.6.0
|
||||
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
|
||||
@@ -217,11 +223,12 @@ require (
|
||||
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/spf13/pflag v1.0.5
|
||||
github.com/xaionaro-go/datacounter v1.0.4
|
||||
github.com/xaionaro-go/unsafetools v0.0.0-20210722164218-75ba48cf7b3c
|
||||
github.com/yl2chen/cidranger v1.0.2
|
||||
golang.org/x/crypto v0.23.0
|
||||
golang.org/x/crypto v0.24.0
|
||||
google.golang.org/grpc v1.63.2
|
||||
google.golang.org/protobuf v1.33.1-0.20240408130810-98873a205002
|
||||
)
|
||||
|
||||
50
go.sum
50
go.sum
@@ -54,6 +54,8 @@ fyne.io/fyne/v2 v2.4.6-0.20240630191754-79694263c06a/go.mod h1:9D4oT3NWeG+MLi/lP
|
||||
fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
|
||||
fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/AlexxIT/go2rtc v1.9.4 h1:GC25fWz9S0XwZn/RV5Y4cV7UEGbtIWktYy8Aq96RROg=
|
||||
github.com/AlexxIT/go2rtc v1.9.4/go.mod h1:3nYj8jnqS0O38cCxa96fbifX1RF6GxAjlzhfEl32zeY=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
@@ -136,8 +138,6 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
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-broadcast v0.0.0-20211018055107-71439988bd91 h1:jAUM3D1KIrJmwx60DKB+a/qqM69yHnu6otDGVa2t0vs=
|
||||
github.com/dustin/go-broadcast v0.0.0-20211018055107-71439988bd91/go.mod h1:8rK6Kbo1Jd6sK22b24aPVgAm3jlNy1q1ft+lBALdIqA=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
|
||||
github.com/elastic/gosigar v0.14.2 h1:Dg80n8cr90OZ7x+bAax/QjoW/XqTI11RmA79ZwIm9/4=
|
||||
@@ -499,6 +499,7 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
@@ -506,8 +507,8 @@ github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00v
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||
github.com/miekg/dns v1.1.42/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
||||
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
|
||||
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
|
||||
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
|
||||
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
|
||||
github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8=
|
||||
github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms=
|
||||
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc=
|
||||
@@ -588,6 +589,14 @@ github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhM
|
||||
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
|
||||
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtp v1.8.6 h1:MTmn/b0aWWsAzux2AmP8WGllusBVw4NPYPVFFd7jUPw=
|
||||
github.com/pion/rtp v1.8.6/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
|
||||
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
|
||||
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
||||
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -626,6 +635,9 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
@@ -664,6 +676,10 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV
|
||||
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
|
||||
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I=
|
||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
|
||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA=
|
||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
@@ -709,6 +725,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
@@ -810,8 +827,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -858,8 +875,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -907,8 +924,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -1007,15 +1024,16 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1097,8 +1115,8 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
17
pkg/loggedsync/mutex.go
Normal file
17
pkg/loggedsync/mutex.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package loggedsync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
)
|
||||
|
||||
type Mutex struct {
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func (m *Mutex) LockCtx(ctx context.Context) {
|
||||
logger.Tracef(ctx, "")
|
||||
m.Mutex.Lock()
|
||||
}
|
||||
@@ -6,31 +6,16 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
)
|
||||
|
||||
type OAuthHandlerArgument struct {
|
||||
AuthURL string
|
||||
RedirectURL string
|
||||
ListenPort uint16
|
||||
ExchangeFn func(code string) error
|
||||
}
|
||||
|
||||
// it is guaranteed exchangeFn was called if error is nil.
|
||||
func OAuth2Handler(ctx context.Context, arg OAuthHandlerArgument) error {
|
||||
if arg.RedirectURL != "" {
|
||||
err := OAuth2HandlerViaBrowser(ctx, arg)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
logger.Errorf(ctx, "unable to authenticate automatically: %v", err)
|
||||
}
|
||||
return OAuth2HandlerViaCLI(ctx, arg)
|
||||
}
|
||||
|
||||
func OAuth2HandlerViaCLI(ctx context.Context, arg OAuthHandlerArgument) error {
|
||||
fmt.Printf(
|
||||
"It is required to get an oauth2 token. "+
|
||||
@@ -47,7 +32,9 @@ func OAuth2HandlerViaCLI(ctx context.Context, arg OAuthHandlerArgument) error {
|
||||
}
|
||||
|
||||
func OAuth2HandlerViaBrowser(ctx context.Context, arg OAuthHandlerArgument) error {
|
||||
codeCh, err := NewCodeReceiver(arg.RedirectURL)
|
||||
ctx, cancelFn := context.WithCancel(ctx)
|
||||
defer cancelFn()
|
||||
codeCh, _, err := NewCodeReceiver(ctx, arg.ListenPort)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -64,32 +51,39 @@ func OAuth2HandlerViaBrowser(ctx context.Context, arg OAuthHandlerArgument) erro
|
||||
return arg.ExchangeFn(code)
|
||||
}
|
||||
|
||||
func NewCodeReceiver(redirectURL string) (codeCh chan string, err error) {
|
||||
urlParsed, err := url.Parse(redirectURL)
|
||||
func NewCodeReceiver(
|
||||
ctx context.Context,
|
||||
listenPort uint16,
|
||||
) (chan string, uint16, error) {
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", listenPort))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse URL '%s': %w", redirectURL, err)
|
||||
return nil, 0, err
|
||||
}
|
||||
codeCh := make(chan string)
|
||||
|
||||
// this function was mostly borrowed from https://developers.google.com/youtube/v3/code_samples/go#authorize_a_request
|
||||
listener, err := net.Listen("tcp", urlParsed.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
codeCh = make(chan string)
|
||||
|
||||
go http.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
srv := &http.Server{
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
code := r.FormValue("code")
|
||||
codeCh <- code
|
||||
listener.Close()
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
if code == "" {
|
||||
fmt.Fprintf(w, "No code received :(\r\n\r\nYou can close this browser window.")
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "Received code: %v\r\n\r\nYou can now safely close this browser window.", code)
|
||||
}))
|
||||
}),
|
||||
}
|
||||
|
||||
return codeCh, nil
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
listener.Close()
|
||||
srv.Close()
|
||||
close(codeCh)
|
||||
}()
|
||||
|
||||
go srv.Serve(listener)
|
||||
|
||||
return codeCh, uint16(listener.Addr().(*net.TCPAddr).Port), nil
|
||||
}
|
||||
|
||||
func LaunchBrowser(url string) error {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
@@ -18,7 +19,11 @@ import (
|
||||
|
||||
type Twitch struct {
|
||||
client *helix.Client
|
||||
config Config
|
||||
broadcasterID string
|
||||
lazyInitOnce sync.Once
|
||||
saveCfgFn func(Config) error
|
||||
tokenLocker sync.Mutex
|
||||
}
|
||||
|
||||
var _ streamcontrol.StreamController[StreamProfile] = (*Twitch)(nil)
|
||||
@@ -26,7 +31,7 @@ var _ streamcontrol.StreamController[StreamProfile] = (*Twitch)(nil)
|
||||
func New(
|
||||
ctx context.Context,
|
||||
cfg Config,
|
||||
safeCfgFn func(Config) error,
|
||||
saveCfgFn func(Config) error,
|
||||
) (*Twitch, error) {
|
||||
if cfg.Config.Channel == "" {
|
||||
return nil, fmt.Errorf("'channel' is not set")
|
||||
@@ -34,20 +39,17 @@ func New(
|
||||
if cfg.Config.ClientID == "" || cfg.Config.ClientSecret == "" {
|
||||
return nil, fmt.Errorf("'clientid' or/and 'clientsecret' is/are not set; go to https://dev.twitch.tv/console/apps/create and create an app if it not created, yet")
|
||||
}
|
||||
client, err := getClient(ctx, cfg, safeCfgFn)
|
||||
client, err := getClient(ctx, cfg, saveCfgFn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.Debugf(ctx, "initialized a client")
|
||||
broadcasterID, err := getUserID(ctx, client, cfg.Config.Channel)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get the user ID: %w", err)
|
||||
}
|
||||
logger.Debugf(ctx, "broadcaster_id: %s (login: %s)", broadcasterID, cfg.Config.Channel)
|
||||
return &Twitch{
|
||||
t := &Twitch{
|
||||
client: client,
|
||||
broadcasterID: broadcasterID,
|
||||
}, nil
|
||||
config: cfg,
|
||||
saveCfgFn: saveCfgFn,
|
||||
}
|
||||
logger.Debugf(ctx, "initialized a client")
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func getUserID(
|
||||
@@ -67,6 +69,25 @@ func getUserID(
|
||||
return resp.Data.Users[0].ID, nil
|
||||
}
|
||||
|
||||
func (t *Twitch) prepare(ctx context.Context) error {
|
||||
err := t.getTokenIfNeeded(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.lazyInitOnce.Do(func() {
|
||||
if t.broadcasterID != "" {
|
||||
return
|
||||
}
|
||||
t.broadcasterID, err = getUserID(ctx, t.client, t.config.Config.Channel)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
logger.Debugf(ctx, "broadcaster_id: %s (login: %s)", t.broadcasterID, t.config.Config.Channel)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *Twitch) Close() error {
|
||||
return nil
|
||||
}
|
||||
@@ -125,6 +146,8 @@ func (t *Twitch) ApplyProfile(
|
||||
profile StreamProfile,
|
||||
customArgs ...any,
|
||||
) error {
|
||||
t.prepare(ctx)
|
||||
|
||||
if profile.CategoryName != nil {
|
||||
if profile.CategoryID != nil {
|
||||
logger.Warnf(ctx, "both category name and ID are set; these are contradicting stream profile settings; prioritizing the name")
|
||||
@@ -195,6 +218,7 @@ func (t *Twitch) SetTitle(
|
||||
ctx context.Context,
|
||||
title string,
|
||||
) error {
|
||||
t.prepare(ctx)
|
||||
return t.editChannelInfo(ctx, &helix.EditChannelInformationParams{
|
||||
Title: title,
|
||||
})
|
||||
@@ -233,6 +257,7 @@ func (t *Twitch) StartStream(
|
||||
profile StreamProfile,
|
||||
customArgs ...any,
|
||||
) error {
|
||||
t.prepare(ctx)
|
||||
var result error
|
||||
if err := t.SetTitle(ctx, title); err != nil {
|
||||
result = multierror.Append(result, fmt.Errorf("unable to set title: %w", err))
|
||||
@@ -256,6 +281,7 @@ func (t *Twitch) EndStream(
|
||||
func (t *Twitch) GetStreamStatus(
|
||||
ctx context.Context,
|
||||
) (*streamcontrol.StreamStatus, error) {
|
||||
t.prepare(ctx)
|
||||
// Twitch ends a stream automatically, nothing to do:
|
||||
reply, err := t.client.GetStreams(&helix.StreamsParams{
|
||||
UserIDs: []string{t.broadcasterID},
|
||||
@@ -281,15 +307,185 @@ func (t *Twitch) GetStreamStatus(
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *Twitch) getTokenIfNeeded(
|
||||
ctx context.Context,
|
||||
) error {
|
||||
switch t.config.Config.AuthType {
|
||||
case "user":
|
||||
t.tokenLocker.Lock()
|
||||
t.client.SetUserAccessToken(t.config.Config.UserAccessToken)
|
||||
t.client.SetRefreshToken(t.config.Config.RefreshToken)
|
||||
t.tokenLocker.Unlock()
|
||||
if t.config.Config.UserAccessToken != "" {
|
||||
return nil
|
||||
}
|
||||
case "app":
|
||||
t.tokenLocker.Lock()
|
||||
t.client.SetUserAccessToken(t.config.Config.AppAccessToken) // shouldn't it be "SetAppAccessToken"?
|
||||
t.tokenLocker.Unlock()
|
||||
if t.config.Config.AppAccessToken != "" {
|
||||
logger.Debugf(ctx, "already have an app access token")
|
||||
return nil
|
||||
}
|
||||
logger.Debugf(ctx, "do not have an app access token")
|
||||
}
|
||||
|
||||
return t.getNewToken(ctx)
|
||||
}
|
||||
|
||||
func (t *Twitch) getNewToken(
|
||||
ctx context.Context,
|
||||
) error {
|
||||
t.tokenLocker.Lock()
|
||||
defer t.tokenLocker.Unlock()
|
||||
|
||||
switch t.config.Config.AuthType {
|
||||
case "user":
|
||||
if t.config.Config.ClientCode == "" {
|
||||
getPortsFn := t.config.Config.GetOAuthListenPorts
|
||||
if getPortsFn == nil {
|
||||
return fmt.Errorf("the function GetOAuthListenPorts is not set")
|
||||
}
|
||||
|
||||
oauthHandler := t.config.Config.CustomOAuthHandler
|
||||
if oauthHandler == nil {
|
||||
oauthHandler = oauthhandler.OAuth2HandlerViaCLI
|
||||
}
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(ctx)
|
||||
|
||||
var errWg sync.WaitGroup
|
||||
var resultErr error
|
||||
errCh := make(chan error)
|
||||
errWg.Add(1)
|
||||
go func() {
|
||||
errWg.Done()
|
||||
for err := range errCh {
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
resultErr = multierror.Append(resultErr, err)
|
||||
}
|
||||
}()
|
||||
|
||||
alreadyListening := map[uint16]struct{}{}
|
||||
var wg sync.WaitGroup
|
||||
success := false
|
||||
|
||||
startHandlerForPort := func(listenPort uint16) {
|
||||
if _, ok := alreadyListening[listenPort]; ok {
|
||||
return
|
||||
}
|
||||
logger.Tracef(ctx, "starting the oauth handler at port %d", listenPort)
|
||||
wg.Add(1)
|
||||
go func(listenPort uint16) {
|
||||
defer wg.Done()
|
||||
authURL := GetAuthorizationURL(
|
||||
&helix.AuthorizationURLParams{
|
||||
ResponseType: "code", // or "token"
|
||||
Scopes: []string{"channel:manage:broadcast"},
|
||||
},
|
||||
t.config.Config.ClientID,
|
||||
fmt.Sprintf("127.0.0.1:%d", listenPort),
|
||||
)
|
||||
|
||||
arg := oauthhandler.OAuthHandlerArgument{
|
||||
AuthURL: authURL,
|
||||
ExchangeFn: func(code string) error {
|
||||
t.config.Config.ClientCode = code
|
||||
err := t.saveCfgFn(t.config)
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
err := oauthHandler(ctx, arg)
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("unable to get or exchange the oauth code to a token: %w", err)
|
||||
return
|
||||
}
|
||||
cancelFunc()
|
||||
success = true
|
||||
}(listenPort)
|
||||
}
|
||||
|
||||
for _, listenPort := range getPortsFn() {
|
||||
startHandlerForPort(listenPort)
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
t := time.NewTicker(time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
}
|
||||
ports := getPortsFn()
|
||||
logger.Tracef(ctx, "oauth listener ports: %#+v", ports)
|
||||
|
||||
alreadyListeningNext := map[uint16]struct{}{}
|
||||
for _, listenPort := range ports {
|
||||
startHandlerForPort(listenPort)
|
||||
alreadyListeningNext[listenPort] = struct{}{}
|
||||
}
|
||||
alreadyListening = alreadyListeningNext
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(errCh)
|
||||
}()
|
||||
<-ctx.Done()
|
||||
if !success {
|
||||
errWg.Wait()
|
||||
return resultErr
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := t.client.RequestUserAccessToken(t.config.Config.ClientCode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get user access token: %w", err)
|
||||
}
|
||||
if resp.ErrorStatus != 0 {
|
||||
return fmt.Errorf("unable to query: %d %v: %v", resp.ErrorStatus, resp.Error, resp.ErrorMessage)
|
||||
}
|
||||
t.client.SetUserAccessToken(resp.Data.AccessToken)
|
||||
t.client.SetRefreshToken(resp.Data.RefreshToken)
|
||||
t.config.Config.ClientCode = ""
|
||||
t.config.Config.UserAccessToken = resp.Data.AccessToken
|
||||
t.config.Config.RefreshToken = resp.Data.RefreshToken
|
||||
err = t.saveCfgFn(t.config)
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
case "app":
|
||||
resp, err := t.client.RequestAppAccessToken(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get app access token: %w", err)
|
||||
}
|
||||
if resp.ErrorStatus != 0 {
|
||||
return fmt.Errorf("unable to get app access token (the response contains an error): %d %v: %v", resp.ErrorStatus, resp.Error, resp.ErrorMessage)
|
||||
}
|
||||
logger.Debugf(ctx, "setting the app access token")
|
||||
t.client.SetAppAccessToken(resp.Data.AccessToken)
|
||||
t.config.Config.AppAccessToken = resp.Data.AccessToken
|
||||
err = t.saveCfgFn(t.config)
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
default:
|
||||
return fmt.Errorf("invalid AuthType: <%s>", t.config.Config.AuthType)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getClient(
|
||||
ctx context.Context,
|
||||
cfg Config,
|
||||
safeCfgFn func(Config) error,
|
||||
saveCfgFn func(Config) error,
|
||||
) (*helix.Client, error) {
|
||||
options := &helix.Options{
|
||||
ClientID: cfg.Config.ClientID,
|
||||
ClientSecret: cfg.Config.ClientSecret,
|
||||
RedirectURI: "http://0.0.0.0:8091/", // TODO: make this secure and also random
|
||||
}
|
||||
client, err := helix.NewClient(options)
|
||||
if err != nil {
|
||||
@@ -299,89 +495,41 @@ func getClient(
|
||||
logger.Debugf(ctx, "updated tokens")
|
||||
cfg.Config.UserAccessToken = newAccessToken
|
||||
cfg.Config.RefreshToken = newRefreshToken
|
||||
err := safeCfgFn(cfg)
|
||||
err := saveCfgFn(cfg)
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
})
|
||||
|
||||
switch cfg.Config.AuthType {
|
||||
case "user":
|
||||
client.SetUserAccessToken(cfg.Config.UserAccessToken)
|
||||
client.SetRefreshToken(cfg.Config.RefreshToken)
|
||||
|
||||
if cfg.Config.UserAccessToken == "" {
|
||||
if cfg.Config.ClientCode == "" {
|
||||
authURL := client.GetAuthorizationURL(&helix.AuthorizationURLParams{
|
||||
ResponseType: "code", // or "token"
|
||||
Scopes: []string{"channel:manage:broadcast"},
|
||||
})
|
||||
|
||||
arg := oauthhandler.OAuthHandlerArgument{
|
||||
AuthURL: authURL,
|
||||
RedirectURL: options.RedirectURI,
|
||||
ExchangeFn: func(code string) error {
|
||||
cfg.Config.ClientCode = code
|
||||
err = safeCfgFn(cfg)
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
oauthHandler := cfg.Config.CustomOAuthHandler
|
||||
if oauthHandler == nil {
|
||||
oauthHandler = oauthhandler.OAuth2HandlerViaCLI
|
||||
}
|
||||
|
||||
err := oauthHandler(ctx, arg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get or exchange the oauth code to a token: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := client.RequestUserAccessToken(cfg.Config.ClientCode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get user access token: %w", err)
|
||||
}
|
||||
if resp.ErrorStatus != 0 {
|
||||
return nil, fmt.Errorf("unable to query: %d %v: %v", resp.ErrorStatus, resp.Error, resp.ErrorMessage)
|
||||
}
|
||||
client.SetUserAccessToken(resp.Data.AccessToken)
|
||||
client.SetRefreshToken(resp.Data.RefreshToken)
|
||||
cfg.Config.ClientCode = ""
|
||||
cfg.Config.UserAccessToken = resp.Data.AccessToken
|
||||
cfg.Config.RefreshToken = resp.Data.RefreshToken
|
||||
err = safeCfgFn(cfg)
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
}
|
||||
case "app":
|
||||
if cfg.Config.AppAccessToken != "" {
|
||||
logger.Debugf(ctx, "already have an app access token")
|
||||
client.SetUserAccessToken(cfg.Config.AppAccessToken) // shouldn't it be "SetAppAccessToken"?
|
||||
break
|
||||
}
|
||||
logger.Debugf(ctx, "do not have an app access token")
|
||||
|
||||
resp, err := client.RequestAppAccessToken(nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get app access token: %w", err)
|
||||
}
|
||||
if resp.ErrorStatus != 0 {
|
||||
return nil, fmt.Errorf("unable to get app access token (the response contains an error): %d %v: %v", resp.ErrorStatus, resp.Error, resp.ErrorMessage)
|
||||
}
|
||||
logger.Debugf(ctx, "setting the app access token")
|
||||
client.SetAppAccessToken(resp.Data.AccessToken)
|
||||
cfg.Config.AppAccessToken = resp.Data.AccessToken
|
||||
err = safeCfgFn(cfg)
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid AuthType: <%s>", cfg.Config.AuthType)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func GetAuthorizationURL(
|
||||
params *helix.AuthorizationURLParams,
|
||||
clientID string,
|
||||
redirectURI string,
|
||||
) string {
|
||||
url := helix.AuthBaseURL + "/authorize"
|
||||
url += "?response_type=" + params.ResponseType
|
||||
url += "&client_id=" + clientID
|
||||
url += "&redirect_uri=" + redirectURI
|
||||
|
||||
if params.State != "" {
|
||||
url += "&state=" + params.State
|
||||
}
|
||||
|
||||
if params.ForceVerify {
|
||||
url += "&force_verify=true"
|
||||
}
|
||||
|
||||
if len(params.Scopes) != 0 {
|
||||
url += "&scope=" + strings.Join(params.Scopes, "%20")
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
func (t *Twitch) GetAllCategories(
|
||||
ctx context.Context,
|
||||
) ([]helix.Game, error) {
|
||||
t.prepare(ctx)
|
||||
categoriesMap := map[string]helix.Game{}
|
||||
var pagination *helix.Pagination
|
||||
for {
|
||||
|
||||
@@ -21,6 +21,7 @@ type PlatformSpecificConfig struct {
|
||||
UserAccessToken string
|
||||
RefreshToken string
|
||||
CustomOAuthHandler OAuthHandler `yaml:"-"`
|
||||
GetOAuthListenPorts func() []uint16 `yaml:"-"`
|
||||
}
|
||||
|
||||
type Config = streamctl.PlatformConfig[PlatformSpecificConfig, StreamProfile]
|
||||
|
||||
@@ -17,6 +17,7 @@ type PlatformSpecificConfig struct {
|
||||
ClientSecret string
|
||||
Token *oauth2.Token
|
||||
CustomOAuthHandler OAuthHandler `yaml:"-"`
|
||||
GetOAuthListenPorts func() []uint16 `yaml:"-"`
|
||||
}
|
||||
|
||||
type Config = streamctl.PlatformConfig[PlatformSpecificConfig, StreamProfile]
|
||||
|
||||
@@ -11,12 +11,14 @@ import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/facebookincubator/go-belt/tool/experimental/errmon"
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
"github.com/go-yaml/yaml"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/xaionaro-go/streamctl/pkg/oauthhandler"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol"
|
||||
"golang.org/x/oauth2"
|
||||
@@ -74,6 +76,10 @@ func New(
|
||||
case <-ticker.C:
|
||||
err := yt.checkToken(ctx)
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
if err != nil && strings.Contains(err.Error(), "expired or revoked") {
|
||||
_, err := yt.getNewToken(ctx)
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -82,8 +88,8 @@ func New(
|
||||
}
|
||||
|
||||
func (yt *YouTube) checkToken(ctx context.Context) (_err error) {
|
||||
logger.Debugf(ctx, "YouTube.checkToken")
|
||||
defer func() { logger.Debugf(ctx, "/YouTube.checkToken: %v", _err) }()
|
||||
logger.Tracef(ctx, "YouTube.checkToken")
|
||||
defer func() { logger.Tracef(ctx, "/YouTube.checkToken: %v", _err) }()
|
||||
|
||||
yt.locker.Lock()
|
||||
defer yt.locker.Unlock()
|
||||
@@ -91,24 +97,29 @@ func (yt *YouTube) checkToken(ctx context.Context) (_err error) {
|
||||
}
|
||||
|
||||
func (yt *YouTube) checkTokenNoLock(ctx context.Context) (_err error) {
|
||||
logger.Debugf(ctx, "YouTube.checkTokenNoLock")
|
||||
defer func() { logger.Debugf(ctx, "/YouTube.checkTokenNoLock: %v", _err) }()
|
||||
logger.Tracef(ctx, "YouTube.checkTokenNoLock")
|
||||
defer func() { logger.Tracef(ctx, "/YouTube.checkTokenNoLock: %v", _err) }()
|
||||
|
||||
tokenSource := getAuthCfg(yt.Config).TokenSource(ctx, yt.Config.Config.Token)
|
||||
logger.Debugf(ctx, "checking if the token changed")
|
||||
tokenSource := getAuthCfgBase(yt.Config).TokenSource(ctx, yt.Config.Config.Token)
|
||||
counter := 0
|
||||
for {
|
||||
logger.Tracef(ctx, "checking if the token changed")
|
||||
token, err := tokenSource.Token()
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "unable to get a token: %v", err)
|
||||
return err
|
||||
if yt.fixError(ctx, err, &counter) {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("unable to get a token: %v", err)
|
||||
}
|
||||
if token.AccessToken == yt.Config.Config.Token.AccessToken {
|
||||
logger.Debugf(ctx, "the token have not change")
|
||||
return err
|
||||
logger.Tracef(ctx, "the token have not changed")
|
||||
return nil
|
||||
}
|
||||
logger.Debugf(ctx, "the token have changed")
|
||||
logger.Tracef(ctx, "the token have changed")
|
||||
yt.Config.Config.Token = token
|
||||
return yt.SaveConfigFunc(yt.Config)
|
||||
}
|
||||
}
|
||||
|
||||
func (yt *YouTube) getNewToken(ctx context.Context) (_ret *oauth2.Token, _err error) {
|
||||
logger.Debugf(ctx, "YouTube.getNewToken")
|
||||
@@ -144,7 +155,9 @@ func (yt *YouTube) init(ctx context.Context) (_err error) {
|
||||
isNewToken = true
|
||||
}
|
||||
|
||||
tokenSource := getAuthCfg(yt.Config).TokenSource(ctx, yt.Config.Config.Token)
|
||||
authCfg := getAuthCfgBase(yt.Config)
|
||||
|
||||
tokenSource := authCfg.TokenSource(ctx, yt.Config.Config.Token)
|
||||
|
||||
if !isNewToken {
|
||||
if err := yt.checkTokenNoLock(ctx); err != nil {
|
||||
@@ -155,7 +168,7 @@ func (yt *YouTube) init(ctx context.Context) (_err error) {
|
||||
return err
|
||||
}
|
||||
isNewToken = true
|
||||
tokenSource = getAuthCfg(yt.Config).TokenSource(ctx, yt.Config.Config.Token)
|
||||
tokenSource = authCfg.TokenSource(ctx, yt.Config.Config.Token)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,12 +188,11 @@ func (yt *YouTube) init(ctx context.Context) (_err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAuthCfg(cfg Config) *oauth2.Config {
|
||||
func getAuthCfgBase(cfg Config) *oauth2.Config {
|
||||
return &oauth2.Config{
|
||||
ClientID: cfg.Config.ClientID,
|
||||
ClientSecret: cfg.Config.ClientSecret,
|
||||
Endpoint: google.Endpoint,
|
||||
RedirectURL: "http://0.0.0.0:8090", // TODO: make this secure and also random
|
||||
Scopes: []string{
|
||||
"https://www.googleapis.com/auth/youtube",
|
||||
},
|
||||
@@ -188,18 +200,51 @@ func getAuthCfg(cfg Config) *oauth2.Config {
|
||||
}
|
||||
|
||||
func getToken(ctx context.Context, cfg Config) (*oauth2.Token, error) {
|
||||
googleAuthCfg := getAuthCfg(cfg)
|
||||
if cfg.Config.GetOAuthListenPorts == nil {
|
||||
return nil, fmt.Errorf("function GetOAuthListenPorts is not set")
|
||||
}
|
||||
|
||||
ctx, cancelFn := context.WithCancel(ctx)
|
||||
defer cancelFn()
|
||||
|
||||
var errWg sync.WaitGroup
|
||||
var resultErr error
|
||||
errCh := make(chan error)
|
||||
errWg.Add(1)
|
||||
go func() {
|
||||
errWg.Done()
|
||||
for err := range errCh {
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
resultErr = multierror.Append(resultErr, err)
|
||||
}
|
||||
}()
|
||||
|
||||
alreadyListening := map[uint16]struct{}{}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var tok *oauth2.Token
|
||||
|
||||
startHandlerForPort := func(listenPort uint16) {
|
||||
if _, ok := alreadyListening[listenPort]; ok {
|
||||
return
|
||||
}
|
||||
logger.Tracef(ctx, "starting the oauth handler at port %d", listenPort)
|
||||
alreadyListening[listenPort] = struct{}{}
|
||||
oauthCfg := getAuthCfgBase(cfg)
|
||||
oauthCfg.RedirectURL = fmt.Sprintf("http://127.0.0.1:%d", listenPort)
|
||||
wg.Add(1)
|
||||
go func(oauthCfg *oauth2.Config) {
|
||||
defer wg.Done()
|
||||
oauthHandlerArg := oauthhandler.OAuthHandlerArgument{
|
||||
AuthURL: googleAuthCfg.AuthCodeURL("state-token", oauth2.AccessTypeOffline),
|
||||
RedirectURL: googleAuthCfg.RedirectURL,
|
||||
AuthURL: oauthCfg.AuthCodeURL("state-token", oauth2.AccessTypeOffline),
|
||||
ListenPort: listenPort,
|
||||
ExchangeFn: func(code string) error {
|
||||
_tok, err := googleAuthCfg.Exchange(ctx, code)
|
||||
_tok, err := oauthCfg.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get a token: %w", err)
|
||||
}
|
||||
tok = _tok
|
||||
cancelFn()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -208,9 +253,51 @@ func getToken(ctx context.Context, cfg Config) (*oauth2.Token, error) {
|
||||
if oauthHandler == nil {
|
||||
oauthHandler = oauthhandler.OAuth2HandlerViaCLI
|
||||
}
|
||||
logger.Tracef(ctx, "calling oauthHandler for %d", listenPort)
|
||||
err := oauthHandler(ctx, oauthHandlerArg)
|
||||
logger.Tracef(ctx, "called oauthHandler for %d: %v", listenPort, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
}(oauthCfg)
|
||||
}
|
||||
|
||||
for _, listenPort := range cfg.Config.GetOAuthListenPorts() {
|
||||
startHandlerForPort(listenPort)
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
t := time.NewTicker(time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
}
|
||||
ports := cfg.Config.GetOAuthListenPorts()
|
||||
logger.Tracef(ctx, "oauth listener ports: %#+v", ports)
|
||||
|
||||
alreadyListeningNext := map[uint16]struct{}{}
|
||||
for _, listenPort := range ports {
|
||||
startHandlerForPort(listenPort)
|
||||
alreadyListeningNext[listenPort] = struct{}{}
|
||||
}
|
||||
alreadyListening = alreadyListeningNext
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(errCh)
|
||||
}()
|
||||
<-ctx.Done()
|
||||
|
||||
if tok == nil {
|
||||
errWg.Wait()
|
||||
return nil, resultErr
|
||||
}
|
||||
|
||||
return tok, nil
|
||||
@@ -575,9 +662,11 @@ func (yt *YouTube) StartStream(
|
||||
now := time.Now().UTC()
|
||||
broadcast.Id = ""
|
||||
broadcast.Etag = ""
|
||||
broadcast.ContentDetails.EnableAutoStop = false
|
||||
broadcast.ContentDetails.BoundStreamLastUpdateTimeMs = ""
|
||||
broadcast.ContentDetails.BoundStreamId = ""
|
||||
broadcast.ContentDetails.MonitorStream = nil
|
||||
broadcast.ContentDetails.ForceSendFields = []string{"EnableAutoStop"}
|
||||
broadcast.Snippet.ScheduledStartTime = now.Format("2006-01-02T15:04:05") + ".00Z"
|
||||
broadcast.Snippet.ScheduledEndTime = now.Add(time.Hour*12).Format("2006-01-02T15:04:05") + ".00Z"
|
||||
broadcast.Snippet.LiveChatId = ""
|
||||
@@ -696,6 +785,7 @@ func (yt *YouTube) EndStream(
|
||||
) error {
|
||||
return yt.updateActiveBroadcasts(ctx, func(broadcast *youtube.LiveBroadcast) error {
|
||||
broadcast.ContentDetails.EnableAutoStop = true
|
||||
broadcast.ContentDetails.MonitorStream.ForceSendFields = []string{"BroadcastStreamDelayMs"}
|
||||
return nil
|
||||
}, "contentDetails")
|
||||
}
|
||||
@@ -842,17 +932,16 @@ func (yt *YouTube) ListBroadcasts(
|
||||
}
|
||||
|
||||
func (yt *YouTube) fixError(ctx context.Context, err error, counterPtr *int) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if *counterPtr > 2 {
|
||||
return false
|
||||
}
|
||||
*counterPtr++
|
||||
|
||||
gErr := &googleapi.Error{}
|
||||
if !errors.As(err, &gErr) {
|
||||
return false
|
||||
}
|
||||
|
||||
if gErr.Code == 401 {
|
||||
tryGetNewToken := func() bool {
|
||||
logger.Debugf(ctx, "trying to get a new token")
|
||||
_, tErr := yt.getNewToken(ctx)
|
||||
if tErr != nil {
|
||||
logger.Errorf(ctx, "unable to get a new token: %w", err)
|
||||
@@ -866,5 +955,20 @@ func (yt *YouTube) fixError(ctx context.Context, err error, counterPtr *int) boo
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.Contains(err.Error(), "token expired") {
|
||||
logger.Tracef(ctx, "token expired")
|
||||
return tryGetNewToken()
|
||||
}
|
||||
|
||||
gErr := &googleapi.Error{}
|
||||
if !errors.As(err, &gErr) {
|
||||
return false
|
||||
}
|
||||
|
||||
if gErr.Code == 401 {
|
||||
logger.Tracef(ctx, "error 401")
|
||||
return tryGetNewToken()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
32
pkg/streamd/api/grpcconv/stream_server.go
Normal file
32
pkg/streamd/api/grpcconv/stream_server.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package grpcconv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/api"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/grpc/go/streamd_grpc"
|
||||
)
|
||||
|
||||
func StreamServerTypeGo2GRPC(t api.StreamServerType) (streamd_grpc.StreamServerType, error) {
|
||||
switch t {
|
||||
case api.StreamServerTypeUndefined:
|
||||
return streamd_grpc.StreamServerType_Undefined, nil
|
||||
case api.StreamServerTypeRTMP:
|
||||
return streamd_grpc.StreamServerType_RTMP, nil
|
||||
case api.StreamServerTypeRTSP:
|
||||
return streamd_grpc.StreamServerType_RTSP, nil
|
||||
}
|
||||
return streamd_grpc.StreamServerType_Undefined, fmt.Errorf("unexpected value: %v", t)
|
||||
}
|
||||
|
||||
func StreamServerTypeGRPC2Go(t streamd_grpc.StreamServerType) (api.StreamServerType, error) {
|
||||
switch t {
|
||||
case streamd_grpc.StreamServerType_Undefined:
|
||||
return api.StreamServerTypeUndefined, nil
|
||||
case streamd_grpc.StreamServerType_RTMP:
|
||||
return api.StreamServerTypeRTMP, nil
|
||||
case streamd_grpc.StreamServerType_RTSP:
|
||||
return api.StreamServerTypeRTSP, nil
|
||||
}
|
||||
return api.StreamServerTypeUndefined, fmt.Errorf("unexpected value: %v", t)
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/cache"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/config"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/grpc/go/streamd_grpc"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streampanel/consts"
|
||||
)
|
||||
|
||||
@@ -58,6 +59,52 @@ type StreamD interface {
|
||||
ctx context.Context,
|
||||
req *scenes.SetCurrentProgramSceneParams,
|
||||
) error
|
||||
|
||||
SubmitOAuthCode(
|
||||
context.Context,
|
||||
*streamd_grpc.SubmitOAuthCodeRequest,
|
||||
) (*streamd_grpc.SubmitOAuthCodeReply, error)
|
||||
|
||||
ListStreamServers(
|
||||
ctx context.Context,
|
||||
) ([]StreamServer, error)
|
||||
StartStreamServer(
|
||||
ctx context.Context,
|
||||
serverType StreamServerType,
|
||||
listenAddr string,
|
||||
) error
|
||||
StopStreamServer(
|
||||
ctx context.Context,
|
||||
listenAddr string,
|
||||
) error
|
||||
ListIncomingStreams(
|
||||
ctx context.Context,
|
||||
) ([]IncomingStream, error)
|
||||
ListStreamDestinations(
|
||||
ctx context.Context,
|
||||
) ([]StreamDestination, error)
|
||||
AddStreamDestination(
|
||||
ctx context.Context,
|
||||
streamID StreamID,
|
||||
url string,
|
||||
) error
|
||||
RemoveStreamDestination(
|
||||
ctx context.Context,
|
||||
streamID StreamID,
|
||||
) error
|
||||
ListStreamForwards(
|
||||
ctx context.Context,
|
||||
) ([]StreamForward, error)
|
||||
AddStreamForward(
|
||||
ctx context.Context,
|
||||
streamIDSrc StreamID,
|
||||
streamIDDst StreamID,
|
||||
) error
|
||||
RemoveStreamForward(
|
||||
ctx context.Context,
|
||||
streamIDSrc StreamID,
|
||||
streamIDDst StreamID,
|
||||
) error
|
||||
}
|
||||
|
||||
type BackendDataOBS struct{}
|
||||
@@ -69,3 +116,32 @@ type BackendDataTwitch struct {
|
||||
type BackendDataYouTube struct {
|
||||
Cache cache.YouTube
|
||||
}
|
||||
|
||||
type StreamServerType int
|
||||
|
||||
const (
|
||||
StreamServerTypeUndefined = StreamServerType(iota)
|
||||
StreamServerTypeRTSP
|
||||
StreamServerTypeRTMP
|
||||
)
|
||||
|
||||
type StreamServer struct {
|
||||
Type StreamServerType
|
||||
ListenAddr string
|
||||
}
|
||||
|
||||
type StreamDestination struct {
|
||||
StreamID StreamID
|
||||
URL string
|
||||
}
|
||||
|
||||
type StreamForward struct {
|
||||
StreamIDSrc StreamID
|
||||
StreamIDDst StreamID
|
||||
}
|
||||
|
||||
type IncomingStream struct {
|
||||
StreamID StreamID
|
||||
}
|
||||
|
||||
type StreamID string
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
twitch "github.com/xaionaro-go/streamctl/pkg/streamcontrol/twitch/types"
|
||||
youtube "github.com/xaionaro-go/streamctl/pkg/streamcontrol/youtube/types"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/api"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/api/grpcconv"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/config"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/grpc/go/streamd_grpc"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streampanel/consts"
|
||||
@@ -438,15 +439,18 @@ func (c *Client) UpdateStream(
|
||||
|
||||
func (c *Client) SubscriberToOAuthURLs(
|
||||
ctx context.Context,
|
||||
) (chan string, error) {
|
||||
listenPort uint16,
|
||||
) (chan *streamd_grpc.OAuthRequest, error) {
|
||||
client, conn, err := c.grpcClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(chan string)
|
||||
result := make(chan *streamd_grpc.OAuthRequest)
|
||||
|
||||
subClient, err := client.SubscribeToOAuthRequests(ctx, &streamd_grpc.SubscribeToOAuthRequestsRequest{})
|
||||
subClient, err := client.SubscribeToOAuthRequests(ctx, &streamd_grpc.SubscribeToOAuthRequestsRequest{
|
||||
ListenPort: int32(listenPort),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to subscribe to oauth URLs: %w", err)
|
||||
}
|
||||
@@ -460,10 +464,15 @@ func (c *Client) SubscriberToOAuthURLs(
|
||||
for {
|
||||
res, err := subClient.Recv()
|
||||
if err == io.EOF {
|
||||
logger.Debugf(ctx, "the receiver is closed: %v", err)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "unable to read data: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
result <- res.GetAuthURL()
|
||||
result <- res
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -605,3 +614,250 @@ func (c *Client) OBSSetCurrentProgramScene(
|
||||
func ptr[T any](in T) *T {
|
||||
return &in
|
||||
}
|
||||
|
||||
func (c *Client) SubmitOAuthCode(
|
||||
ctx context.Context,
|
||||
req *streamd_grpc.SubmitOAuthCodeRequest,
|
||||
) (*streamd_grpc.SubmitOAuthCodeReply, error) {
|
||||
client, conn, err := c.grpcClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
return client.SubmitOAuthCode(ctx, req)
|
||||
}
|
||||
|
||||
func (c *Client) ListStreamServers(
|
||||
ctx context.Context,
|
||||
) ([]api.StreamServer, error) {
|
||||
client, conn, err := c.grpcClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
reply, err := client.ListStreamServers(
|
||||
ctx,
|
||||
&streamd_grpc.ListStreamServersRequest{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to request to list of the stream servers: %w", err)
|
||||
}
|
||||
var result []api.StreamServer
|
||||
for _, server := range reply.GetStreamServers() {
|
||||
t, err := grpcconv.StreamServerTypeGRPC2Go(server.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(),
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) StartStreamServer(
|
||||
ctx context.Context,
|
||||
serverType api.StreamServerType,
|
||||
listenAddr string,
|
||||
) error {
|
||||
client, conn, err := c.grpcClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
t, err := grpcconv.StreamServerTypeGo2GRPC(serverType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to convert the server type: %w", err)
|
||||
}
|
||||
_, err = client.StartStreamServer(ctx, &streamd_grpc.StartStreamServerRequest{
|
||||
Config: &streamd_grpc.StreamServer{
|
||||
ServerType: t,
|
||||
ListenAddr: listenAddr,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to request to start the stream server: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) StopStreamServer(
|
||||
ctx context.Context,
|
||||
listenAddr string,
|
||||
) error {
|
||||
client, conn, err := c.grpcClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = client.StopStreamServer(ctx, &streamd_grpc.StopStreamServerRequest{
|
||||
ListenAddr: listenAddr,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to request to stop the stream server: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) ListIncomingStreams(
|
||||
ctx context.Context,
|
||||
) ([]api.IncomingStream, error) {
|
||||
client, conn, err := c.grpcClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
reply, err := client.ListIncomingStreams(ctx, &streamd_grpc.ListIncomingStreamsRequest{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to request to list the incoming streams: %w", err)
|
||||
}
|
||||
|
||||
var result []api.IncomingStream
|
||||
for _, stream := range reply.GetIncomingStreams() {
|
||||
result = append(result, api.IncomingStream{
|
||||
StreamID: api.StreamID(stream.GetStreamID()),
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) ListStreamDestinations(
|
||||
ctx context.Context,
|
||||
) ([]api.StreamDestination, error) {
|
||||
client, conn, err := c.grpcClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
reply, err := client.ListStreamDestinations(ctx, &streamd_grpc.ListStreamDestinationsRequest{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to request to list the stream destinations: %w", err)
|
||||
}
|
||||
|
||||
var result []api.StreamDestination
|
||||
for _, dst := range reply.GetStreamDestinations() {
|
||||
result = append(result, api.StreamDestination{
|
||||
StreamID: api.StreamID(dst.GetStreamID()),
|
||||
URL: dst.GetUrl(),
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) AddStreamDestination(
|
||||
ctx context.Context,
|
||||
streamID api.StreamID,
|
||||
url string,
|
||||
) error {
|
||||
client, conn, err := c.grpcClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = client.AddStreamDestination(ctx, &streamd_grpc.AddStreamDestinationRequest{
|
||||
Config: &streamd_grpc.StreamDestination{
|
||||
StreamID: string(streamID),
|
||||
Url: url,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to request to add the stream destination: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) RemoveStreamDestination(
|
||||
ctx context.Context,
|
||||
streamID api.StreamID,
|
||||
) error {
|
||||
client, conn, err := c.grpcClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = client.RemoveStreamDestination(ctx, &streamd_grpc.RemoveStreamDestinationRequest{
|
||||
StreamID: string(streamID),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to request to remove the stream destination: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) ListStreamForwards(
|
||||
ctx context.Context,
|
||||
) ([]api.StreamForward, error) {
|
||||
client, conn, err := c.grpcClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
reply, err := client.ListStreamForwards(ctx, &streamd_grpc.ListStreamForwardsRequest{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to request to list the stream forwards: %w", err)
|
||||
}
|
||||
|
||||
var result []api.StreamForward
|
||||
for _, forward := range reply.GetStreamForwards() {
|
||||
result = append(result, api.StreamForward{
|
||||
StreamIDSrc: api.StreamID(forward.GetStreamIDSrc()),
|
||||
StreamIDDst: api.StreamID(forward.GetStreamIDDst()),
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) AddStreamForward(
|
||||
ctx context.Context,
|
||||
streamIDSrc api.StreamID,
|
||||
streamIDDst api.StreamID,
|
||||
) error {
|
||||
client, conn, err := c.grpcClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = client.AddStreamForward(ctx, &streamd_grpc.AddStreamForwardRequest{
|
||||
Config: &streamd_grpc.StreamForward{
|
||||
StreamIDSrc: string(streamIDSrc),
|
||||
StreamIDDst: string(streamIDDst),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to request to add the stream forward: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) RemoveStreamForward(
|
||||
ctx context.Context,
|
||||
streamIDSrc api.StreamID,
|
||||
streamIDDst api.StreamID,
|
||||
) error {
|
||||
client, conn, err := c.grpcClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = client.RemoveStreamForward(ctx, &streamd_grpc.RemoveStreamForwardRequest{
|
||||
Config: &streamd_grpc.StreamForward{
|
||||
StreamIDSrc: string(streamIDSrc),
|
||||
StreamIDDst: string(streamIDDst),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to request to remove the stream forward: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ func (d *StreamD) onConfigUpdateViaGIT(ctx context.Context, cfg *config.Config)
|
||||
}
|
||||
|
||||
func (d *StreamD) initGitIfNeeded(ctx context.Context) {
|
||||
logger.Debugf(ctx, "initGitIfNeeded")
|
||||
logger.Debugf(ctx, "initGitIfNeeded: %#+v", d.Config.GitRepo)
|
||||
if d.Config.GitRepo.Enable != nil && !*d.Config.GitRepo.Enable {
|
||||
logger.Debugf(ctx, "git is disabled in the configuration")
|
||||
return
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -44,8 +44,19 @@ type StreamDClient interface {
|
||||
OBSOLETE_GitInfo(ctx context.Context, in *OBSOLETE_GetGitInfoRequest, opts ...grpc.CallOption) (*OBSOLETE_GetGitInfoReply, error)
|
||||
OBSOLETE_GitRelogin(ctx context.Context, in *OBSOLETE_GitReloginRequest, opts ...grpc.CallOption) (*OBSOLETE_GitReloginReply, error)
|
||||
SubscribeToOAuthRequests(ctx context.Context, in *SubscribeToOAuthRequestsRequest, opts ...grpc.CallOption) (StreamD_SubscribeToOAuthRequestsClient, error)
|
||||
SubmitOAuthCode(ctx context.Context, in *SubmitOAuthCodeRequest, opts ...grpc.CallOption) (*SubmitOAuthCodeReply, error)
|
||||
OBSGetSceneList(ctx context.Context, in *OBSGetSceneListRequest, opts ...grpc.CallOption) (*OBSGetSceneListReply, error)
|
||||
OBSSetCurrentProgramScene(ctx context.Context, in *OBSSetCurrentProgramSceneRequest, opts ...grpc.CallOption) (*OBSSetCurrentProgramSceneReply, error)
|
||||
ListStreamServers(ctx context.Context, in *ListStreamServersRequest, opts ...grpc.CallOption) (*ListStreamServersReply, error)
|
||||
StartStreamServer(ctx context.Context, in *StartStreamServerRequest, opts ...grpc.CallOption) (*StartStreamServerReply, error)
|
||||
StopStreamServer(ctx context.Context, in *StopStreamServerRequest, opts ...grpc.CallOption) (*StopStreamServerReply, error)
|
||||
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)
|
||||
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)
|
||||
RemoveStreamForward(ctx context.Context, in *RemoveStreamForwardRequest, opts ...grpc.CallOption) (*RemoveStreamForwardReply, error)
|
||||
}
|
||||
|
||||
type streamDClient struct {
|
||||
@@ -277,6 +288,15 @@ func (x *streamDSubscribeToOAuthRequestsClient) Recv() (*OAuthRequest, error) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (c *streamDClient) SubmitOAuthCode(ctx context.Context, in *SubmitOAuthCodeRequest, opts ...grpc.CallOption) (*SubmitOAuthCodeReply, error) {
|
||||
out := new(SubmitOAuthCodeReply)
|
||||
err := c.cc.Invoke(ctx, "/StreamD/SubmitOAuthCode", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *streamDClient) OBSGetSceneList(ctx context.Context, in *OBSGetSceneListRequest, opts ...grpc.CallOption) (*OBSGetSceneListReply, error) {
|
||||
out := new(OBSGetSceneListReply)
|
||||
err := c.cc.Invoke(ctx, "/StreamD/OBSGetSceneList", in, out, opts...)
|
||||
@@ -295,6 +315,96 @@ func (c *streamDClient) OBSSetCurrentProgramScene(ctx context.Context, in *OBSSe
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *streamDClient) ListStreamServers(ctx context.Context, in *ListStreamServersRequest, opts ...grpc.CallOption) (*ListStreamServersReply, error) {
|
||||
out := new(ListStreamServersReply)
|
||||
err := c.cc.Invoke(ctx, "/StreamD/ListStreamServers", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *streamDClient) StartStreamServer(ctx context.Context, in *StartStreamServerRequest, opts ...grpc.CallOption) (*StartStreamServerReply, error) {
|
||||
out := new(StartStreamServerReply)
|
||||
err := c.cc.Invoke(ctx, "/StreamD/StartStreamServer", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *streamDClient) StopStreamServer(ctx context.Context, in *StopStreamServerRequest, opts ...grpc.CallOption) (*StopStreamServerReply, error) {
|
||||
out := new(StopStreamServerReply)
|
||||
err := c.cc.Invoke(ctx, "/StreamD/StopStreamServer", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *streamDClient) ListStreamDestinations(ctx context.Context, in *ListStreamDestinationsRequest, opts ...grpc.CallOption) (*ListStreamDestinationsReply, error) {
|
||||
out := new(ListStreamDestinationsReply)
|
||||
err := c.cc.Invoke(ctx, "/StreamD/ListStreamDestinations", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *streamDClient) AddStreamDestination(ctx context.Context, in *AddStreamDestinationRequest, opts ...grpc.CallOption) (*AddStreamDestinationReply, error) {
|
||||
out := new(AddStreamDestinationReply)
|
||||
err := c.cc.Invoke(ctx, "/StreamD/AddStreamDestination", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *streamDClient) RemoveStreamDestination(ctx context.Context, in *RemoveStreamDestinationRequest, opts ...grpc.CallOption) (*RemoveStreamDestinationReply, error) {
|
||||
out := new(RemoveStreamDestinationReply)
|
||||
err := c.cc.Invoke(ctx, "/StreamD/RemoveStreamDestination", 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...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *streamDClient) ListStreamForwards(ctx context.Context, in *ListStreamForwardsRequest, opts ...grpc.CallOption) (*ListStreamForwardsReply, error) {
|
||||
out := new(ListStreamForwardsReply)
|
||||
err := c.cc.Invoke(ctx, "/StreamD/ListStreamForwards", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *streamDClient) AddStreamForward(ctx context.Context, in *AddStreamForwardRequest, opts ...grpc.CallOption) (*AddStreamForwardReply, error) {
|
||||
out := new(AddStreamForwardReply)
|
||||
err := c.cc.Invoke(ctx, "/StreamD/AddStreamForward", 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...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// StreamDServer is the server API for StreamD service.
|
||||
// All implementations must embed UnimplementedStreamDServer
|
||||
// for forward compatibility
|
||||
@@ -321,8 +431,19 @@ type StreamDServer interface {
|
||||
OBSOLETE_GitInfo(context.Context, *OBSOLETE_GetGitInfoRequest) (*OBSOLETE_GetGitInfoReply, error)
|
||||
OBSOLETE_GitRelogin(context.Context, *OBSOLETE_GitReloginRequest) (*OBSOLETE_GitReloginReply, error)
|
||||
SubscribeToOAuthRequests(*SubscribeToOAuthRequestsRequest, StreamD_SubscribeToOAuthRequestsServer) error
|
||||
SubmitOAuthCode(context.Context, *SubmitOAuthCodeRequest) (*SubmitOAuthCodeReply, error)
|
||||
OBSGetSceneList(context.Context, *OBSGetSceneListRequest) (*OBSGetSceneListReply, error)
|
||||
OBSSetCurrentProgramScene(context.Context, *OBSSetCurrentProgramSceneRequest) (*OBSSetCurrentProgramSceneReply, error)
|
||||
ListStreamServers(context.Context, *ListStreamServersRequest) (*ListStreamServersReply, error)
|
||||
StartStreamServer(context.Context, *StartStreamServerRequest) (*StartStreamServerReply, error)
|
||||
StopStreamServer(context.Context, *StopStreamServerRequest) (*StopStreamServerReply, error)
|
||||
ListStreamDestinations(context.Context, *ListStreamDestinationsRequest) (*ListStreamDestinationsReply, error)
|
||||
AddStreamDestination(context.Context, *AddStreamDestinationRequest) (*AddStreamDestinationReply, error)
|
||||
RemoveStreamDestination(context.Context, *RemoveStreamDestinationRequest) (*RemoveStreamDestinationReply, error)
|
||||
ListIncomingStreams(context.Context, *ListIncomingStreamsRequest) (*ListIncomingStreamsReply, error)
|
||||
ListStreamForwards(context.Context, *ListStreamForwardsRequest) (*ListStreamForwardsReply, error)
|
||||
AddStreamForward(context.Context, *AddStreamForwardRequest) (*AddStreamForwardReply, error)
|
||||
RemoveStreamForward(context.Context, *RemoveStreamForwardRequest) (*RemoveStreamForwardReply, error)
|
||||
mustEmbedUnimplementedStreamDServer()
|
||||
}
|
||||
|
||||
@@ -396,12 +517,45 @@ func (UnimplementedStreamDServer) OBSOLETE_GitRelogin(context.Context, *OBSOLETE
|
||||
func (UnimplementedStreamDServer) SubscribeToOAuthRequests(*SubscribeToOAuthRequestsRequest, StreamD_SubscribeToOAuthRequestsServer) error {
|
||||
return status.Errorf(codes.Unimplemented, "method SubscribeToOAuthRequests not implemented")
|
||||
}
|
||||
func (UnimplementedStreamDServer) SubmitOAuthCode(context.Context, *SubmitOAuthCodeRequest) (*SubmitOAuthCodeReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SubmitOAuthCode not implemented")
|
||||
}
|
||||
func (UnimplementedStreamDServer) OBSGetSceneList(context.Context, *OBSGetSceneListRequest) (*OBSGetSceneListReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method OBSGetSceneList not implemented")
|
||||
}
|
||||
func (UnimplementedStreamDServer) OBSSetCurrentProgramScene(context.Context, *OBSSetCurrentProgramSceneRequest) (*OBSSetCurrentProgramSceneReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method OBSSetCurrentProgramScene not implemented")
|
||||
}
|
||||
func (UnimplementedStreamDServer) ListStreamServers(context.Context, *ListStreamServersRequest) (*ListStreamServersReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListStreamServers not implemented")
|
||||
}
|
||||
func (UnimplementedStreamDServer) StartStreamServer(context.Context, *StartStreamServerRequest) (*StartStreamServerReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method StartStreamServer not implemented")
|
||||
}
|
||||
func (UnimplementedStreamDServer) StopStreamServer(context.Context, *StopStreamServerRequest) (*StopStreamServerReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method StopStreamServer not implemented")
|
||||
}
|
||||
func (UnimplementedStreamDServer) ListStreamDestinations(context.Context, *ListStreamDestinationsRequest) (*ListStreamDestinationsReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListStreamDestinations not implemented")
|
||||
}
|
||||
func (UnimplementedStreamDServer) AddStreamDestination(context.Context, *AddStreamDestinationRequest) (*AddStreamDestinationReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AddStreamDestination not implemented")
|
||||
}
|
||||
func (UnimplementedStreamDServer) RemoveStreamDestination(context.Context, *RemoveStreamDestinationRequest) (*RemoveStreamDestinationReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RemoveStreamDestination not implemented")
|
||||
}
|
||||
func (UnimplementedStreamDServer) ListIncomingStreams(context.Context, *ListIncomingStreamsRequest) (*ListIncomingStreamsReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListIncomingStreams not implemented")
|
||||
}
|
||||
func (UnimplementedStreamDServer) ListStreamForwards(context.Context, *ListStreamForwardsRequest) (*ListStreamForwardsReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListStreamForwards not implemented")
|
||||
}
|
||||
func (UnimplementedStreamDServer) AddStreamForward(context.Context, *AddStreamForwardRequest) (*AddStreamForwardReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AddStreamForward not implemented")
|
||||
}
|
||||
func (UnimplementedStreamDServer) RemoveStreamForward(context.Context, *RemoveStreamForwardRequest) (*RemoveStreamForwardReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RemoveStreamForward not implemented")
|
||||
}
|
||||
func (UnimplementedStreamDServer) mustEmbedUnimplementedStreamDServer() {}
|
||||
|
||||
// UnsafeStreamDServer may be embedded to opt out of forward compatibility for this service.
|
||||
@@ -814,6 +968,24 @@ func (x *streamDSubscribeToOAuthRequestsServer) Send(m *OAuthRequest) error {
|
||||
return x.ServerStream.SendMsg(m)
|
||||
}
|
||||
|
||||
func _StreamD_SubmitOAuthCode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SubmitOAuthCodeRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(StreamDServer).SubmitOAuthCode(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/StreamD/SubmitOAuthCode",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(StreamDServer).SubmitOAuthCode(ctx, req.(*SubmitOAuthCodeRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StreamD_OBSGetSceneList_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(OBSGetSceneListRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -850,6 +1022,186 @@ func _StreamD_OBSSetCurrentProgramScene_Handler(srv interface{}, ctx context.Con
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StreamD_ListStreamServers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListStreamServersRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(StreamDServer).ListStreamServers(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/StreamD/ListStreamServers",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(StreamDServer).ListStreamServers(ctx, req.(*ListStreamServersRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StreamD_StartStreamServer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(StartStreamServerRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(StreamDServer).StartStreamServer(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/StreamD/StartStreamServer",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(StreamDServer).StartStreamServer(ctx, req.(*StartStreamServerRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StreamD_StopStreamServer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(StopStreamServerRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(StreamDServer).StopStreamServer(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/StreamD/StopStreamServer",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(StreamDServer).StopStreamServer(ctx, req.(*StopStreamServerRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StreamD_ListStreamDestinations_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListStreamDestinationsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(StreamDServer).ListStreamDestinations(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/StreamD/ListStreamDestinations",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(StreamDServer).ListStreamDestinations(ctx, req.(*ListStreamDestinationsRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StreamD_AddStreamDestination_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AddStreamDestinationRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(StreamDServer).AddStreamDestination(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/StreamD/AddStreamDestination",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(StreamDServer).AddStreamDestination(ctx, req.(*AddStreamDestinationRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StreamD_RemoveStreamDestination_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RemoveStreamDestinationRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(StreamDServer).RemoveStreamDestination(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/StreamD/RemoveStreamDestination",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(StreamDServer).RemoveStreamDestination(ctx, req.(*RemoveStreamDestinationRequest))
|
||||
}
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(StreamDServer).ListIncomingStreams(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/StreamD/ListIncomingStreams",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(StreamDServer).ListIncomingStreams(ctx, req.(*ListIncomingStreamsRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StreamD_ListStreamForwards_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListStreamForwardsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(StreamDServer).ListStreamForwards(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/StreamD/ListStreamForwards",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(StreamDServer).ListStreamForwards(ctx, req.(*ListStreamForwardsRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StreamD_AddStreamForward_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AddStreamForwardRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(StreamDServer).AddStreamForward(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/StreamD/AddStreamForward",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(StreamDServer).AddStreamForward(ctx, req.(*AddStreamForwardRequest))
|
||||
}
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(StreamDServer).RemoveStreamForward(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/StreamD/RemoveStreamForward",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(StreamDServer).RemoveStreamForward(ctx, req.(*RemoveStreamForwardRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// StreamD_ServiceDesc is the grpc.ServiceDesc for StreamD service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@@ -941,6 +1293,10 @@ var StreamD_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "OBSOLETE_GitRelogin",
|
||||
Handler: _StreamD_OBSOLETE_GitRelogin_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "SubmitOAuthCode",
|
||||
Handler: _StreamD_SubmitOAuthCode_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "OBSGetSceneList",
|
||||
Handler: _StreamD_OBSGetSceneList_Handler,
|
||||
@@ -949,6 +1305,46 @@ var StreamD_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "OBSSetCurrentProgramScene",
|
||||
Handler: _StreamD_OBSSetCurrentProgramScene_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListStreamServers",
|
||||
Handler: _StreamD_ListStreamServers_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "StartStreamServer",
|
||||
Handler: _StreamD_StartStreamServer_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "StopStreamServer",
|
||||
Handler: _StreamD_StopStreamServer_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListStreamDestinations",
|
||||
Handler: _StreamD_ListStreamDestinations_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AddStreamDestination",
|
||||
Handler: _StreamD_AddStreamDestination_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "RemoveStreamDestination",
|
||||
Handler: _StreamD_RemoveStreamDestination_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListIncomingStreams",
|
||||
Handler: _StreamD_ListIncomingStreams_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListStreamForwards",
|
||||
Handler: _StreamD_ListStreamForwards_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AddStreamForward",
|
||||
Handler: _StreamD_AddStreamForward_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "RemoveStreamForward",
|
||||
Handler: _StreamD_RemoveStreamForward_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
|
||||
@@ -27,9 +27,21 @@ service StreamD {
|
||||
rpc OBSOLETE_GitRelogin(OBSOLETE_GitReloginRequest) returns (OBSOLETE_GitReloginReply) {}
|
||||
|
||||
rpc SubscribeToOAuthRequests(SubscribeToOAuthRequestsRequest) returns (stream OAuthRequest) {}
|
||||
rpc SubmitOAuthCode(SubmitOAuthCodeRequest) returns (SubmitOAuthCodeReply) {}
|
||||
|
||||
rpc OBSGetSceneList(OBSGetSceneListRequest) returns (OBSGetSceneListReply) {}
|
||||
rpc OBSSetCurrentProgramScene(OBSSetCurrentProgramSceneRequest) returns (OBSSetCurrentProgramSceneReply) {}
|
||||
|
||||
rpc ListStreamServers(ListStreamServersRequest) returns (ListStreamServersReply) {}
|
||||
rpc StartStreamServer(StartStreamServerRequest) returns (StartStreamServerReply) {}
|
||||
rpc StopStreamServer(StopStreamServerRequest) returns (StopStreamServerReply) {}
|
||||
rpc ListStreamDestinations(ListStreamDestinationsRequest) returns (ListStreamDestinationsReply) {}
|
||||
rpc AddStreamDestination(AddStreamDestinationRequest) returns (AddStreamDestinationReply) {}
|
||||
rpc RemoveStreamDestination(RemoveStreamDestinationRequest) returns (RemoveStreamDestinationReply) {}
|
||||
rpc ListIncomingStreams(ListIncomingStreamsRequest) returns (ListIncomingStreamsReply) {}
|
||||
rpc ListStreamForwards(ListStreamForwardsRequest) returns (ListStreamForwardsReply) {}
|
||||
rpc AddStreamForward(AddStreamForwardRequest) returns (AddStreamForwardReply) {}
|
||||
rpc RemoveStreamForward(RemoveStreamForwardRequest) returns (RemoveStreamForwardReply) {}
|
||||
}
|
||||
|
||||
message GetConfigRequest {}
|
||||
@@ -113,9 +125,12 @@ message OBSOLETE_GitReloginRequest {}
|
||||
message OBSOLETE_GitReloginReply {}
|
||||
|
||||
|
||||
message SubscribeToOAuthRequestsRequest{}
|
||||
message SubscribeToOAuthRequestsRequest{
|
||||
int32 listenPort = 1;
|
||||
}
|
||||
message OAuthRequest{
|
||||
string authURL = 1;
|
||||
string platID = 1;
|
||||
string authURL = 2;
|
||||
}
|
||||
|
||||
message GetVariableRequest {
|
||||
@@ -164,3 +179,85 @@ message OBSSetCurrentProgramSceneRequest {
|
||||
}
|
||||
}
|
||||
message OBSSetCurrentProgramSceneReply {}
|
||||
|
||||
message SubmitOAuthCodeRequest {
|
||||
string platID = 1;
|
||||
string code = 2;
|
||||
}
|
||||
message SubmitOAuthCodeReply {}
|
||||
|
||||
enum StreamServerType {
|
||||
Undefined = 0;
|
||||
RTSP = 1;
|
||||
RTMP = 2;
|
||||
}
|
||||
|
||||
message StreamServer {
|
||||
StreamServerType serverType = 1;
|
||||
string listenAddr = 2;
|
||||
}
|
||||
|
||||
message ListStreamServersRequest {}
|
||||
message ListStreamServersReply {
|
||||
repeated StreamServer streamServers = 1;
|
||||
}
|
||||
|
||||
message StartStreamServerRequest {
|
||||
StreamServer config = 1;
|
||||
}
|
||||
message StartStreamServerReply {
|
||||
}
|
||||
|
||||
message StopStreamServerRequest {
|
||||
string listenAddr = 1;
|
||||
}
|
||||
message StopStreamServerReply {}
|
||||
|
||||
message StreamDestination {
|
||||
string streamID = 1;
|
||||
string url = 2;
|
||||
}
|
||||
|
||||
message ListStreamDestinationsRequest {}
|
||||
message ListStreamDestinationsReply {
|
||||
repeated StreamDestination streamDestinations = 1;
|
||||
}
|
||||
|
||||
message AddStreamDestinationRequest {
|
||||
StreamDestination config = 1;
|
||||
}
|
||||
message AddStreamDestinationReply {}
|
||||
|
||||
message RemoveStreamDestinationRequest {
|
||||
string streamID = 1;
|
||||
}
|
||||
message RemoveStreamDestinationReply {}
|
||||
|
||||
message IncomingStream {
|
||||
string streamID = 1;
|
||||
}
|
||||
|
||||
message ListIncomingStreamsRequest {}
|
||||
message ListIncomingStreamsReply {
|
||||
repeated IncomingStream incomingStreams = 1;
|
||||
}
|
||||
|
||||
message StreamForward {
|
||||
string streamIDSrc = 1;
|
||||
string streamIDDst = 2;
|
||||
}
|
||||
|
||||
message ListStreamForwardsRequest {}
|
||||
message ListStreamForwardsReply {
|
||||
repeated StreamForward streamForwards = 1;
|
||||
}
|
||||
|
||||
message AddStreamForwardRequest {
|
||||
StreamForward config = 1;
|
||||
}
|
||||
message AddStreamForwardReply {}
|
||||
|
||||
message RemoveStreamForwardRequest {
|
||||
StreamForward config = 1;
|
||||
}
|
||||
message RemoveStreamForwardReply {}
|
||||
|
||||
@@ -3,17 +3,17 @@ package server
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"crypto"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/andreykaipov/goobs/api/requests/scenes"
|
||||
"github.com/dustin/go-broadcast"
|
||||
"github.com/facebookincubator/go-belt/tool/experimental/errmon"
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/google/uuid"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/immune-gmbh/attestation-sdk/pkg/lockmap"
|
||||
"github.com/immune-gmbh/attestation-sdk/pkg/objhash"
|
||||
@@ -21,20 +21,31 @@ import (
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/obs"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/twitch"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/youtube"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/api"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/api/grpcconv"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/config"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/grpc/go/streamd_grpc"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streampanel/consts"
|
||||
)
|
||||
|
||||
type GRPCServer struct {
|
||||
streamd_grpc.StreamDServer
|
||||
streamd_grpc.UnimplementedStreamDServer
|
||||
StreamD api.StreamD
|
||||
Cache map[objhash.ObjHash]any
|
||||
CacheMetaLock sync.Mutex
|
||||
CacheLockMap *lockmap.LockMap
|
||||
OAuthURLBroadcaster broadcast.Broadcaster
|
||||
OAuthURLHandlerLocker sync.Mutex
|
||||
OAuthURLHandlers map[uint16]map[uuid.UUID]*OAuthURLHandler
|
||||
YouTubeStreamStartedAt time.Time
|
||||
|
||||
UnansweredOAuthRequestsLocker sync.Mutex
|
||||
UnansweredOAuthRequests map[streamcontrol.PlatformName]map[uint16]*streamd_grpc.OAuthRequest
|
||||
}
|
||||
|
||||
type OAuthURLHandler struct {
|
||||
Sender streamd_grpc.StreamD_SubscribeToOAuthRequestsServer
|
||||
CancelFn context.CancelFunc
|
||||
}
|
||||
|
||||
var _ streamd_grpc.StreamDServer = (*GRPCServer)(nil)
|
||||
@@ -45,7 +56,8 @@ func NewGRPCServer(streamd api.StreamD) *GRPCServer {
|
||||
Cache: map[objhash.ObjHash]any{},
|
||||
CacheLockMap: lockmap.NewLockMap(),
|
||||
|
||||
OAuthURLBroadcaster: broadcast.NewBroadcaster(10),
|
||||
OAuthURLHandlers: map[uint16]map[uuid.UUID]*OAuthURLHandler{},
|
||||
UnansweredOAuthRequests: map[streamcontrol.PlatformName]map[uint16]*streamd_grpc.OAuthRequest{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,13 +112,13 @@ func memoize[REQ any, REPLY any, T func(context.Context, *REQ) (*REPLY, error)](
|
||||
|
||||
cachedResult, ok := cache[key]
|
||||
|
||||
grpc.CacheMetaLock.Unlock()
|
||||
logger.Tracef(ctx, "grpc.CacheMetaLock.Unlock()-ed")
|
||||
if ok {
|
||||
if v, ok := cachedResult.(cacheItem); ok {
|
||||
cutoffTS := time.Now().Add(-cacheDuration)
|
||||
if cacheDuration > 0 && !v.SavedAt.Before(cutoffTS) {
|
||||
h.UserData = v
|
||||
grpc.CacheMetaLock.Unlock()
|
||||
logger.Tracef(ctx, "grpc.CacheMetaLock.Unlock()-ed")
|
||||
h.UserData = nil
|
||||
h.Unlock()
|
||||
logger.Tracef(ctx, "grpc.CacheLockMap.Unlock(%X)-ed", key[:])
|
||||
logger.Debugf(ctx, "using the cached value")
|
||||
@@ -118,6 +130,8 @@ func memoize[REQ any, REPLY any, T func(context.Context, *REQ) (*REPLY, error)](
|
||||
logger.Errorf(ctx, "cache-failure: expected type %T, but got %T", (*cacheItem)(nil), cachedResult)
|
||||
}
|
||||
}
|
||||
grpc.CacheMetaLock.Unlock()
|
||||
logger.Tracef(ctx, "grpc.CacheMetaLock.Unlock()-ed")
|
||||
|
||||
var ts time.Time
|
||||
defer func() {
|
||||
@@ -144,7 +158,12 @@ func memoize[REQ any, REPLY any, T func(context.Context, *REQ) (*REPLY, error)](
|
||||
|
||||
func (grpc *GRPCServer) Close() error {
|
||||
err := &multierror.Error{}
|
||||
err = multierror.Append(err, grpc.OAuthURLBroadcaster.Close())
|
||||
grpc.OAuthURLHandlerLocker.Lock()
|
||||
grpc.OAuthURLHandlerLocker.Unlock()
|
||||
for listenPort, sender := range grpc.OAuthURLHandlers {
|
||||
_ = sender // TODO: invent sender.Close()
|
||||
delete(grpc.OAuthURLHandlers, listenPort)
|
||||
}
|
||||
return err.ErrorOrNil()
|
||||
}
|
||||
|
||||
@@ -450,32 +469,116 @@ func (grpc *GRPCServer) getStreamStatus(
|
||||
func (grpc *GRPCServer) SubscribeToOAuthRequests(
|
||||
req *streamd_grpc.SubscribeToOAuthRequestsRequest,
|
||||
sender streamd_grpc.StreamD_SubscribeToOAuthRequestsServer,
|
||||
) error {
|
||||
logger.Tracef(sender.Context(), "SubscribeToOAuthRequests()")
|
||||
defer logger.Tracef(sender.Context(), "/SubscribeToOAuthRequests()")
|
||||
|
||||
oauthURLChan := make(chan any)
|
||||
grpc.OAuthURLBroadcaster.Register(oauthURLChan)
|
||||
defer grpc.OAuthURLBroadcaster.Unregister(oauthURLChan)
|
||||
|
||||
for {
|
||||
select {
|
||||
case _oauthURL := <-oauthURLChan:
|
||||
oauthURL := _oauthURL.(string)
|
||||
err := sender.Send(&streamd_grpc.OAuthRequest{
|
||||
AuthURL: oauthURL,
|
||||
})
|
||||
) (_ret error) {
|
||||
ctx, cancelFn := context.WithCancel(sender.Context())
|
||||
_uuid, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to send the OAuth URL: %w", err)
|
||||
logger.Errorf(ctx, "unable to generate an UUID: %v", err)
|
||||
}
|
||||
case <-sender.Context().Done():
|
||||
return sender.Context().Err()
|
||||
|
||||
logger.Tracef(sender.Context(), "SubscribeToOAuthRequests(): UUID:%s", _uuid)
|
||||
defer func() { logger.Tracef(sender.Context(), "/SubscribeToOAuthRequests(): UUID:%s: %v", _uuid, _ret) }()
|
||||
|
||||
listenPort := uint16(req.ListenPort)
|
||||
streamD, _ := grpc.StreamD.(*streamd.StreamD)
|
||||
|
||||
grpc.OAuthURLHandlerLocker.Lock()
|
||||
logger.Tracef(sender.Context(), "grpc.OAuthURLHandlerLocker.Lock()-ed")
|
||||
m := grpc.OAuthURLHandlers[listenPort]
|
||||
if m == nil {
|
||||
m = map[uuid.UUID]*OAuthURLHandler{}
|
||||
}
|
||||
m[_uuid] = &OAuthURLHandler{
|
||||
Sender: sender,
|
||||
CancelFn: cancelFn,
|
||||
}
|
||||
grpc.OAuthURLHandlers[listenPort] = m // unnecessary, but feels safer
|
||||
grpc.OAuthURLHandlerLocker.Unlock()
|
||||
logger.Tracef(sender.Context(), "grpc.OAuthURLHandlerLocker.Unlock()-ed")
|
||||
|
||||
var unansweredRequests []*streamd_grpc.OAuthRequest
|
||||
grpc.UnansweredOAuthRequestsLocker.Lock()
|
||||
for _, m := range grpc.UnansweredOAuthRequests {
|
||||
req := m[listenPort]
|
||||
if req == nil {
|
||||
continue
|
||||
}
|
||||
unansweredRequests = append(unansweredRequests, req)
|
||||
}
|
||||
grpc.UnansweredOAuthRequestsLocker.Unlock()
|
||||
|
||||
for _, req := range unansweredRequests {
|
||||
logger.Tracef(ctx, "re-sending an unanswered request to a new client: %#+v", *req)
|
||||
err := sender.Send(req)
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
}
|
||||
|
||||
if streamD != nil {
|
||||
streamD.AddOAuthListenPort(listenPort)
|
||||
}
|
||||
|
||||
logger.Tracef(ctx, "waiting for the subscription to be cancelled")
|
||||
<-ctx.Done()
|
||||
grpc.OAuthURLHandlerLocker.Lock()
|
||||
defer grpc.OAuthURLHandlerLocker.Unlock()
|
||||
delete(grpc.OAuthURLHandlers[listenPort], _uuid)
|
||||
if len(grpc.OAuthURLHandlers[listenPort]) == 0 {
|
||||
delete(grpc.OAuthURLHandlers, listenPort)
|
||||
if streamD != nil {
|
||||
streamD.RemoveOAuthListenPort(listenPort)
|
||||
}
|
||||
}
|
||||
|
||||
func (grpc *GRPCServer) OpenOAuthURL(authURL string) {
|
||||
grpc.OAuthURLBroadcaster.Submit(authURL)
|
||||
return nil
|
||||
}
|
||||
|
||||
type ErrNoOAuthHandlerForPort struct {
|
||||
Port uint16
|
||||
}
|
||||
|
||||
func (err ErrNoOAuthHandlerForPort) Error() string {
|
||||
return fmt.Sprintf("no handler for port %d", err.Port)
|
||||
}
|
||||
|
||||
func (grpc *GRPCServer) OpenOAuthURL(
|
||||
ctx context.Context,
|
||||
listenPort uint16,
|
||||
platID streamcontrol.PlatformName,
|
||||
authURL string,
|
||||
) (_ret error) {
|
||||
logger.Tracef(ctx, "OpenOAuthURL()")
|
||||
defer func() { logger.Tracef(ctx, "/OpenOAuthURL(): %v", _ret) }()
|
||||
|
||||
grpc.OAuthURLHandlerLocker.Lock()
|
||||
logger.Tracef(ctx, "grpc.OAuthURLHandlerLocker.Lock()-ed")
|
||||
defer logger.Tracef(ctx, "grpc.OAuthURLHandlerLocker.Unlock()-ed")
|
||||
defer grpc.OAuthURLHandlerLocker.Unlock()
|
||||
|
||||
handlers := grpc.OAuthURLHandlers[listenPort]
|
||||
if handlers == nil {
|
||||
return ErrNoOAuthHandlerForPort{
|
||||
Port: listenPort,
|
||||
}
|
||||
}
|
||||
req := streamd_grpc.OAuthRequest{
|
||||
PlatID: string(platID),
|
||||
AuthURL: authURL,
|
||||
}
|
||||
grpc.UnansweredOAuthRequestsLocker.Lock()
|
||||
if grpc.UnansweredOAuthRequests[platID] == nil {
|
||||
grpc.UnansweredOAuthRequests[platID] = map[uint16]*streamd_grpc.OAuthRequest{}
|
||||
}
|
||||
grpc.UnansweredOAuthRequests[platID][listenPort] = &req
|
||||
grpc.UnansweredOAuthRequestsLocker.Unlock()
|
||||
logger.Tracef(ctx, "OpenOAuthURL() sending %#+v", req)
|
||||
var resultErr *multierror.Error
|
||||
for _, handler := range handlers {
|
||||
err := handler.Sender.Send(&req)
|
||||
if err != nil {
|
||||
err = multierror.Append(resultErr, fmt.Errorf("unable to send oauth request: %w", err))
|
||||
}
|
||||
}
|
||||
return resultErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
func (grpc *GRPCServer) GetVariable(
|
||||
@@ -498,26 +601,25 @@ func (grpc *GRPCServer) GetVariableHash(
|
||||
ctx context.Context,
|
||||
req *streamd_grpc.GetVariableHashRequest,
|
||||
) (*streamd_grpc.GetVariableHashReply, error) {
|
||||
var hashType crypto.Hash
|
||||
hashTypeIn := req.GetHashType()
|
||||
switch hashTypeIn {
|
||||
case streamd_grpc.HashType_HASH_SHA1:
|
||||
hashType = crypto.SHA1
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected hash type: %v", hashTypeIn)
|
||||
}
|
||||
|
||||
key := consts.VarKey(req.GetKey())
|
||||
b, err := grpc.StreamD.GetVariable(ctx, key)
|
||||
b, err := grpc.StreamD.GetVariableHash(ctx, key, hashType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get variable '%s': %w", key, err)
|
||||
}
|
||||
|
||||
var hash []byte
|
||||
hashType := req.GetHashType()
|
||||
switch hashType {
|
||||
case streamd_grpc.HashType_HASH_SHA1:
|
||||
_hash := sha1.Sum(b)
|
||||
hash = _hash[:]
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected hash type: %v", hashType)
|
||||
}
|
||||
|
||||
return &streamd_grpc.GetVariableHashReply{
|
||||
Key: string(key),
|
||||
HashType: hashType,
|
||||
Hash: hash,
|
||||
HashType: hashTypeIn,
|
||||
Hash: b,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -579,3 +681,203 @@ func (grpc *GRPCServer) OBSSetCurrentProgramScene(
|
||||
}
|
||||
return &streamd_grpc.OBSSetCurrentProgramSceneReply{}, nil
|
||||
}
|
||||
|
||||
func (grpc *GRPCServer) SubmitOAuthCode(
|
||||
ctx context.Context,
|
||||
req *streamd_grpc.SubmitOAuthCodeRequest,
|
||||
) (*streamd_grpc.SubmitOAuthCodeReply, error) {
|
||||
grpc.UnansweredOAuthRequestsLocker.Lock()
|
||||
delete(grpc.UnansweredOAuthRequests, streamcontrol.PlatformName(req.PlatID))
|
||||
grpc.UnansweredOAuthRequestsLocker.Unlock()
|
||||
|
||||
_, err := grpc.StreamD.SubmitOAuthCode(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &streamd_grpc.SubmitOAuthCodeReply{}, nil
|
||||
}
|
||||
|
||||
func (grpc *GRPCServer) ListStreamServers(
|
||||
ctx context.Context,
|
||||
_ *streamd_grpc.ListStreamServersRequest,
|
||||
) (*streamd_grpc.ListStreamServersReply, error) {
|
||||
servers, err := grpc.StreamD.ListStreamServers(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []*streamd_grpc.StreamServer
|
||||
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,
|
||||
})
|
||||
}
|
||||
return &streamd_grpc.ListStreamServersReply{
|
||||
StreamServers: result,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (grpc *GRPCServer) StartStreamServer(
|
||||
ctx context.Context,
|
||||
req *streamd_grpc.StartStreamServerRequest,
|
||||
) (*streamd_grpc.StartStreamServerReply, error) {
|
||||
t, err := grpcconv.StreamServerTypeGRPC2Go(req.GetConfig().GetServerType())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to convert the server type value: %w", err)
|
||||
}
|
||||
|
||||
err = grpc.StreamD.StartStreamServer(
|
||||
ctx,
|
||||
t,
|
||||
req.GetConfig().GetListenAddr(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &streamd_grpc.StartStreamServerReply{}, nil
|
||||
}
|
||||
|
||||
func (grpc *GRPCServer) StopStreamServer(
|
||||
ctx context.Context,
|
||||
req *streamd_grpc.StopStreamServerRequest,
|
||||
) (*streamd_grpc.StopStreamServerReply, error) {
|
||||
err := grpc.StreamD.StopStreamServer(
|
||||
ctx,
|
||||
req.GetListenAddr(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &streamd_grpc.StopStreamServerReply{}, nil
|
||||
}
|
||||
|
||||
func (grpc *GRPCServer) ListStreamDestinations(
|
||||
ctx context.Context,
|
||||
req *streamd_grpc.ListStreamDestinationsRequest,
|
||||
) (*streamd_grpc.ListStreamDestinationsReply, error) {
|
||||
dsts, err := grpc.StreamD.ListStreamDestinations(
|
||||
ctx,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []*streamd_grpc.StreamDestination
|
||||
for _, dst := range dsts {
|
||||
result = append(result, &streamd_grpc.StreamDestination{
|
||||
StreamID: string(dst.StreamID),
|
||||
Url: dst.URL,
|
||||
})
|
||||
}
|
||||
return &streamd_grpc.ListStreamDestinationsReply{
|
||||
StreamDestinations: result,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (grpc *GRPCServer) AddStreamDestination(
|
||||
ctx context.Context,
|
||||
req *streamd_grpc.AddStreamDestinationRequest,
|
||||
) (*streamd_grpc.AddStreamDestinationReply, error) {
|
||||
err := grpc.StreamD.AddStreamDestination(
|
||||
ctx,
|
||||
api.StreamID(req.GetConfig().GetStreamID()),
|
||||
req.GetConfig().GetUrl(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &streamd_grpc.AddStreamDestinationReply{}, nil
|
||||
}
|
||||
|
||||
func (grpc *GRPCServer) RemoveStreamDestination(
|
||||
ctx context.Context,
|
||||
req *streamd_grpc.RemoveStreamDestinationRequest,
|
||||
) (*streamd_grpc.RemoveStreamDestinationReply, error) {
|
||||
err := grpc.StreamD.RemoveStreamDestination(
|
||||
ctx,
|
||||
api.StreamID(req.GetStreamID()),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &streamd_grpc.RemoveStreamDestinationReply{}, nil
|
||||
}
|
||||
|
||||
func (grpc *GRPCServer) ListIncomingStreams(
|
||||
ctx context.Context,
|
||||
req *streamd_grpc.ListIncomingStreamsRequest,
|
||||
) (*streamd_grpc.ListIncomingStreamsReply, error) {
|
||||
inStreams, err := grpc.StreamD.ListIncomingStreams(
|
||||
ctx,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []*streamd_grpc.IncomingStream
|
||||
for _, s := range inStreams {
|
||||
result = append(result, &streamd_grpc.IncomingStream{
|
||||
StreamID: string(s.StreamID),
|
||||
})
|
||||
}
|
||||
return &streamd_grpc.ListIncomingStreamsReply{
|
||||
IncomingStreams: result,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (grpc *GRPCServer) ListStreamForwards(
|
||||
ctx context.Context,
|
||||
req *streamd_grpc.ListStreamForwardsRequest,
|
||||
) (*streamd_grpc.ListStreamForwardsReply, error) {
|
||||
streamFwds, err := grpc.StreamD.ListStreamForwards(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []*streamd_grpc.StreamForward
|
||||
for _, s := range streamFwds {
|
||||
result = append(result, &streamd_grpc.StreamForward{
|
||||
StreamIDSrc: string(s.StreamIDSrc),
|
||||
StreamIDDst: string(s.StreamIDDst),
|
||||
})
|
||||
}
|
||||
return &streamd_grpc.ListStreamForwardsReply{
|
||||
StreamForwards: result,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (grpc *GRPCServer) AddStreamForward(
|
||||
ctx context.Context,
|
||||
req *streamd_grpc.AddStreamForwardRequest,
|
||||
) (*streamd_grpc.AddStreamForwardReply, error) {
|
||||
err := grpc.StreamD.AddStreamForward(
|
||||
ctx,
|
||||
api.StreamID(req.GetConfig().GetStreamIDSrc()),
|
||||
api.StreamID(req.GetConfig().GetStreamIDDst()),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &streamd_grpc.AddStreamForwardReply{}, nil
|
||||
}
|
||||
|
||||
func (grpc *GRPCServer) RemoveStreamForward(
|
||||
ctx context.Context,
|
||||
req *streamd_grpc.RemoveStreamForwardRequest,
|
||||
) (*streamd_grpc.RemoveStreamForwardReply, error) {
|
||||
err := grpc.StreamD.RemoveStreamForward(
|
||||
ctx,
|
||||
api.StreamID(req.GetConfig().GetStreamIDSrc()),
|
||||
api.StreamID(req.GetConfig().GetStreamIDDst()),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &streamd_grpc.RemoveStreamForwardReply{}, nil
|
||||
}
|
||||
|
||||
@@ -108,6 +108,7 @@ func newTwitch(
|
||||
setUserData func(context.Context, *streamcontrol.PlatformConfig[twitch.PlatformSpecificConfig, twitch.StreamProfile]) (bool, error),
|
||||
saveCfgFunc func(*streamcontrol.AbstractPlatformConfig) error,
|
||||
customOAuthHandler twitch.OAuthHandler,
|
||||
getOAuthListenPorts func() []uint16,
|
||||
) (
|
||||
*twitch.Twitch,
|
||||
error,
|
||||
@@ -146,6 +147,7 @@ func newTwitch(
|
||||
|
||||
logger.Debugf(ctx, "twitch config: %#+v", platCfg)
|
||||
platCfg.Config.CustomOAuthHandler = customOAuthHandler
|
||||
platCfg.Config.GetOAuthListenPorts = getOAuthListenPorts
|
||||
cfg = streamcontrol.ToAbstractPlatformConfig(ctx, platCfg)
|
||||
twitch, err := twitch.New(ctx, *platCfg,
|
||||
func(c twitch.Config) error {
|
||||
@@ -175,6 +177,7 @@ func newYouTube(
|
||||
setUserData func(context.Context, *streamcontrol.PlatformConfig[youtube.PlatformSpecificConfig, youtube.StreamProfile]) (bool, error),
|
||||
saveCfgFunc func(*streamcontrol.AbstractPlatformConfig) error,
|
||||
customOAuthHandler youtube.OAuthHandler,
|
||||
getOAuthListenPorts func() []uint16,
|
||||
) (
|
||||
*youtube.YouTube,
|
||||
error,
|
||||
@@ -213,6 +216,7 @@ func newYouTube(
|
||||
|
||||
logger.Debugf(ctx, "youtube config: %#+v", platCfg)
|
||||
platCfg.Config.CustomOAuthHandler = customOAuthHandler
|
||||
platCfg.Config.GetOAuthListenPorts = getOAuthListenPorts
|
||||
cfg = streamcontrol.ToAbstractPlatformConfig(ctx, platCfg)
|
||||
yt, err := youtube.New(ctx, *platCfg,
|
||||
func(c youtube.Config) error {
|
||||
@@ -260,7 +264,9 @@ func (d *StreamD) initTwitchBackend(ctx context.Context) error {
|
||||
func(cfg *streamcontrol.AbstractPlatformConfig) error {
|
||||
return d.setPlatformConfig(ctx, twitch.ID, cfg)
|
||||
},
|
||||
d.UI.OAuthHandlerTwitch)
|
||||
d.UI.OAuthHandlerTwitch,
|
||||
d.GetOAuthListenPorts,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -277,6 +283,7 @@ func (d *StreamD) initYouTubeBackend(ctx context.Context) error {
|
||||
return d.setPlatformConfig(ctx, youtube.ID, cfg)
|
||||
},
|
||||
d.UI.OAuthHandlerYouTube,
|
||||
d.GetOAuthListenPorts,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to initialize the backend 'YouTube': %w", err)
|
||||
|
||||
@@ -19,8 +19,11 @@ import (
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/api"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/cache"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/config"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/grpc/go/streamd_grpc"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/ui"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streampanel/consts"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamserver"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamserver/types"
|
||||
"github.com/xaionaro-go/streamctl/pkg/xpath"
|
||||
)
|
||||
|
||||
@@ -51,6 +54,12 @@ type StreamD struct {
|
||||
StreamControllers StreamControllers
|
||||
|
||||
Variables sync.Map
|
||||
|
||||
OAuthListenPortsLocker sync.Mutex
|
||||
OAuthListenPorts map[uint16]struct{}
|
||||
|
||||
StreamServerLocker sync.Mutex
|
||||
StreamServer *streamserver.StreamServer
|
||||
}
|
||||
|
||||
var _ api.StreamD = (*StreamD)(nil)
|
||||
@@ -68,6 +77,8 @@ func New(
|
||||
SaveConfigFunc: saveCfgFunc,
|
||||
Config: config,
|
||||
Cache: &cache.Cache{},
|
||||
OAuthListenPorts: map[uint16]struct{}{},
|
||||
StreamServer: streamserver.New(),
|
||||
}
|
||||
|
||||
err := d.readCache(ctx)
|
||||
@@ -228,7 +239,7 @@ func (d *StreamD) initTwitchData(ctx context.Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
allCategories, err := twitch.GetAllCategories(ctx)
|
||||
allCategories, err := twitch.GetAllCategories(d.ctxForController(ctx))
|
||||
if err != nil {
|
||||
d.UI.DisplayError(err)
|
||||
return false
|
||||
@@ -269,7 +280,7 @@ func (d *StreamD) initYoutubeData(ctx context.Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
broadcasts, err := youtube.ListBroadcasts(ctx)
|
||||
broadcasts, err := youtube.ListBroadcasts(d.ctxForController(ctx))
|
||||
if err != nil {
|
||||
d.UI.DisplayError(err)
|
||||
return false
|
||||
@@ -360,7 +371,7 @@ func (d *StreamD) StartStream(
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to convert the profile into OBS profile: %w", err)
|
||||
}
|
||||
err = d.StreamControllers.OBS.StartStream(ctx, title, description, *profile, customArgs...)
|
||||
err = d.StreamControllers.OBS.StartStream(d.ctxForController(ctx), title, description, *profile, customArgs...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to start the stream on OBS: %w", err)
|
||||
}
|
||||
@@ -370,7 +381,7 @@ func (d *StreamD) StartStream(
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to convert the profile into Twitch profile: %w", err)
|
||||
}
|
||||
err = d.StreamControllers.Twitch.StartStream(ctx, title, description, *profile, customArgs...)
|
||||
err = d.StreamControllers.Twitch.StartStream(d.ctxForController(ctx), title, description, *profile, customArgs...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to start the stream on Twitch: %w", err)
|
||||
}
|
||||
@@ -380,7 +391,7 @@ func (d *StreamD) StartStream(
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to convert the profile into YouTube profile: %w", err)
|
||||
}
|
||||
err = d.StreamControllers.YouTube.StartStream(ctx, title, description, *profile, customArgs...)
|
||||
err = d.StreamControllers.YouTube.StartStream(d.ctxForController(ctx), title, description, *profile, customArgs...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to start the stream on YouTube: %w", err)
|
||||
}
|
||||
@@ -393,11 +404,11 @@ func (d *StreamD) StartStream(
|
||||
func (d *StreamD) EndStream(ctx context.Context, platID streamcontrol.PlatformName) error {
|
||||
switch platID {
|
||||
case obs.ID:
|
||||
return d.StreamControllers.OBS.EndStream(ctx)
|
||||
return d.StreamControllers.OBS.EndStream(d.ctxForController(ctx))
|
||||
case twitch.ID:
|
||||
return d.StreamControllers.Twitch.EndStream(ctx)
|
||||
return d.StreamControllers.Twitch.EndStream(d.ctxForController(ctx))
|
||||
case youtube.ID:
|
||||
return d.StreamControllers.YouTube.EndStream(ctx)
|
||||
return d.StreamControllers.YouTube.EndStream(d.ctxForController(ctx))
|
||||
default:
|
||||
return fmt.Errorf("unexpected platform ID '%s'", platID)
|
||||
}
|
||||
@@ -499,7 +510,7 @@ func (d *StreamD) GetStreamStatus(
|
||||
return nil, fmt.Errorf("controller '%s' is not initialized", platID)
|
||||
}
|
||||
|
||||
return c.GetStreamStatus(ctx)
|
||||
return c.GetStreamStatus(d.ctxForController(ctx))
|
||||
}
|
||||
|
||||
func (d *StreamD) SetTitle(
|
||||
@@ -512,7 +523,7 @@ func (d *StreamD) SetTitle(
|
||||
return err
|
||||
}
|
||||
|
||||
return c.SetTitle(ctx, title)
|
||||
return c.SetTitle(d.ctxForController(ctx), title)
|
||||
}
|
||||
|
||||
func (d *StreamD) SetDescription(
|
||||
@@ -525,7 +536,12 @@ func (d *StreamD) SetDescription(
|
||||
return err
|
||||
}
|
||||
|
||||
return c.SetDescription(ctx, description)
|
||||
return c.SetDescription(d.ctxForController(ctx), description)
|
||||
}
|
||||
|
||||
// TODO: delete this function (yes, it is not needed at all)
|
||||
func (d *StreamD) ctxForController(ctx context.Context) context.Context {
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (d *StreamD) ApplyProfile(
|
||||
@@ -534,12 +550,12 @@ func (d *StreamD) ApplyProfile(
|
||||
profile streamcontrol.AbstractStreamProfile,
|
||||
customArgs ...any,
|
||||
) error {
|
||||
c, err := d.streamController(ctx, platID)
|
||||
c, err := d.streamController(d.ctxForController(ctx), platID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.ApplyProfile(ctx, profile, customArgs...)
|
||||
return c.ApplyProfile(d.ctxForController(ctx), profile, customArgs...)
|
||||
}
|
||||
|
||||
func (d *StreamD) UpdateStream(
|
||||
@@ -549,17 +565,17 @@ func (d *StreamD) UpdateStream(
|
||||
profile streamcontrol.AbstractStreamProfile,
|
||||
customArgs ...any,
|
||||
) error {
|
||||
err := d.SetTitle(ctx, platID, title)
|
||||
err := d.SetTitle(d.ctxForController(ctx), platID, title)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to set the title: %w", err)
|
||||
}
|
||||
|
||||
err = d.SetDescription(ctx, platID, description)
|
||||
err = d.SetDescription(d.ctxForController(ctx), platID, description)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to set the description: %w", err)
|
||||
}
|
||||
|
||||
err = d.ApplyProfile(ctx, platID, profile, customArgs...)
|
||||
err = d.ApplyProfile(d.ctxForController(ctx), platID, profile, customArgs...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to apply the profile: %w", err)
|
||||
}
|
||||
@@ -617,7 +633,7 @@ func (d *StreamD) OBSGetSceneList(
|
||||
return nil, fmt.Errorf("OBS is not initialized")
|
||||
}
|
||||
|
||||
return obs.GetSceneList(ctx)
|
||||
return obs.GetSceneList(d.ctxForController(ctx))
|
||||
}
|
||||
|
||||
func (d *StreamD) OBSSetCurrentProgramScene(
|
||||
@@ -629,5 +645,224 @@ func (d *StreamD) OBSSetCurrentProgramScene(
|
||||
return fmt.Errorf("OBS is not initialized")
|
||||
}
|
||||
|
||||
return obs.SetCurrentProgramScene(ctx, req)
|
||||
return obs.SetCurrentProgramScene(d.ctxForController(ctx), req)
|
||||
}
|
||||
|
||||
func (d *StreamD) SubmitOAuthCode(
|
||||
ctx context.Context,
|
||||
req *streamd_grpc.SubmitOAuthCodeRequest,
|
||||
) (*streamd_grpc.SubmitOAuthCodeReply, error) {
|
||||
err := d.UI.OnSubmittedOAuthCode(
|
||||
ctx,
|
||||
streamcontrol.PlatformName(req.GetPlatID()),
|
||||
req.GetCode(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &streamd_grpc.SubmitOAuthCodeReply{}, nil
|
||||
}
|
||||
|
||||
func (d *StreamD) AddOAuthListenPort(port uint16) {
|
||||
logger.Default().Debugf("AddOAuthListenPort(%d)", port)
|
||||
defer logger.Default().Debugf("/AddOAuthListenPort(%d)", port)
|
||||
d.OAuthListenPortsLocker.Lock()
|
||||
defer d.OAuthListenPortsLocker.Unlock()
|
||||
d.OAuthListenPorts[port] = struct{}{}
|
||||
}
|
||||
|
||||
func (d *StreamD) RemoveOAuthListenPort(port uint16) {
|
||||
logger.Default().Debugf("RemoveOAuthListenPort(%d)", port)
|
||||
defer logger.Default().Debugf("/RemoveOAuthListenPort(%d)", port)
|
||||
d.OAuthListenPortsLocker.Lock()
|
||||
defer d.OAuthListenPortsLocker.Unlock()
|
||||
delete(d.OAuthListenPorts, port)
|
||||
}
|
||||
|
||||
func (d *StreamD) GetOAuthListenPorts() []uint16 {
|
||||
d.OAuthListenPortsLocker.Lock()
|
||||
defer d.OAuthListenPortsLocker.Unlock()
|
||||
|
||||
var ports []uint16
|
||||
for k := range d.OAuthListenPorts {
|
||||
ports = append(ports, k)
|
||||
}
|
||||
|
||||
sort.Slice(ports, func(i, j int) bool {
|
||||
return ports[i] < ports[j]
|
||||
})
|
||||
|
||||
logger.Default().Debugf("oauth ports: %#+v", ports)
|
||||
return ports
|
||||
}
|
||||
|
||||
func serverTypeServer2API(t types.ServerType) api.StreamServerType {
|
||||
switch t {
|
||||
case types.ServerTypeUndefined:
|
||||
return api.StreamServerTypeUndefined
|
||||
case types.ServerTypeRTSP:
|
||||
return api.StreamServerTypeRTSP
|
||||
case types.ServerTypeRTMP:
|
||||
return api.StreamServerTypeRTMP
|
||||
default:
|
||||
panic(fmt.Errorf("unexpected server type: %v", t))
|
||||
}
|
||||
}
|
||||
|
||||
func serverTypeAPI2Server(t api.StreamServerType) types.ServerType {
|
||||
switch t {
|
||||
case api.StreamServerTypeUndefined:
|
||||
return types.ServerTypeUndefined
|
||||
case api.StreamServerTypeRTSP:
|
||||
return types.ServerTypeRTSP
|
||||
case api.StreamServerTypeRTMP:
|
||||
return types.ServerTypeRTMP
|
||||
default:
|
||||
panic(fmt.Errorf("unexpected server type: %v", t))
|
||||
}
|
||||
}
|
||||
|
||||
func (d *StreamD) ListStreamServers(
|
||||
ctx context.Context,
|
||||
) ([]api.StreamServer, error) {
|
||||
d.StreamServerLocker.Lock()
|
||||
defer d.StreamServerLocker.Unlock()
|
||||
servers := d.StreamServer.ListServers(ctx)
|
||||
|
||||
var result []api.StreamServer
|
||||
for _, src := range servers {
|
||||
result = append(result, api.StreamServer{
|
||||
Type: serverTypeServer2API(src.Type()),
|
||||
ListenAddr: src.ListenAddr(),
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (d *StreamD) StartStreamServer(
|
||||
ctx context.Context,
|
||||
serverType api.StreamServerType,
|
||||
listenAddr string,
|
||||
) error {
|
||||
d.StreamServerLocker.Lock()
|
||||
defer d.StreamServerLocker.Unlock()
|
||||
return d.StreamServer.StartServer(
|
||||
ctx,
|
||||
serverTypeAPI2Server(serverType),
|
||||
listenAddr,
|
||||
)
|
||||
}
|
||||
|
||||
func (d *StreamD) StopStreamServer(
|
||||
ctx context.Context,
|
||||
listenAddr string,
|
||||
) error {
|
||||
d.StreamServerLocker.Lock()
|
||||
defer d.StreamServerLocker.Unlock()
|
||||
for _, server := range d.StreamServer.ListServers(ctx) {
|
||||
if server.ListenAddr() == listenAddr {
|
||||
return d.StreamServer.StopServer(ctx, server)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("have not found any stream listeners at %s", listenAddr)
|
||||
}
|
||||
|
||||
func (d *StreamD) ListIncomingStreams(
|
||||
ctx context.Context,
|
||||
) ([]api.IncomingStream, error) {
|
||||
d.StreamServerLocker.Lock()
|
||||
defer d.StreamServerLocker.Unlock()
|
||||
var result []api.IncomingStream
|
||||
for _, src := range d.StreamServer.ListIncomingStreams(ctx) {
|
||||
result = append(result, api.IncomingStream{
|
||||
StreamID: api.StreamID(src.StreamID),
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (d *StreamD) ListStreamDestinations(
|
||||
ctx context.Context,
|
||||
) ([]api.StreamDestination, error) {
|
||||
d.StreamServerLocker.Lock()
|
||||
defer d.StreamServerLocker.Unlock()
|
||||
streamDestinations, err := d.StreamServer.ListStreamDestinations(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := make([]api.StreamDestination, 0, len(streamDestinations))
|
||||
for _, dst := range streamDestinations {
|
||||
c = append(c, api.StreamDestination{
|
||||
StreamID: api.StreamID(dst.StreamID),
|
||||
URL: dst.URL,
|
||||
})
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (d *StreamD) AddStreamDestination(
|
||||
ctx context.Context,
|
||||
streamID api.StreamID,
|
||||
url string,
|
||||
) error {
|
||||
d.StreamServerLocker.Lock()
|
||||
defer d.StreamServerLocker.Unlock()
|
||||
return d.StreamServer.AddStreamDestination(ctx, types.StreamID(streamID), url)
|
||||
}
|
||||
|
||||
func (d *StreamD) RemoveStreamDestination(
|
||||
ctx context.Context,
|
||||
streamID api.StreamID,
|
||||
) error {
|
||||
d.StreamServerLocker.Lock()
|
||||
defer d.StreamServerLocker.Unlock()
|
||||
return d.StreamServer.RemoveStreamDestination(ctx, types.StreamID(streamID))
|
||||
}
|
||||
|
||||
func (d *StreamD) listStreamForwards(
|
||||
ctx context.Context,
|
||||
) ([]api.StreamForward, error) {
|
||||
var result []api.StreamForward
|
||||
streamForwards, err := d.StreamServer.ListStreamForwards(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, streamFwd := range streamForwards {
|
||||
result = append(result, api.StreamForward{
|
||||
StreamIDSrc: api.StreamID(streamFwd.StreamIDSrc),
|
||||
StreamIDDst: api.StreamID(streamFwd.StreamIDDst),
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (d *StreamD) ListStreamForwards(
|
||||
ctx context.Context,
|
||||
) ([]api.StreamForward, error) {
|
||||
d.StreamServerLocker.Lock()
|
||||
defer d.StreamServerLocker.Unlock()
|
||||
return d.listStreamForwards(ctx)
|
||||
}
|
||||
|
||||
func (d *StreamD) AddStreamForward(
|
||||
ctx context.Context,
|
||||
streamIDSrc api.StreamID,
|
||||
streamIDDst api.StreamID,
|
||||
) error {
|
||||
d.StreamServerLocker.Lock()
|
||||
defer d.StreamServerLocker.Unlock()
|
||||
return d.StreamServer.AddStreamForward(ctx, types.StreamID(streamIDSrc), types.StreamID(streamIDDst))
|
||||
}
|
||||
|
||||
func (d *StreamD) RemoveStreamForward(
|
||||
ctx context.Context,
|
||||
streamIDSrc api.StreamID,
|
||||
streamIDDst api.StreamID,
|
||||
) error {
|
||||
d.StreamServerLocker.Lock()
|
||||
defer d.StreamServerLocker.Unlock()
|
||||
return d.StreamServer.RemoveStreamForward(ctx, types.StreamID(streamIDSrc), types.StreamID(streamIDDst))
|
||||
}
|
||||
|
||||
@@ -31,4 +31,9 @@ type UI interface {
|
||||
ctx context.Context,
|
||||
cfg *streamcontrol.PlatformConfig[obs.PlatformSpecificConfig, obs.StreamProfile],
|
||||
) (bool, error)
|
||||
OnSubmittedOAuthCode(
|
||||
ctx context.Context,
|
||||
platID streamcontrol.PlatformName,
|
||||
code string,
|
||||
) error
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func (p *Panel) setImage(
|
||||
|
||||
err = p.StreamD.SetVariable(ctx, key, b)
|
||||
if err != nil {
|
||||
p.DisplayError(fmt.Errorf("unable to set the screenshot: %w", err))
|
||||
logger.Error(ctx, fmt.Errorf("unable to set the screenshot: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +151,10 @@ func (p *Panel) setScreenshot(
|
||||
}
|
||||
|
||||
if bounds.Max.X > ScreenshotMaxWidth || bounds.Max.Y > ScreenshotMaxHeight {
|
||||
screenshot = imgFitTo(screenshot, bounds.Max)
|
||||
screenshot = imgFitTo(screenshot, image.Point{
|
||||
X: ScreenshotMaxWidth,
|
||||
Y: ScreenshotMaxHeight,
|
||||
})
|
||||
logger.Tracef(ctx, "rescaled the screenshot from %#+v to %#+v", bounds, screenshot.Bounds())
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,21 @@ package streampanel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"image"
|
||||
"sync"
|
||||
|
||||
//"image"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"github.com/anthonynsimon/bild/adjust"
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/obs"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/twitch"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/youtube"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streampanel/consts"
|
||||
)
|
||||
|
||||
@@ -62,14 +69,18 @@ func (p *Panel) updateMonitorPage(
|
||||
logger.Tracef(ctx, "updateMonitorPage")
|
||||
defer logger.Tracef(ctx, "/updateMonitorPage")
|
||||
|
||||
{
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
img, err := p.getImage(ctx, consts.ImageScreenshot)
|
||||
|
||||
if err != nil {
|
||||
logger.Error(ctx, err)
|
||||
} else {
|
||||
s := p.screenshotContainer.Size()
|
||||
img = imgFitTo(img, image.Point{X: int(s.Width), Y: int(s.Height)})
|
||||
//s := p.mainWindow.Canvas().Size()
|
||||
//img = imgFitTo(img, image.Point{X: int(s.Width), Y: int(s.Height)})
|
||||
img = adjust.Brightness(img, -0.5)
|
||||
imgFyne := canvas.NewImageFromImage(img)
|
||||
imgFyne.FillMode = canvas.ImageFillOriginal
|
||||
@@ -79,15 +90,17 @@ func (p *Panel) updateMonitorPage(
|
||||
p.screenshotContainer.Objects = append(p.screenshotContainer.Objects, imgFyne)
|
||||
p.screenshotContainer.Refresh()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
{
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
img, err := p.getImage(ctx, consts.ImageChat)
|
||||
if err != nil {
|
||||
logger.Error(ctx, err)
|
||||
} else {
|
||||
s := p.chatContainer.Size()
|
||||
img = imgFitTo(img, image.Point{X: int(s.Width), Y: int(s.Height)})
|
||||
//s := p.mainWindow.Canvas().Size()
|
||||
//img = imgFitTo(img, image.Point{X: int(s.Width), Y: int(s.Height)})
|
||||
imgFyne := canvas.NewImageFromImage(img)
|
||||
imgFyne.FillMode = canvas.ImageFillOriginal
|
||||
|
||||
@@ -96,5 +109,53 @@ func (p *Panel) updateMonitorPage(
|
||||
p.chatContainer.Objects = append(p.chatContainer.Objects, layout.NewSpacer(), container.NewHBox(imgFyne, layout.NewSpacer()))
|
||||
p.chatContainer.Refresh()
|
||||
}
|
||||
}()
|
||||
|
||||
for _, platID := range []streamcontrol.PlatformName{
|
||||
obs.ID,
|
||||
youtube.ID,
|
||||
twitch.ID,
|
||||
} {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
dst := p.streamStatus[platID]
|
||||
|
||||
ok, err := p.StreamD.IsBackendEnabled(ctx, platID)
|
||||
if err != nil {
|
||||
logger.Error(ctx, err)
|
||||
dst.Importance = widget.LowImportance
|
||||
dst.SetText("error")
|
||||
return
|
||||
}
|
||||
|
||||
if !ok {
|
||||
dst.SetText("disabled")
|
||||
return
|
||||
}
|
||||
|
||||
streamStatus, err := p.StreamD.GetStreamStatus(ctx, platID)
|
||||
if err != nil {
|
||||
logger.Error(ctx, err)
|
||||
dst.SetText("error")
|
||||
return
|
||||
}
|
||||
|
||||
if !streamStatus.IsActive {
|
||||
dst.Importance = widget.DangerImportance
|
||||
dst.SetText("stopped")
|
||||
return
|
||||
}
|
||||
dst.Importance = widget.SuccessImportance
|
||||
if streamStatus.StartedAt != nil {
|
||||
duration := time.Since(*streamStatus.StartedAt)
|
||||
dst.SetText(duration.Truncate(time.Second).String())
|
||||
} else {
|
||||
dst.SetText("started")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import (
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/api"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/client"
|
||||
streamdconfig "github.com/xaionaro-go/streamctl/pkg/streamd/config"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/grpc/go/streamd_grpc"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streampanel/config"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streampanel/consts"
|
||||
"github.com/xaionaro-go/streamctl/pkg/xpath"
|
||||
@@ -58,6 +59,12 @@ type Panel struct {
|
||||
StreamD api.StreamD
|
||||
Screenshoter Screenshoter
|
||||
|
||||
OnInternallySubmittedOAuthCode func(
|
||||
ctx context.Context,
|
||||
platID streamcontrol.PlatformName,
|
||||
code string,
|
||||
) error
|
||||
|
||||
screenshoterClose context.CancelFunc
|
||||
screenshoterLocker sync.Mutex
|
||||
|
||||
@@ -82,6 +89,8 @@ type Panel struct {
|
||||
screenshotContainer *fyne.Container
|
||||
chatContainer *fyne.Container
|
||||
|
||||
streamStatus map[streamcontrol.PlatformName]*widget.Label
|
||||
|
||||
filterValue string
|
||||
|
||||
youtubeCheck *widget.Check
|
||||
@@ -100,6 +109,8 @@ type Panel struct {
|
||||
|
||||
imageLocker sync.Mutex
|
||||
imageLastDownloaded map[consts.ImageID][]byte
|
||||
|
||||
lastDisplayedError error
|
||||
}
|
||||
|
||||
func New(
|
||||
@@ -122,6 +133,7 @@ func New(
|
||||
Config: Options(opts).ApplyOverrides(cfg),
|
||||
Screenshoter: screenshoter.New(screenshot.Implementation{}),
|
||||
imageLastDownloaded: map[consts.ImageID][]byte{},
|
||||
streamStatus: map[streamcontrol.PlatformName]*widget.Label{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -193,31 +205,15 @@ func (p *Panel) Loop(ctx context.Context, opts ...LoopOption) error {
|
||||
loadingWindowText.ParseMarkdown(fmt.Sprintf("# %s", msg))
|
||||
}
|
||||
}
|
||||
if streamD, ok := p.StreamD.(*client.Client); ok && false {
|
||||
oauthURLChan, err := streamD.SubscriberToOAuthURLs(ctx)
|
||||
if err != nil {
|
||||
p.DisplayError(fmt.Errorf("unable to subscribe to OAuth requests of streamd: %w", err))
|
||||
}
|
||||
if streamD, ok := p.StreamD.(*client.Client); ok {
|
||||
p.startOAuthListenerForRemoteStreamD(ctx, streamD)
|
||||
} else {
|
||||
// TODO: delete this hardcoding of the port
|
||||
streamD := p.StreamD.(*streamd.StreamD)
|
||||
streamD.AddOAuthListenPort(8091)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case authURL, ok := <-oauthURLChan:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if authURL == "" {
|
||||
logger.Errorf(ctx, "received an empty authURL")
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
oauthhandler.LaunchBrowser(authURL)
|
||||
time.Sleep(1 * time.Second) // throttling the execution to avoid hanging the OS
|
||||
}
|
||||
}
|
||||
<-ctx.Done()
|
||||
streamD.RemoveOAuthListenPort(8091)
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -247,6 +243,64 @@ func (p *Panel) Loop(ctx context.Context, opts ...LoopOption) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Panel) startOAuthListenerForRemoteStreamD(
|
||||
ctx context.Context,
|
||||
streamD *client.Client,
|
||||
) {
|
||||
ctx, cancelFn := context.WithCancel(ctx)
|
||||
receiver, listenPort, err := oauthhandler.NewCodeReceiver(ctx, 0)
|
||||
if err != nil {
|
||||
cancelFn()
|
||||
p.DisplayError(fmt.Errorf("unable to start listener for OAuth responses: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
oauthURLChan, err := streamD.SubscriberToOAuthURLs(ctx, listenPort)
|
||||
if err != nil {
|
||||
cancelFn()
|
||||
p.DisplayError(fmt.Errorf("unable to subscribe to OAuth requests of streamd: %w", err))
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer cancelFn()
|
||||
defer p.DisplayError(fmt.Errorf("oauth handler was closed"))
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case req, ok := <-oauthURLChan:
|
||||
if !ok {
|
||||
logger.Errorf(ctx, "oauth request receiver is closed")
|
||||
return
|
||||
}
|
||||
|
||||
if req == nil || req.AuthURL == "" {
|
||||
logger.Errorf(ctx, "received an empty oauth request")
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := p.openBrowser(req.GetAuthURL()); err != nil {
|
||||
p.DisplayError(fmt.Errorf("unable to open browser with URL '%s': %w", req.GetAuthURL(), err))
|
||||
continue
|
||||
}
|
||||
|
||||
code := <-receiver
|
||||
logger.Debugf(ctx, "received oauth code: %s", code)
|
||||
_, err := p.StreamD.SubmitOAuthCode(ctx, &streamd_grpc.SubmitOAuthCodeRequest{
|
||||
PlatID: req.GetPlatID(),
|
||||
Code: code,
|
||||
})
|
||||
if err != nil {
|
||||
p.DisplayError(fmt.Errorf("unable to submit the oauth code of '%s': %w", req.GetPlatID(), err))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
func (p *Panel) newLoadingWindow(ctx context.Context) fyne.Window {
|
||||
logger.FromCtx(ctx).Debugf("newLoadingWindow")
|
||||
defer logger.FromCtx(ctx).Debugf("endof newLoadingWindow")
|
||||
@@ -383,20 +437,35 @@ func (p *Panel) InputOBSConnectInfo(
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (p *Panel) OnSubmittedOAuthCode(
|
||||
ctx context.Context,
|
||||
platID streamcontrol.PlatformName,
|
||||
code string,
|
||||
) error {
|
||||
logger.Debugf(ctx, "OnSubmittedOAuthCode(ctx, '%s', '%s')", platID, code)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Panel) OAuthHandlerTwitch(ctx context.Context, arg oauthhandler.OAuthHandlerArgument) error {
|
||||
logger.Infof(ctx, "OAuthHandlerTwitch: %#+v", arg)
|
||||
defer logger.Infof(ctx, "/OAuthHandlerTwitch")
|
||||
return p.oauthHandler(ctx, arg)
|
||||
return p.oauthHandler(ctx, twitch.ID, arg)
|
||||
}
|
||||
|
||||
func (p *Panel) OAuthHandlerYouTube(ctx context.Context, arg oauthhandler.OAuthHandlerArgument) error {
|
||||
logger.Infof(ctx, "OAuthHandlerYouTube: %#+v", arg)
|
||||
defer logger.Infof(ctx, "/OAuthHandlerYouTube")
|
||||
return p.oauthHandler(ctx, arg)
|
||||
return p.oauthHandler(ctx, youtube.ID, arg)
|
||||
}
|
||||
|
||||
func (p *Panel) oauthHandler(ctx context.Context, arg oauthhandler.OAuthHandlerArgument) error {
|
||||
codeCh, err := oauthhandler.NewCodeReceiver(arg.RedirectURL)
|
||||
func (p *Panel) oauthHandler(
|
||||
ctx context.Context,
|
||||
platID streamcontrol.PlatformName,
|
||||
arg oauthhandler.OAuthHandlerArgument,
|
||||
) error {
|
||||
ctx, cancelFn := context.WithCancel(ctx)
|
||||
defer cancelFn()
|
||||
codeCh, _, err := oauthhandler.NewCodeReceiver(ctx, arg.ListenPort)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -410,7 +479,17 @@ func (p *Panel) oauthHandler(ctx context.Context, arg oauthhandler.OAuthHandlerA
|
||||
// Wait for the web server to get the code.
|
||||
code := <-codeCh
|
||||
logger.Debugf(ctx, "received the auth code")
|
||||
return arg.ExchangeFn(code)
|
||||
err = arg.ExchangeFn(code)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to exchange the code: %w", err)
|
||||
}
|
||||
if p.OnInternallySubmittedOAuthCode != nil {
|
||||
err := p.OnInternallySubmittedOAuthCode(ctx, platID, code)
|
||||
if err != nil {
|
||||
return fmt.Errorf("OnInternallySubmittedOAuthCode return an error: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Panel) openBrowser(authURL string) error {
|
||||
@@ -1393,10 +1472,30 @@ func (p *Panel) initMainWindow(
|
||||
monitorBackgroundFyne.FillMode = canvas.ImageFillStretch
|
||||
|
||||
p.screenshotContainer = container.NewBorder(nil, nil, nil, nil)
|
||||
obsLabel := widget.NewLabel("OBS:")
|
||||
obsLabel.Importance = widget.LowImportance
|
||||
p.streamStatus[obs.ID] = widget.NewLabel("")
|
||||
twLabel := widget.NewLabel("TW:")
|
||||
twLabel.Importance = widget.LowImportance
|
||||
p.streamStatus[twitch.ID] = widget.NewLabel("")
|
||||
ytLabel := widget.NewLabel("YT:")
|
||||
ytLabel.Importance = widget.LowImportance
|
||||
p.streamStatus[youtube.ID] = widget.NewLabel("")
|
||||
streamInfoContainer := container.NewBorder(
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
container.NewVBox(
|
||||
container.NewHBox(obsLabel, p.streamStatus[obs.ID]),
|
||||
container.NewHBox(twLabel, p.streamStatus[twitch.ID]),
|
||||
container.NewHBox(ytLabel, p.streamStatus[youtube.ID]),
|
||||
),
|
||||
)
|
||||
p.chatContainer = container.NewBorder(nil, nil, nil, nil)
|
||||
monitorPage := container.NewStack(
|
||||
monitorBackgroundFyne,
|
||||
p.screenshotContainer,
|
||||
streamInfoContainer,
|
||||
p.chatContainer,
|
||||
)
|
||||
|
||||
@@ -2322,6 +2421,10 @@ func (p *Panel) DisplayError(err error) {
|
||||
logger.Debugf(p.defaultContext, "DisplayError('%v')", err)
|
||||
defer logger.Debugf(p.defaultContext, "/DisplayError('%v')", err)
|
||||
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
errorMessage := fmt.Sprintf("Error: %v\n\nstack trace:\n%s", err, debug.Stack())
|
||||
textWidget := widget.NewMultiLineEntry()
|
||||
textWidget.SetText(errorMessage)
|
||||
@@ -2333,6 +2436,15 @@ func (p *Panel) DisplayError(err error) {
|
||||
|
||||
p.displayErrorLocker.Lock()
|
||||
defer p.displayErrorLocker.Unlock()
|
||||
|
||||
if p.lastDisplayedError != nil {
|
||||
// protection against flood:
|
||||
if err.Error() == p.lastDisplayedError.Error() {
|
||||
return
|
||||
}
|
||||
}
|
||||
p.lastDisplayedError = err
|
||||
|
||||
if p.displayErrorWindow != nil {
|
||||
p.displayErrorWindow.SetContent(container.NewVSplit(p.displayErrorWindow.Content(), textWidget))
|
||||
return
|
||||
|
||||
9
pkg/streamserver/consts/consts.go
Normal file
9
pkg/streamserver/consts/consts.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package consts
|
||||
|
||||
const (
|
||||
UserAgent = "streamcontrol/0.1"
|
||||
)
|
||||
|
||||
const (
|
||||
StreamNotFound = "stream not found"
|
||||
)
|
||||
237
pkg/streamserver/server/rtmp/rtmp_server.go
Normal file
237
pkg/streamserver/server/rtmp/rtmp_server.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package rtmp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/flv"
|
||||
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
||||
"github.com/facebookincubator/go-belt/tool/experimental/errmon"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamserver/consts"
|
||||
"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
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Listen string `yaml:"listen" json:"listen"`
|
||||
}
|
||||
|
||||
func New(
|
||||
ctx context.Context,
|
||||
cfg Config,
|
||||
streamHandler *streams.StreamHandler,
|
||||
) (*RTMPServer, error) {
|
||||
if cfg.Listen == "" {
|
||||
cfg.Listen = "127.0.0.1:1935"
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", cfg.Listen)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to start listening '%s': %w", cfg.Listen, err)
|
||||
}
|
||||
|
||||
ctx, cancelFn := context.WithCancel(ctx)
|
||||
s := &RTMPServer{
|
||||
Config: cfg,
|
||||
StreamHandler: streamHandler,
|
||||
CancelFn: cancelFn,
|
||||
Listener: ln,
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
err := ln.Close()
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err = s.tcpHandle(conn); err != nil {
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}()
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *RTMPServer) Type() types.ServerType {
|
||||
return types.ServerTypeRTMP
|
||||
}
|
||||
func (s *RTMPServer) ListenAddr() string {
|
||||
return s.Listener.Addr().String()
|
||||
}
|
||||
func (s *RTMPServer) Close() error {
|
||||
s.CancelFn()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RTMPServer) tcpHandle(netConn net.Conn) error {
|
||||
rtmpConn, err := rtmp.NewServer(netConn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = rtmpConn.ReadCommands(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch rtmpConn.Intent {
|
||||
case rtmp.CommandPlay:
|
||||
stream := s.StreamHandler.Get(rtmpConn.App)
|
||||
if stream == nil {
|
||||
return errors.New("stream not found: " + rtmpConn.App)
|
||||
}
|
||||
|
||||
cons := flv.NewConsumer()
|
||||
if err = stream.AddConsumer(cons); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer stream.RemoveConsumer(cons)
|
||||
|
||||
if err = rtmpConn.WriteStart(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = cons.WriteTo(rtmpConn)
|
||||
|
||||
return nil
|
||||
|
||||
case rtmp.CommandPublish:
|
||||
stream := s.StreamHandler.Get(rtmpConn.App)
|
||||
if stream == nil {
|
||||
return errors.New("stream not found: " + rtmpConn.App)
|
||||
}
|
||||
|
||||
if err = rtmpConn.WriteStart(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prod, err := rtmpConn.Producer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stream.AddProducer(prod)
|
||||
|
||||
defer stream.RemoveProducer(prod)
|
||||
|
||||
_ = prod.Start()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("rtmp: unknown command: " + rtmpConn.Intent)
|
||||
}
|
||||
|
||||
func StreamsHandle(url string) (core.Producer, error) {
|
||||
return rtmp.DialPlay(url)
|
||||
}
|
||||
|
||||
func StreamsConsumerHandle(url string) (core.Consumer, func(context.Context) error, error) {
|
||||
cons := flv.NewConsumer()
|
||||
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)
|
||||
}
|
||||
|
||||
ctx, cancelFn := context.WithCancel(ctx)
|
||||
defer cancelFn()
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
cancelFn()
|
||||
err := wr.(io.Closer).Close()
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
}()
|
||||
|
||||
_, err = cons.WriteTo(wr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to write: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return cons, run, nil
|
||||
}
|
||||
|
||||
func (s *RTMPServer) apiHandle(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
s.outputFLV(w, r)
|
||||
} else {
|
||||
s.inputFLV(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RTMPServer) outputFLV(w http.ResponseWriter, r *http.Request) {
|
||||
src := r.URL.Query().Get("src")
|
||||
stream := s.StreamHandler.Get(src)
|
||||
if stream == nil {
|
||||
http.Error(w, consts.StreamNotFound, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
cons := flv.NewConsumer()
|
||||
cons.WithRequest(r)
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
h := w.Header()
|
||||
h.Set("Content-Type", "video/x-flv")
|
||||
|
||||
_, _ = cons.WriteTo(w)
|
||||
|
||||
stream.RemoveConsumer(cons)
|
||||
}
|
||||
|
||||
func (s *RTMPServer) inputFLV(w http.ResponseWriter, r *http.Request) {
|
||||
dst := r.URL.Query().Get("dst")
|
||||
stream := s.StreamHandler.Get(dst)
|
||||
if stream == nil {
|
||||
http.Error(w, consts.StreamNotFound, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
client, err := flv.Open(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
stream.AddProducer(client)
|
||||
|
||||
if err = client.Start(); err != nil && err != io.EOF {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
}
|
||||
|
||||
stream.RemoveProducer(client)
|
||||
}
|
||||
302
pkg/streamserver/server/rtsp/rtsp_server.go
Normal file
302
pkg/streamserver/server/rtsp/rtsp_server.go
Normal file
@@ -0,0 +1,302 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"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/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
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
ListenAddr string
|
||||
Username string
|
||||
Password string
|
||||
DefaultQuery string
|
||||
PacketSize uint16
|
||||
}
|
||||
|
||||
func New(
|
||||
ctx context.Context,
|
||||
cfg Config,
|
||||
streamHandler *streams.StreamHandler,
|
||||
) (*RTSPServer, error) {
|
||||
if cfg.ListenAddr == "" {
|
||||
cfg.ListenAddr = "127.0.0.1:8554"
|
||||
}
|
||||
if cfg.DefaultQuery == "" {
|
||||
cfg.DefaultQuery = "video&audio"
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", cfg.ListenAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to listen '%s': %w", cfg.ListenAddr, err)
|
||||
}
|
||||
|
||||
logger.Default().WithField("addr", cfg.ListenAddr).Info("[rtsp] listen")
|
||||
|
||||
ctx, cancelFn := context.WithCancel(ctx)
|
||||
s := &RTSPServer{
|
||||
Config: cfg,
|
||||
Listener: ln,
|
||||
StreamHandler: streamHandler,
|
||||
CancelFn: cancelFn,
|
||||
}
|
||||
|
||||
if query, err := url.ParseQuery(cfg.DefaultQuery); err == nil {
|
||||
s.DefaultMedias = ParseQuery(query)
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
err := ln.Close()
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c := rtsp.NewServer(conn)
|
||||
c.PacketSize = cfg.PacketSize
|
||||
// skip check auth for localhost
|
||||
if cfg.Username != "" && !conn.RemoteAddr().(*net.TCPAddr).IP.IsLoopback() {
|
||||
c.Auth(cfg.Username, cfg.Password)
|
||||
}
|
||||
go s.tcpHandler(c)
|
||||
}
|
||||
}()
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
type HandlerFunc func(conn *rtsp.Conn) bool
|
||||
|
||||
func (s *RTSPServer) HandleFunc(handler HandlerFunc) {
|
||||
s.Handlers = append(s.Handlers, handler)
|
||||
}
|
||||
|
||||
func Handler(rawURL string) (core.Producer, error) {
|
||||
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
|
||||
|
||||
conn := rtsp.NewClient(rawURL)
|
||||
conn.Backchannel = true
|
||||
conn.UserAgent = consts.UserAgent
|
||||
|
||||
if rawQuery != "" {
|
||||
query := streams.ParseQuery(rawQuery)
|
||||
conn.Backchannel = query.Get("backchannel") == "1"
|
||||
conn.Media = query.Get("media")
|
||||
conn.Timeout = core.Atoi(query.Get("timeout"))
|
||||
conn.Transport = query.Get("transport")
|
||||
}
|
||||
|
||||
if logger.Default().Level() >= logger.LevelTrace {
|
||||
conn.Listen(func(msg any) {
|
||||
switch msg := msg.(type) {
|
||||
case *tcp.Request:
|
||||
logger.Default().Tracef("[rtsp] client request:\n%s", msg)
|
||||
case *tcp.Response:
|
||||
logger.Default().Tracef("[rtsp] client response:\n%s", msg)
|
||||
case string:
|
||||
logger.Default().Tracef("[rtsp] client msg: %s", msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if err := conn.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := conn.Describe(); err != nil {
|
||||
if !conn.Backchannel {
|
||||
return nil, err
|
||||
}
|
||||
logger.Default().Tracef("[rtsp] describe (backchannel=%t) err: %v", conn.Backchannel, err)
|
||||
|
||||
// second try without backchannel, we need to reconnect
|
||||
conn.Backchannel = false
|
||||
if err = conn.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = conn.Describe(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (s *RTSPServer) tcpHandler(conn *rtsp.Conn) {
|
||||
var name string
|
||||
var closer func()
|
||||
|
||||
trace := logger.Default().Level() >= logger.LevelTrace
|
||||
|
||||
conn.Listen(func(msg any) {
|
||||
if trace {
|
||||
switch msg := msg.(type) {
|
||||
case *tcp.Request:
|
||||
logger.Default().Tracef("[rtsp] server request:\n%s", msg)
|
||||
case *tcp.Response:
|
||||
logger.Default().Tracef("[rtsp] server response:\n%s", msg)
|
||||
}
|
||||
}
|
||||
|
||||
switch msg {
|
||||
case rtsp.MethodDescribe:
|
||||
if len(conn.URL.Path) == 0 {
|
||||
logger.Default().Warn("[rtsp] server empty URL on DESCRIBE")
|
||||
return
|
||||
}
|
||||
|
||||
name = conn.URL.Path[1:]
|
||||
|
||||
stream := s.StreamHandler.Get(name)
|
||||
if stream == nil {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Default().WithField("stream", name).Debug("[rtsp] new consumer")
|
||||
|
||||
conn.SessionName = consts.UserAgent
|
||||
|
||||
query := conn.URL.Query()
|
||||
conn.Medias = ParseQuery(query)
|
||||
if conn.Medias == nil {
|
||||
for _, media := range s.DefaultMedias {
|
||||
conn.Medias = append(conn.Medias, media.Clone())
|
||||
}
|
||||
}
|
||||
|
||||
if s := query.Get("pkt_size"); s != "" {
|
||||
conn.PacketSize = uint16(core.Atoi(s))
|
||||
}
|
||||
|
||||
if err := stream.AddConsumer(conn); err != nil {
|
||||
logger.Default().WithField("error", err).WithField("stream", name).Warn("[rtsp]")
|
||||
return
|
||||
}
|
||||
|
||||
closer = func() {
|
||||
stream.RemoveConsumer(conn)
|
||||
}
|
||||
|
||||
case rtsp.MethodAnnounce:
|
||||
if len(conn.URL.Path) == 0 {
|
||||
logger.Default().Warn("[rtsp] server empty URL on ANNOUNCE")
|
||||
return
|
||||
}
|
||||
|
||||
name = conn.URL.Path[1:]
|
||||
|
||||
stream := s.StreamHandler.Get(name)
|
||||
if stream == nil {
|
||||
return
|
||||
}
|
||||
|
||||
query := conn.URL.Query()
|
||||
if s := query.Get("timeout"); s != "" {
|
||||
conn.Timeout = core.Atoi(s)
|
||||
}
|
||||
|
||||
logger.Default().WithField("stream", name).Debug("[rtsp] new producer")
|
||||
|
||||
stream.AddProducer(conn)
|
||||
|
||||
closer = func() {
|
||||
stream.RemoveProducer(conn)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if err := conn.Accept(); err != nil {
|
||||
if err != io.EOF {
|
||||
logger.Default().WithField("error", err).Warn()
|
||||
}
|
||||
if closer != nil {
|
||||
closer()
|
||||
}
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
for _, handler := range s.Handlers {
|
||||
if handler(conn) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if closer != nil {
|
||||
if err := conn.Handle(); err != nil {
|
||||
logger.Default().WithField("error", err).Debug("[rtsp] handle")
|
||||
}
|
||||
|
||||
closer()
|
||||
|
||||
logger.Default().WithField("stream", name).Debug("[rtsp] disconnect")
|
||||
}
|
||||
|
||||
_ = conn.Close()
|
||||
}
|
||||
|
||||
func (s *RTSPServer) Type() types.ServerType {
|
||||
return types.ServerTypeRTSP
|
||||
}
|
||||
func (s *RTSPServer) ListenAddr() string {
|
||||
return s.Listener.Addr().String()
|
||||
}
|
||||
func (s *RTSPServer) Close() error {
|
||||
s.CancelFn()
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseQuery(query map[string][]string) []*core.Media {
|
||||
if v := query["mp4"]; v != nil {
|
||||
return []*core.Media{
|
||||
{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecH264},
|
||||
{Name: core.CodecH265},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecAAC},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return core.ParseQuery(query)
|
||||
}
|
||||
351
pkg/streamserver/stream_server.go
Normal file
351
pkg/streamserver/stream_server.go
Normal file
@@ -0,0 +1,351 @@
|
||||
package streamserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
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"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamserver/types"
|
||||
)
|
||||
|
||||
type StreamServer struct {
|
||||
sync.Mutex
|
||||
StreamHandler *streams.StreamHandler
|
||||
ServerHandlers []types.ServerHandler
|
||||
StreamDestinations []types.StreamDestination
|
||||
}
|
||||
|
||||
func New() *StreamServer {
|
||||
s := streams.NewStreamHandler()
|
||||
|
||||
s.HandleFunc("rtmp", rtmpserver.StreamsHandle)
|
||||
s.HandleFunc("rtmps", rtmpserver.StreamsHandle)
|
||||
s.HandleFunc("rtmpx", rtmpserver.StreamsHandle)
|
||||
s.HandleConsumerFunc("rtmp", rtmpserver.StreamsConsumerHandle)
|
||||
s.HandleConsumerFunc("rtmps", rtmpserver.StreamsConsumerHandle)
|
||||
s.HandleConsumerFunc("rtmpx", rtmpserver.StreamsConsumerHandle)
|
||||
s.HandleFunc("rtsp", rtspserver.Handler)
|
||||
s.HandleFunc("rtsps", rtspserver.Handler)
|
||||
s.HandleFunc("rtspx", rtspserver.Handler)
|
||||
|
||||
return &StreamServer{
|
||||
StreamHandler: s,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StreamServer) ListServers(
|
||||
ctx context.Context,
|
||||
) []types.ServerHandler {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
c := make([]types.ServerHandler, len(s.ServerHandlers))
|
||||
copy(c, s.ServerHandlers)
|
||||
return c
|
||||
}
|
||||
|
||||
func (s *StreamServer) StartServer(
|
||||
ctx context.Context,
|
||||
serverType types.ServerType,
|
||||
listenAddr string,
|
||||
) error {
|
||||
var srv types.ServerHandler
|
||||
var err error
|
||||
switch serverType {
|
||||
case types.ServerTypeRTMP:
|
||||
srv, err = rtmpserver.New(ctx, rtmpserver.Config{
|
||||
Listen: listenAddr,
|
||||
}, s.StreamHandler)
|
||||
case types.ServerTypeRTSP:
|
||||
srv, err = rtspserver.New(ctx, rtspserver.Config{
|
||||
ListenAddr: listenAddr,
|
||||
}, s.StreamHandler)
|
||||
default:
|
||||
return fmt.Errorf("unexpected server type %v", serverType)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
s.ServerHandlers = append(s.ServerHandlers, srv)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StreamServer) StopServer(
|
||||
ctx context.Context,
|
||||
server types.ServerHandler,
|
||||
) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
for i := range s.ServerHandlers {
|
||||
if s.ServerHandlers[i] == server {
|
||||
s.ServerHandlers = append(s.ServerHandlers[:i], s.ServerHandlers[i+1:]...)
|
||||
return server.Close()
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("server not found")
|
||||
}
|
||||
|
||||
func (s *StreamServer) AddIncomingStream(
|
||||
ctx context.Context,
|
||||
streamID types.StreamID,
|
||||
) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
return s.addIncomingStream(ctx, streamID)
|
||||
}
|
||||
|
||||
func (s *StreamServer) addIncomingStream(
|
||||
_ context.Context,
|
||||
streamID types.StreamID,
|
||||
) error {
|
||||
if s.StreamHandler.Get(string(streamID)) != nil {
|
||||
return fmt.Errorf("stream '%s' already exists", streamID)
|
||||
}
|
||||
_, err := s.StreamHandler.New(string(streamID), "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create the stream '%s': %w", streamID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type IncomingStream struct {
|
||||
StreamID types.StreamID
|
||||
}
|
||||
|
||||
func (s *StreamServer) ListIncomingStreams(
|
||||
ctx context.Context,
|
||||
) []IncomingStream {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
var result []IncomingStream
|
||||
for _, name := range s.StreamHandler.GetAll() {
|
||||
result = append(
|
||||
result,
|
||||
IncomingStream{
|
||||
StreamID: types.StreamID(name),
|
||||
},
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *StreamServer) RemoveIncomingStream(
|
||||
ctx context.Context,
|
||||
streamID types.StreamID,
|
||||
) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
return s.removeIncomingStream(ctx, streamID)
|
||||
}
|
||||
|
||||
func (s *StreamServer) removeIncomingStream(
|
||||
_ context.Context,
|
||||
streamID types.StreamID,
|
||||
) error {
|
||||
if s.StreamHandler.Get(string(streamID)) == nil {
|
||||
return fmt.Errorf("stream '%s' does not exist", streamID)
|
||||
}
|
||||
s.StreamHandler.Delete(string(streamID))
|
||||
return nil
|
||||
}
|
||||
|
||||
type StreamForward struct {
|
||||
StreamIDSrc types.StreamID
|
||||
StreamIDDst types.StreamID
|
||||
}
|
||||
|
||||
func (s *StreamServer) AddStreamForward(
|
||||
ctx context.Context,
|
||||
streamIDSrc types.StreamID,
|
||||
streamIDDst types.StreamID,
|
||||
) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
return s.addStreamForward(ctx, streamIDSrc, streamIDDst)
|
||||
}
|
||||
|
||||
func (s *StreamServer) addStreamForward(
|
||||
ctx context.Context,
|
||||
streamIDSrc types.StreamID,
|
||||
streamIDDst types.StreamID,
|
||||
) error {
|
||||
streamSrc := s.StreamHandler.Get(string(streamIDSrc))
|
||||
if streamSrc != nil {
|
||||
return fmt.Errorf("unable to find stream ID '%s'", streamIDSrc)
|
||||
}
|
||||
dst, err := s.findStreamDestinationByID(ctx, streamIDDst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to find stream destination '%s': %w", streamIDDst, err)
|
||||
}
|
||||
_, err = streamSrc.Publish(ctx, dst.URL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to start publishing '%s' to '%s': %w", streamIDSrc, dst.URL, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StreamServer) ListStreamForwards(
|
||||
ctx context.Context,
|
||||
) ([]StreamForward, error) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
return s.listStreamForwards(ctx)
|
||||
}
|
||||
|
||||
func (s *StreamServer) listStreamForwards(
|
||||
ctx context.Context,
|
||||
) ([]StreamForward, error) {
|
||||
var result []StreamForward
|
||||
for _, name := range s.StreamHandler.GetAll() {
|
||||
stream := s.StreamHandler.Get(name)
|
||||
if stream == nil {
|
||||
continue
|
||||
}
|
||||
for _, fwd := range stream.Forwardings() {
|
||||
streamIDSrc := types.StreamID(name)
|
||||
streamDst, err := s.findStreamDestinationByURL(ctx, fwd.URL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to convert URL '%s' to a stream ID: %w", fwd.URL, err)
|
||||
}
|
||||
result = append(result, StreamForward{
|
||||
StreamIDSrc: streamIDSrc,
|
||||
StreamIDDst: streamDst.StreamID,
|
||||
})
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *StreamServer) RemoveStreamForward(
|
||||
ctx context.Context,
|
||||
streamIDSrc types.StreamID,
|
||||
streamIDDst types.StreamID,
|
||||
) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
return s.removeStreamForward(ctx, streamIDSrc, streamIDDst)
|
||||
}
|
||||
|
||||
func (s *StreamServer) removeStreamForward(
|
||||
ctx context.Context,
|
||||
streamIDSrc types.StreamID,
|
||||
streamIDDst types.StreamID,
|
||||
) error {
|
||||
stream := s.StreamHandler.Get(string(streamIDSrc))
|
||||
if stream == nil {
|
||||
return fmt.Errorf("unable to find a source stream with ID '%s'", streamIDSrc)
|
||||
}
|
||||
for _, fwd := range stream.Forwardings() {
|
||||
streamDst, err := s.findStreamDestinationByURL(ctx, fwd.URL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to convert URL '%s' to a stream ID: %w", fwd.URL, err)
|
||||
}
|
||||
if streamDst.StreamID != streamIDDst {
|
||||
continue
|
||||
}
|
||||
|
||||
err = fwd.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to close forwarding from %s to %s (%s): %w", streamIDSrc, streamIDDst, fwd.URL, err)
|
||||
}
|
||||
stream.Cleanup()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unable to find stream forwarding from '%s' to '%s'", streamIDSrc, streamIDDst)
|
||||
}
|
||||
|
||||
func (s *StreamServer) ListStreamDestinations(
|
||||
ctx context.Context,
|
||||
) ([]types.StreamDestination, error) {
|
||||
s.Mutex.Lock()
|
||||
defer s.Mutex.Unlock()
|
||||
return s.listStreamDestinations(ctx)
|
||||
}
|
||||
|
||||
func (s *StreamServer) listStreamDestinations(
|
||||
_ context.Context,
|
||||
) ([]types.StreamDestination, error) {
|
||||
c := make([]types.StreamDestination, len(s.StreamDestinations))
|
||||
copy(c, s.StreamDestinations)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (s *StreamServer) AddStreamDestination(
|
||||
ctx context.Context,
|
||||
streamID types.StreamID,
|
||||
url string,
|
||||
) error {
|
||||
s.Mutex.Lock()
|
||||
defer s.Mutex.Unlock()
|
||||
return s.addStreamDestination(ctx, streamID, url)
|
||||
}
|
||||
|
||||
func (s *StreamServer) addStreamDestination(
|
||||
_ context.Context,
|
||||
streamID types.StreamID,
|
||||
url string,
|
||||
) error {
|
||||
s.StreamDestinations = append(s.StreamDestinations, types.StreamDestination{
|
||||
StreamID: streamID,
|
||||
URL: url,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StreamServer) RemoveStreamDestination(
|
||||
ctx context.Context,
|
||||
streamID types.StreamID,
|
||||
) error {
|
||||
s.Mutex.Lock()
|
||||
defer s.Mutex.Unlock()
|
||||
|
||||
streamForwards, err := s.listStreamForwards(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to list stream forwardings: %w", err)
|
||||
}
|
||||
for _, fwd := range streamForwards {
|
||||
if fwd.StreamIDDst == streamID {
|
||||
s.removeStreamForward(ctx, fwd.StreamIDSrc, fwd.StreamIDDst)
|
||||
}
|
||||
}
|
||||
|
||||
for i := range s.StreamDestinations {
|
||||
if s.StreamDestinations[i].StreamID == streamID {
|
||||
s.StreamDestinations = append(s.StreamDestinations[:i], s.StreamDestinations[i+1:]...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("have not found stream destination with id %s", streamID)
|
||||
}
|
||||
|
||||
func (s *StreamServer) findStreamDestinationByURL(
|
||||
_ context.Context,
|
||||
url string,
|
||||
) (types.StreamDestination, error) {
|
||||
for _, dst := range s.StreamDestinations {
|
||||
if dst.URL == url {
|
||||
return dst, nil
|
||||
}
|
||||
}
|
||||
return types.StreamDestination{}, fmt.Errorf("unable to find a stream destination by URL '%s'", url)
|
||||
}
|
||||
|
||||
func (s *StreamServer) findStreamDestinationByID(
|
||||
_ context.Context,
|
||||
streamID types.StreamID,
|
||||
) (types.StreamDestination, error) {
|
||||
for _, dst := range s.StreamDestinations {
|
||||
if dst.StreamID == streamID {
|
||||
return dst, nil
|
||||
}
|
||||
}
|
||||
return types.StreamDestination{}, fmt.Errorf("unable to find a stream destination by StreamID '%s'", streamID)
|
||||
}
|
||||
39
pkg/streamserver/streams/LICENSE
Normal file
39
pkg/streamserver/streams/LICENSE
Normal file
@@ -0,0 +1,39 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Alexey Khit
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
A remark by Dmitrii Okunev:
|
||||
|
||||
The initial code of this package (package `streams`) was copied from
|
||||
repository https://github.com/AlexxIT/go2rtc of revision
|
||||
a4885c2c3abce58074d04878bba0d72105642a9b.
|
||||
|
||||
The content of the LICENSE file is attached above. I had to modify
|
||||
the code (see the details in the paragraph below), and you are free
|
||||
to use whatever license you like to use the modifications applied
|
||||
to this package: CC-0, WTFPL-v2, MIT, or whatever else license.
|
||||
|
||||
The only reason I had to copy and modify this code, is because
|
||||
a lot of packages (including `streams`) in `go2rtc` are shielded
|
||||
from importing by Go tooling through placing them
|
||||
in the `internal/` directory.
|
||||
161
pkg/streamserver/streams/add_consumer.go
Normal file
161
pkg/streamserver/streams/add_consumer.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package streams
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
)
|
||||
|
||||
func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
||||
// support for multiple simultaneous pending from different consumers
|
||||
consN := s.pending.Add(1) - 1
|
||||
|
||||
var prodErrors = make([]error, len(s.producers))
|
||||
var prodMedias []*core.Media
|
||||
var prodStarts []*Producer
|
||||
|
||||
// Step 1. Get consumer medias
|
||||
consMedias := cons.GetMedias()
|
||||
for _, consMedia := range consMedias {
|
||||
logger.Default().Tracef("[streams] check cons=%d media=%s", consN, consMedia)
|
||||
|
||||
producers:
|
||||
for prodN, prod := range s.producers {
|
||||
if prodErrors[prodN] != nil {
|
||||
logger.Default().Tracef("[streams] skip cons=%d prod=%d", consN, prodN)
|
||||
continue
|
||||
}
|
||||
|
||||
if err = prod.Dial(); err != nil {
|
||||
logger.Default().WithField("error", err).Tracef("[streams] dial cons=%d prod=%d", consN, prodN)
|
||||
prodErrors[prodN] = err
|
||||
continue
|
||||
}
|
||||
|
||||
// Step 2. Get producer medias (not tracks yet)
|
||||
for _, prodMedia := range prod.GetMedias() {
|
||||
logger.Default().Tracef("[streams] check cons=%d prod=%d media=%s", consN, prodN, prodMedia)
|
||||
prodMedias = append(prodMedias, prodMedia)
|
||||
|
||||
// Step 3. Match consumer/producer codecs list
|
||||
prodCodec, consCodec := prodMedia.MatchMedia(consMedia)
|
||||
if prodCodec == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var track *core.Receiver
|
||||
|
||||
switch prodMedia.Direction {
|
||||
case core.DirectionRecvonly:
|
||||
logger.Default().Tracef("[streams] match cons=%d <= prod=%d", consN, prodN)
|
||||
|
||||
// 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
|
||||
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")
|
||||
continue
|
||||
}
|
||||
|
||||
case core.DirectionSendonly:
|
||||
logger.Default().Tracef("[streams] match cons=%d => prod=%d", consN, prodN)
|
||||
|
||||
// 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")
|
||||
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
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
prodStarts = append(prodStarts, prod)
|
||||
|
||||
if !consMedia.MatchAll() {
|
||||
break producers
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stop producers if they don't have readers
|
||||
if s.pending.Add(-1) == 0 {
|
||||
s.stopProducers()
|
||||
}
|
||||
|
||||
if len(prodStarts) == 0 {
|
||||
return formatError(consMedias, prodMedias, prodErrors)
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.consumers = append(s.consumers, cons)
|
||||
s.mu.Unlock()
|
||||
|
||||
// there may be duplicates, but that's not a problem
|
||||
for _, prod := range prodStarts {
|
||||
prod.start()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error {
|
||||
// 1. Return errors if any not nil
|
||||
var text string
|
||||
|
||||
for _, err := range prodErrors {
|
||||
if err != nil {
|
||||
text = appendString(text, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if len(text) != 0 {
|
||||
return errors.New("streams: " + text)
|
||||
}
|
||||
|
||||
// 2. Return "codecs not matched"
|
||||
if prodMedias != nil {
|
||||
var prod, cons string
|
||||
|
||||
for _, media := range prodMedias {
|
||||
if media.Direction == core.DirectionRecvonly {
|
||||
for _, codec := range media.Codecs {
|
||||
prod = appendString(prod, codec.PrintName())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, media := range consMedias {
|
||||
if media.Direction == core.DirectionSendonly {
|
||||
for _, codec := range media.Codecs {
|
||||
cons = appendString(cons, codec.PrintName())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("streams: codecs not matched: " + prod + " => " + cons)
|
||||
}
|
||||
|
||||
// 3. Return unknown error
|
||||
return errors.New("streams: unknown error")
|
||||
}
|
||||
|
||||
func appendString(s, elem string) string {
|
||||
if strings.Contains(s, elem) {
|
||||
return s
|
||||
}
|
||||
if len(s) == 0 {
|
||||
return elem
|
||||
}
|
||||
return s + ", " + elem
|
||||
}
|
||||
175
pkg/streamserver/streams/dot.go
Normal file
175
pkg/streamserver/streams/dot.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package streams
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func AppendDOT(dot []byte, stream *Stream) []byte {
|
||||
for _, prod := range stream.producers {
|
||||
if prod.conn == nil {
|
||||
continue
|
||||
}
|
||||
c, err := marshalConn(prod.conn)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
dot = c.appendDOT(dot, "producer")
|
||||
}
|
||||
for _, cons := range stream.consumers {
|
||||
c, err := marshalConn(cons)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
dot = c.appendDOT(dot, "consumer")
|
||||
}
|
||||
return dot
|
||||
}
|
||||
|
||||
func marshalConn(v any) (*conn, error) {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var c conn
|
||||
if err = json.Unmarshal(b, &c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
const bytesK = "KMGTP"
|
||||
|
||||
func humanBytes(i int) string {
|
||||
if i < 1000 {
|
||||
return fmt.Sprintf("%d B", i)
|
||||
}
|
||||
|
||||
f := float64(i) / 1000
|
||||
var n uint8
|
||||
for f >= 1000 && n < 5 {
|
||||
f /= 1000
|
||||
n++
|
||||
}
|
||||
return fmt.Sprintf("%.2f %cB", f, bytesK[n])
|
||||
}
|
||||
|
||||
type node struct {
|
||||
ID uint32 `json:"id"`
|
||||
Codec map[string]any `json:"codec"`
|
||||
Parent uint32 `json:"parent"`
|
||||
Childs []uint32 `json:"childs"`
|
||||
Bytes int `json:"bytes"`
|
||||
//Packets uint32 `json:"packets"`
|
||||
//Drops uint32 `json:"drops"`
|
||||
}
|
||||
|
||||
var codecKeys = []string{"codec_name", "sample_rate", "channels", "profile", "level"}
|
||||
|
||||
func (n *node) name() string {
|
||||
if name, ok := n.Codec["codec_name"].(string); ok {
|
||||
return name
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func (n *node) codec() []byte {
|
||||
b := make([]byte, 0, 128)
|
||||
for _, k := range codecKeys {
|
||||
if v := n.Codec[k]; v != nil {
|
||||
b = fmt.Appendf(b, "%s=%v\n", k, v)
|
||||
}
|
||||
}
|
||||
if l := len(b); l > 0 {
|
||||
return b[:l-1]
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (n *node) appendDOT(dot []byte, group string) []byte {
|
||||
dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", n.ID, group, n.name(), n.codec())
|
||||
//for _, sink := range n.Childs {
|
||||
// dot = fmt.Appendf(dot, "%d -> %d;\n", n.ID, sink)
|
||||
//}
|
||||
return dot
|
||||
}
|
||||
|
||||
type conn struct {
|
||||
ID uint32 `json:"id"`
|
||||
FormatName string `json:"format_name"`
|
||||
Protocol string `json:"protocol"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
Source string `json:"source"`
|
||||
URL string `json:"url"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Receivers []node `json:"receivers"`
|
||||
Senders []node `json:"senders"`
|
||||
BytesRecv int `json:"bytes_recv"`
|
||||
BytesSend int `json:"bytes_send"`
|
||||
}
|
||||
|
||||
func (c *conn) appendDOT(dot []byte, group string) []byte {
|
||||
host := c.host()
|
||||
dot = fmt.Appendf(dot, "%s [group=host];\n", host)
|
||||
dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", c.ID, group, c.FormatName, c.label())
|
||||
if group == "producer" {
|
||||
dot = fmt.Appendf(dot, "%s -> %d [label=%q];\n", host, c.ID, humanBytes(c.BytesRecv))
|
||||
} else {
|
||||
dot = fmt.Appendf(dot, "%d -> %s [label=%q];\n", c.ID, host, humanBytes(c.BytesSend))
|
||||
}
|
||||
|
||||
for _, recv := range c.Receivers {
|
||||
dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", c.ID, recv.ID, humanBytes(recv.Bytes))
|
||||
dot = recv.appendDOT(dot, "node")
|
||||
}
|
||||
for _, send := range c.Senders {
|
||||
dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", send.Parent, c.ID, humanBytes(send.Bytes))
|
||||
//dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", send.ID, c.ID, humanBytes(send.Bytes))
|
||||
//dot = send.appendDOT(dot, "node")
|
||||
}
|
||||
return dot
|
||||
}
|
||||
|
||||
func (c *conn) host() (s string) {
|
||||
if c.Protocol == "pipe" {
|
||||
return "127.0.0.1"
|
||||
}
|
||||
|
||||
if s = c.RemoteAddr; s == "" {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
if i := strings.Index(s, "forwarded"); i > 0 {
|
||||
s = s[i+10:]
|
||||
}
|
||||
|
||||
if s[0] == '[' {
|
||||
if i := strings.Index(s, "]"); i > 0 {
|
||||
return s[1:i]
|
||||
}
|
||||
}
|
||||
|
||||
if i := strings.IndexAny(s, " ,:"); i > 0 {
|
||||
return s[:i]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *conn) label() string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("format_name=" + c.FormatName)
|
||||
if c.Protocol != "" {
|
||||
sb.WriteString("\nprotocol=" + c.Protocol)
|
||||
}
|
||||
if c.Source != "" {
|
||||
sb.WriteString("\nsource=" + c.Source)
|
||||
}
|
||||
if c.URL != "" {
|
||||
sb.WriteString("\nurl=" + c.URL)
|
||||
}
|
||||
if c.UserAgent != "" {
|
||||
sb.WriteString("\nuser_agent=" + c.UserAgent)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
92
pkg/streamserver/streams/handlers.go
Normal file
92
pkg/streamserver/streams/handlers.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package streams
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
type Handler func(source string) (core.Producer, error)
|
||||
|
||||
func (s *StreamHandler) HandleFunc(scheme string, handler Handler) {
|
||||
s.handlers[scheme] = handler
|
||||
}
|
||||
|
||||
func (s *StreamHandler) HasProducer(url string) bool {
|
||||
if i := strings.IndexByte(url, ':'); i > 0 {
|
||||
scheme := url[:i]
|
||||
|
||||
if _, ok := s.handlers[scheme]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, ok := s.redirects[scheme]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *StreamHandler) GetProducer(url string) (core.Producer, error) {
|
||||
if i := strings.IndexByte(url, ':'); i > 0 {
|
||||
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 handler, ok := s.handlers[scheme]; ok {
|
||||
return handler(url)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("streams: unsupported scheme: " + url)
|
||||
}
|
||||
|
||||
// Redirect can return: location URL or error or empty URL and error
|
||||
type Redirect func(url string) (string, error)
|
||||
|
||||
func (s *StreamHandler) RedirectFunc(scheme string, redirect Redirect) {
|
||||
s.redirects[scheme] = redirect
|
||||
}
|
||||
|
||||
func (s *StreamHandler) Location(url string) (string, error) {
|
||||
if i := strings.IndexByte(url, ':'); i > 0 {
|
||||
scheme := url[:i]
|
||||
|
||||
if redirect, ok := s.redirects[scheme]; ok {
|
||||
return redirect(url)
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// TODO: rework
|
||||
|
||||
type ConsumerHandler func(url string) (core.Consumer, 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) {
|
||||
if i := strings.IndexByte(url, ':'); i > 0 {
|
||||
scheme := url[:i]
|
||||
|
||||
if handler, ok := s.consumerHandlers[scheme]; ok {
|
||||
return handler(url)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil, errors.New("streams: unsupported scheme: " + url)
|
||||
}
|
||||
22
pkg/streamserver/streams/helpers.go
Normal file
22
pkg/streamserver/streams/helpers.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package streams
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ParseQuery(s string) url.Values {
|
||||
if len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
params := url.Values{}
|
||||
for _, key := range strings.Split(s, "#") {
|
||||
var value string
|
||||
i := strings.IndexByte(key, '=')
|
||||
if i > 0 {
|
||||
key, value = key[:i], key[i+1:]
|
||||
}
|
||||
params[key] = append(params[key], value)
|
||||
}
|
||||
return params
|
||||
}
|
||||
155
pkg/streamserver/streams/play.go
Normal file
155
pkg/streamserver/streams/play.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package streams
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func (s *Stream) Play(source string) error {
|
||||
s.mu.Lock()
|
||||
for _, producer := range s.producers {
|
||||
if producer.state == stateInternal && producer.conn != nil {
|
||||
_ = producer.conn.Stop()
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
if source == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var src core.Producer
|
||||
|
||||
for _, producer := range s.producers {
|
||||
if producer.conn == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
cons, ok := producer.conn.(core.Consumer)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if src == nil {
|
||||
var err error
|
||||
if src, err = s.streamHandler.GetProducer(source); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !matchMedia(src, cons) {
|
||||
continue
|
||||
}
|
||||
|
||||
s.AddInternalProducer(src)
|
||||
|
||||
go func() {
|
||||
_ = src.Start()
|
||||
s.RemoveProducer(src)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, producer := range s.producers {
|
||||
// start new client
|
||||
dst, err := s.streamHandler.GetProducer(producer.url)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// check if client support consumer interface
|
||||
cons, ok := dst.(core.Consumer)
|
||||
if !ok {
|
||||
_ = dst.Stop()
|
||||
continue
|
||||
}
|
||||
|
||||
// start new producer
|
||||
if src == nil {
|
||||
if src, err = s.streamHandler.GetProducer(source); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !matchMedia(src, cons) {
|
||||
_ = dst.Stop()
|
||||
continue
|
||||
}
|
||||
|
||||
s.AddInternalProducer(src)
|
||||
s.AddInternalConsumer(cons)
|
||||
|
||||
go func() {
|
||||
_ = dst.Start()
|
||||
_ = src.Stop()
|
||||
s.RemoveInternalConsumer(cons)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
_ = src.Start()
|
||||
// little timeout before stop dst, so the buffer can be transferred
|
||||
time.Sleep(time.Second)
|
||||
_ = dst.Stop()
|
||||
s.RemoveProducer(src)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("can't find consumer")
|
||||
}
|
||||
|
||||
func (s *Stream) AddInternalProducer(conn core.Producer) {
|
||||
producer := &Producer{conn: conn, state: stateInternal}
|
||||
s.mu.Lock()
|
||||
s.producers = append(s.producers, producer)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Stream) AddInternalConsumer(conn core.Consumer) {
|
||||
s.mu.Lock()
|
||||
s.consumers = append(s.consumers, conn)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Stream) RemoveInternalConsumer(conn core.Consumer) {
|
||||
s.mu.Lock()
|
||||
for i, consumer := range s.consumers {
|
||||
if consumer == conn {
|
||||
s.consumers = append(s.consumers[:i], s.consumers[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func matchMedia(prod core.Producer, cons core.Consumer) bool {
|
||||
for _, consMedia := range cons.GetMedias() {
|
||||
for _, prodMedia := range prod.GetMedias() {
|
||||
if prodMedia.Direction != core.DirectionRecvonly {
|
||||
continue
|
||||
}
|
||||
|
||||
prodCodec, consCodec := prodMedia.MatchMedia(consMedia)
|
||||
if prodCodec == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
track, err := prod.GetTrack(prodMedia, prodCodec)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err = cons.AddTrack(consMedia, consCodec, track); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
271
pkg/streamserver/streams/producer.go
Normal file
271
pkg/streamserver/streams/producer.go
Normal file
@@ -0,0 +1,271 @@
|
||||
package streams
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
)
|
||||
|
||||
type state byte
|
||||
|
||||
const (
|
||||
stateNone state = iota
|
||||
stateMedias
|
||||
stateTracks
|
||||
stateStart
|
||||
stateExternal
|
||||
stateInternal
|
||||
)
|
||||
|
||||
type Producer struct {
|
||||
core.Listener
|
||||
|
||||
url string
|
||||
template string
|
||||
|
||||
conn core.Producer
|
||||
receivers []*core.Receiver
|
||||
senders []*core.Receiver
|
||||
|
||||
state state
|
||||
mu sync.Mutex
|
||||
workerID int
|
||||
|
||||
streamHandler *StreamHandler
|
||||
}
|
||||
|
||||
const SourceTemplate = "{input}"
|
||||
|
||||
func (s *StreamHandler) NewProducer(source string) *Producer {
|
||||
if strings.Contains(source, SourceTemplate) {
|
||||
return &Producer{streamHandler: s, template: source}
|
||||
}
|
||||
|
||||
return &Producer{streamHandler: s, url: source}
|
||||
}
|
||||
|
||||
func (p *Producer) SetSource(s string) {
|
||||
if p.template == "" {
|
||||
p.url = s
|
||||
} else {
|
||||
p.url = strings.Replace(p.template, SourceTemplate, s, 1)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Producer) Dial() error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.state == stateNone {
|
||||
conn, err := p.streamHandler.GetProducer(p.url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.conn = conn
|
||||
p.state = stateMedias
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Producer) GetMedias() []*core.Media {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.conn == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return p.conn.GetMedias()
|
||||
}
|
||||
|
||||
func (p *Producer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.state == stateNone {
|
||||
return nil, errors.New("get track from none state")
|
||||
}
|
||||
|
||||
for _, track := range p.receivers {
|
||||
if track.Codec == codec {
|
||||
return track, nil
|
||||
}
|
||||
}
|
||||
|
||||
track, err := p.conn.GetTrack(media, codec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.receivers = append(p.receivers, track)
|
||||
|
||||
if p.state == stateMedias {
|
||||
p.state = stateTracks
|
||||
}
|
||||
|
||||
return track, nil
|
||||
}
|
||||
|
||||
func (p *Producer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.state == stateNone {
|
||||
return errors.New("add track from none state")
|
||||
}
|
||||
|
||||
if err := p.conn.(core.Consumer).AddTrack(media, codec, track); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.senders = append(p.senders, track)
|
||||
|
||||
if p.state == stateMedias {
|
||||
p.state = stateTracks
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Producer) MarshalJSON() ([]byte, error) {
|
||||
if conn := p.conn; conn != nil {
|
||||
return json.Marshal(conn)
|
||||
}
|
||||
info := map[string]string{"url": p.url}
|
||||
return json.Marshal(info)
|
||||
}
|
||||
|
||||
func (p *Producer) start() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.state != stateTracks {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Default().Debugf("[streams] start producer url=%s", p.url)
|
||||
|
||||
p.state = stateStart
|
||||
p.workerID++
|
||||
|
||||
go p.worker(p.conn, p.workerID)
|
||||
}
|
||||
|
||||
func (p *Producer) worker(conn core.Producer, workerID int) {
|
||||
if err := conn.Start(); err != nil {
|
||||
p.mu.Lock()
|
||||
closed := p.workerID != workerID
|
||||
p.mu.Unlock()
|
||||
|
||||
if closed {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Default().Warn(struct{ URL string }{URL: p.url}, err)
|
||||
}
|
||||
|
||||
p.reconnect(workerID, 0)
|
||||
}
|
||||
|
||||
func (p *Producer) reconnect(workerID, retry int) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.workerID != workerID {
|
||||
logger.Default().Tracef("[streams] stop reconnect url=%s", p.url)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Default().Debugf("[streams] retry=%d to url=%s", retry, p.url)
|
||||
|
||||
conn, err := p.streamHandler.GetProducer(p.url)
|
||||
if err != nil {
|
||||
logger.Default().Debugf("[streams] producer=%s", err)
|
||||
|
||||
timeout := time.Minute
|
||||
if retry < 5 {
|
||||
timeout = time.Second
|
||||
} else if retry < 10 {
|
||||
timeout = time.Second * 5
|
||||
} else if retry < 20 {
|
||||
timeout = time.Second * 10
|
||||
}
|
||||
|
||||
time.AfterFunc(timeout, func() {
|
||||
p.reconnect(workerID, retry+1)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
for _, media := range conn.GetMedias() {
|
||||
switch media.Direction {
|
||||
case core.DirectionRecvonly:
|
||||
for i, receiver := range p.receivers {
|
||||
codec := media.MatchCodec(receiver.Codec)
|
||||
if codec == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
track, err := conn.GetTrack(media, codec)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
receiver.Replace(track)
|
||||
p.receivers[i] = track
|
||||
break
|
||||
}
|
||||
|
||||
case core.DirectionSendonly:
|
||||
for _, sender := range p.senders {
|
||||
codec := media.MatchCodec(sender.Codec)
|
||||
if codec == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
_ = conn.(core.Consumer).AddTrack(media, codec, sender)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stop previous connection after moving tracks (fix ghost exec/ffmpeg)
|
||||
_ = p.conn.Stop()
|
||||
// swap connections
|
||||
p.conn = conn
|
||||
|
||||
go p.worker(conn, workerID)
|
||||
}
|
||||
|
||||
func (p *Producer) stop() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
switch p.state {
|
||||
case stateExternal:
|
||||
logger.Default().Tracef("[streams] skip stop external producer")
|
||||
return
|
||||
case stateNone:
|
||||
logger.Default().Tracef("[streams] skip stop none producer")
|
||||
return
|
||||
case stateStart:
|
||||
p.workerID++
|
||||
}
|
||||
|
||||
logger.Default().Tracef("[streams] stop producer url=%s", p.url)
|
||||
|
||||
if p.conn != nil {
|
||||
_ = p.conn.Stop()
|
||||
p.conn = nil
|
||||
}
|
||||
|
||||
p.state = stateNone
|
||||
p.receivers = nil
|
||||
p.senders = nil
|
||||
}
|
||||
69
pkg/streamserver/streams/publish.go
Normal file
69
pkg/streamserver/streams/publish.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package streams
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
)
|
||||
|
||||
func (s *Stream) Publish(
|
||||
ctx context.Context,
|
||||
url string,
|
||||
) (*StreamForwarding, error) {
|
||||
streamFwd := NewStreamForwarding(s.streamHandler)
|
||||
err := streamFwd.Start(ctx, s, url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to start stream forwarding to '%s': %w", url, err)
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.cleanup()
|
||||
s.forwardings = append(s.forwardings, streamFwd)
|
||||
return streamFwd, nil
|
||||
}
|
||||
|
||||
func (s *Stream) Cleanup() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.cleanup()
|
||||
}
|
||||
|
||||
func (s *Stream) cleanup() {
|
||||
c := make([]*StreamForwarding, 0, len(s.forwardings))
|
||||
for _, fwd := range s.forwardings {
|
||||
if fwd.IsClosed() {
|
||||
continue
|
||||
}
|
||||
c = append(c, fwd)
|
||||
}
|
||||
s.forwardings = c
|
||||
}
|
||||
|
||||
func (s *Stream) Forwardings() []*StreamForwarding {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.cleanup()
|
||||
c := make([]*StreamForwarding, 0, len(s.forwardings))
|
||||
c = append(c, s.forwardings...)
|
||||
return c
|
||||
}
|
||||
|
||||
func (s *StreamHandler) Publish(
|
||||
ctx context.Context,
|
||||
stream *Stream,
|
||||
destination any,
|
||||
) {
|
||||
switch v := destination.(type) {
|
||||
case string:
|
||||
if _, err := stream.Publish(ctx, v); err != nil {
|
||||
logger.Default().Error(err)
|
||||
}
|
||||
case []any:
|
||||
for _, v := range v {
|
||||
s.Publish(ctx, stream, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
131
pkg/streamserver/streams/stream.go
Normal file
131
pkg/streamserver/streams/stream.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package streams
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
)
|
||||
|
||||
type Stream struct {
|
||||
streamHandler *StreamHandler
|
||||
|
||||
producers []*Producer
|
||||
consumers []core.Consumer
|
||||
mu sync.Mutex
|
||||
pending atomic.Int32
|
||||
forwardings []*StreamForwarding
|
||||
}
|
||||
|
||||
func (s *StreamHandler) NewStream(source any) *Stream {
|
||||
switch source := source.(type) {
|
||||
case string:
|
||||
return &Stream{
|
||||
producers: []*Producer{s.NewProducer(source)},
|
||||
streamHandler: s,
|
||||
}
|
||||
case []any:
|
||||
stream := new(Stream)
|
||||
stream.streamHandler = s
|
||||
for _, src := range source {
|
||||
str, ok := src.(string)
|
||||
if !ok {
|
||||
logger.Default().Errorf("[stream] NewStream: Expected string, got %v", src)
|
||||
continue
|
||||
}
|
||||
stream.producers = append(stream.producers, s.NewProducer(str))
|
||||
}
|
||||
return stream
|
||||
case map[string]any:
|
||||
return s.NewStream(source["url"])
|
||||
case nil:
|
||||
stream := new(Stream)
|
||||
stream.streamHandler = s
|
||||
return stream
|
||||
default:
|
||||
panic(core.Caller())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Stream) Sources() (sources []string) {
|
||||
for _, prod := range s.producers {
|
||||
sources = append(sources, prod.url)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Stream) SetSource(source string) {
|
||||
for _, prod := range s.producers {
|
||||
prod.SetSource(source)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Stream) RemoveConsumer(cons core.Consumer) {
|
||||
_ = cons.Stop()
|
||||
|
||||
s.mu.Lock()
|
||||
for i, consumer := range s.consumers {
|
||||
if consumer == cons {
|
||||
s.consumers = append(s.consumers[:i], s.consumers[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
s.stopProducers()
|
||||
}
|
||||
|
||||
func (s *Stream) AddProducer(prod core.Producer) {
|
||||
producer := &Producer{conn: prod, state: stateExternal}
|
||||
s.mu.Lock()
|
||||
s.producers = append(s.producers, producer)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Stream) RemoveProducer(prod core.Producer) {
|
||||
s.mu.Lock()
|
||||
for i, producer := range s.producers {
|
||||
if producer.conn == prod {
|
||||
s.producers = append(s.producers[:i], s.producers[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Stream) stopProducers() {
|
||||
if s.pending.Load() > 0 {
|
||||
logger.Default().Tracef("[streams] skip stop pending producer")
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
producers:
|
||||
for _, producer := range s.producers {
|
||||
for _, track := range producer.receivers {
|
||||
if len(track.Senders()) > 0 {
|
||||
continue producers
|
||||
}
|
||||
}
|
||||
for _, track := range producer.senders {
|
||||
if len(track.Senders()) > 0 {
|
||||
continue producers
|
||||
}
|
||||
}
|
||||
producer.stop()
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Stream) MarshalJSON() ([]byte, error) {
|
||||
var info = struct {
|
||||
Producers []*Producer `json:"producers"`
|
||||
Consumers []core.Consumer `json:"consumers"`
|
||||
}{
|
||||
Producers: s.producers,
|
||||
Consumers: s.consumers,
|
||||
}
|
||||
return json.Marshal(info)
|
||||
}
|
||||
94
pkg/streamserver/streams/stream_forwarding.go
Normal file
94
pkg/streamserver/streams/stream_forwarding.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package streams
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/facebookincubator/go-belt/tool/experimental/errmon"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
)
|
||||
|
||||
type StreamForwarding struct {
|
||||
sync.Mutex
|
||||
Stream *Stream
|
||||
Consumer core.Consumer
|
||||
StreamHandler *StreamHandler
|
||||
CancelFunc context.CancelFunc
|
||||
URL string
|
||||
}
|
||||
|
||||
func NewStreamForwarding(streamHandler *StreamHandler) *StreamForwarding {
|
||||
return &StreamForwarding{StreamHandler: streamHandler}
|
||||
}
|
||||
|
||||
func (sf *StreamForwarding) Start(
|
||||
ctx context.Context,
|
||||
s *Stream,
|
||||
url string,
|
||||
) error {
|
||||
sf.Lock()
|
||||
defer sf.Unlock()
|
||||
|
||||
cons, 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
|
||||
|
||||
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() {
|
||||
err := run(ctx)
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
s.RemoveConsumer(cons)
|
||||
|
||||
// TODO: more smart retry
|
||||
time.Sleep(5 * time.Second)
|
||||
_, err = s.Publish(ctx, url)
|
||||
if err != nil {
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
err := sf.Close()
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sf *StreamForwarding) IsClosed() bool {
|
||||
sf.Lock()
|
||||
defer sf.Unlock()
|
||||
return sf.isClosed()
|
||||
}
|
||||
|
||||
func (sf *StreamForwarding) isClosed() bool {
|
||||
return sf.CancelFunc == nil
|
||||
}
|
||||
|
||||
func (sf *StreamForwarding) Close() error {
|
||||
sf.Lock()
|
||||
defer sf.Unlock()
|
||||
if sf.isClosed() {
|
||||
return nil
|
||||
}
|
||||
sf.CancelFunc()
|
||||
|
||||
var result *multierror.Error
|
||||
err := sf.Consumer.Stop()
|
||||
if err != nil {
|
||||
result = multierror.Append(result, err)
|
||||
}
|
||||
sf.Stream = nil
|
||||
sf.Consumer = nil
|
||||
sf.CancelFunc = nil
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
101
pkg/streamserver/streams/stream_handler.go
Normal file
101
pkg/streamserver/streams/stream_handler.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package streams
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func (s *StreamHandler) Get(name string) *Stream {
|
||||
return s.streams[name]
|
||||
}
|
||||
|
||||
var sanitize = regexp.MustCompile(`\s`)
|
||||
|
||||
// Validate - not allow creating dynamic streams with spaces in the source
|
||||
func (s *StreamHandler) Validate(source string) error {
|
||||
if sanitize.MatchString(source) {
|
||||
return errors.New("streams: invalid dynamic source")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StreamHandler) New(name string, source string) (*Stream, error) {
|
||||
if err := s.Validate(source); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stream := s.NewStream(source)
|
||||
s.streams[name] = stream
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
func (s *StreamHandler) CreateOrUpdate(name string, source string) (*Stream, error) {
|
||||
s.streamsMu.Lock()
|
||||
defer s.streamsMu.Unlock()
|
||||
|
||||
// check if source links to some stream name from go2rtc
|
||||
if u, err := url.Parse(source); err == nil && u.Scheme == "rtsp" && len(u.Path) > 1 {
|
||||
rtspName := u.Path[1:]
|
||||
if stream, ok := s.streams[rtspName]; ok {
|
||||
if s.streams[name] != stream {
|
||||
// link (alias) streams[name] to streams[rtspName]
|
||||
s.streams[name] = stream
|
||||
}
|
||||
return stream, nil
|
||||
}
|
||||
}
|
||||
|
||||
if stream, ok := s.streams[source]; ok {
|
||||
if name != source {
|
||||
// link (alias) streams[name] to streams[source]
|
||||
s.streams[name] = stream
|
||||
}
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
// check if src has supported scheme
|
||||
if !s.HasProducer(source) {
|
||||
return nil, fmt.Errorf("scheme is not supported")
|
||||
}
|
||||
|
||||
// check an existing stream with this name
|
||||
if stream, ok := s.streams[name]; ok {
|
||||
stream.SetSource(source)
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
// create new stream with this name
|
||||
return s.New(name, source)
|
||||
}
|
||||
|
||||
func (s *StreamHandler) GetAll() (names []string) {
|
||||
for name := range s.streams {
|
||||
names = append(names, name)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *StreamHandler) Delete(id string) {
|
||||
delete(s.streams, id)
|
||||
}
|
||||
|
||||
type StreamHandler struct {
|
||||
streams map[string]*Stream
|
||||
streamsMu sync.Mutex
|
||||
consumerHandlers map[string]ConsumerHandler
|
||||
handlers map[string]Handler
|
||||
redirects map[string]Redirect
|
||||
}
|
||||
|
||||
func NewStreamHandler() *StreamHandler {
|
||||
return &StreamHandler{
|
||||
streams: map[string]*Stream{},
|
||||
streamsMu: sync.Mutex{},
|
||||
consumerHandlers: map[string]ConsumerHandler{},
|
||||
handlers: map[string]Handler{},
|
||||
redirects: map[string]Redirect{},
|
||||
}
|
||||
}
|
||||
25
pkg/streamserver/types/types.go
Normal file
25
pkg/streamserver/types/types.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package types
|
||||
|
||||
import "io"
|
||||
|
||||
type ServerType int
|
||||
|
||||
const (
|
||||
ServerTypeUndefined = ServerType(iota)
|
||||
ServerTypeRTMP
|
||||
ServerTypeRTSP
|
||||
)
|
||||
|
||||
type ServerHandler interface {
|
||||
io.Closer
|
||||
|
||||
Type() ServerType
|
||||
ListenAddr() string
|
||||
}
|
||||
|
||||
type StreamDestination struct {
|
||||
StreamID StreamID
|
||||
URL string
|
||||
}
|
||||
|
||||
type StreamID string
|
||||
Reference in New Issue
Block a user