mirror of
https://github.com/xaionaro-go/streamctl.git
synced 2025-10-16 12:30:47 +08:00
Initial commit, pt. 22
This commit is contained in:
2
Makefile
2
Makefile
@@ -20,7 +20,7 @@ streampanel-ios: builddir
|
||||
cd cmd/streampanel && fyne package -release -os ios && mv streampanel.ipa ../../build/
|
||||
|
||||
streampanel-windows: builddir
|
||||
CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc GOOS=windows go build -o build/streampanel.exe ./cmd/streampanel/
|
||||
CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc GOOS=windows go build -ldflags "-H windowsgui" -o build/streampanel.exe ./cmd/streampanel/
|
||||
|
||||
builddir:
|
||||
mkdir -p build
|
||||
|
9
go.mod
9
go.mod
@@ -21,6 +21,7 @@ require (
|
||||
fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.0.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
@@ -37,9 +38,14 @@ require (
|
||||
github.com/go-text/typesetting v0.1.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||
github.com/gorilla/websocket v1.5.2 // indirect
|
||||
github.com/hashicorp/logutils v1.0.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mmcloughlin/profile v0.1.1 // indirect
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
@@ -65,6 +71,7 @@ require (
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
fyne.io/fyne/v2 v2.4.5
|
||||
github.com/DataDog/gostackparse v0.6.0 // indirect
|
||||
github.com/andreykaipov/goobs v1.4.1
|
||||
github.com/fatih/color v1.10.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-git/go-git/v5 v5.12.0
|
||||
@@ -98,7 +105,7 @@ require (
|
||||
go.uber.org/zap v1.24.0 // indirect
|
||||
golang.org/x/crypto v0.21.0
|
||||
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
|
||||
golang.org/x/net v0.22.0 // indirect
|
||||
golang.org/x/net v0.23.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
|
15
go.sum
15
go.sum
@@ -56,6 +56,8 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
|
||||
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/andreykaipov/goobs v1.4.1 h1:IpvSMVFzwsrN2d+h8pwMMHIAYiR4sVS2jSTYKNvNmA4=
|
||||
github.com/andreykaipov/goobs v1.4.1/go.mod h1:rjGZl9Y/O2axzrGwq9kL0l+ykKtRUYcNgPVh23YVwgc=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
@@ -68,6 +70,8 @@ github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLj
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
@@ -251,6 +255,8 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR
|
||||
github.com/gopherjs/gopherjs v0.0.0-20211219123610-ec9572f70e60/go.mod h1:cz9oNYuRUWGdHmLF2IodMLkAhcPtXeULvcBNagUrxTI=
|
||||
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
|
||||
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
|
||||
github.com/gorilla/websocket v1.5.2 h1:qoW6V1GT3aZxybsbC6oLnailWnB+qTMVwMreOso9XUw=
|
||||
github.com/gorilla/websocket v1.5.2/go.mod h1:0n9H61RBAcf5/38py2MCYbxzPIY9rOkpvvMT24Rqs30=
|
||||
github.com/goxjs/gl v0.0.0-20210104184919-e3fafc6f8f2a/go.mod h1:dy/f2gjY09hwVfIyATps4G2ai7/hLwLkc5TrPqONuXY=
|
||||
github.com/goxjs/glfw v0.0.0-20191126052801-d2efb5f20838/go.mod h1:oS8P8gVOT4ywTcjV6wZlOU4GuVFQ8F5328KY3MJ79CY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
@@ -273,6 +279,7 @@ github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
@@ -320,6 +327,10 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mmcloughlin/profile v0.1.1 h1:jhDmAqPyebOsVDOCICJoINoLb/AnLBaUw58nFzxWS2w=
|
||||
github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
@@ -327,6 +338,8 @@ github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJE
|
||||
github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/nicklaw5/helix/v2 v2.26.0 h1:Qkc/R0eCDdWtUmnczk2g03+mObPUfc49Kz2Bt4B5d0g=
|
||||
github.com/nicklaw5/helix/v2 v2.26.0/go.mod h1:zZcKsyyBWDli34x3QleYsVMiiNGMXPAEU5NjsiZDtvY=
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
|
||||
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
|
||||
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
@@ -541,6 +554,8 @@ golang.org/x/net v0.6.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.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
25
pkg/streamcontrol/obs/config.go
Normal file
25
pkg/streamcontrol/obs/config.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package obs
|
||||
|
||||
import (
|
||||
streamctl "github.com/xaionaro-go/streamctl/pkg/streamcontrol"
|
||||
)
|
||||
|
||||
const ID = streamctl.PlatformName("obs")
|
||||
|
||||
type PlatformSpecificConfig struct {
|
||||
Host string
|
||||
Port uint16
|
||||
Password string `yaml:"pass" json:"pass"`
|
||||
}
|
||||
|
||||
type Config = streamctl.PlatformConfig[PlatformSpecificConfig, StreamProfile]
|
||||
|
||||
func InitConfig(cfg streamctl.Config) {
|
||||
streamctl.InitConfig(cfg, ID, Config{})
|
||||
}
|
||||
|
||||
type StreamProfile struct {
|
||||
streamctl.StreamProfileBase `yaml:",omitempty,inline,alias"`
|
||||
|
||||
EnableRecording bool `yaml:"enable_recording" json:"enable_recording"`
|
||||
}
|
187
pkg/streamcontrol/obs/obs.go
Normal file
187
pkg/streamcontrol/obs/obs.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package obs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/andreykaipov/goobs"
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol"
|
||||
)
|
||||
|
||||
type OBS struct {
|
||||
Config Config
|
||||
CurrentStream struct {
|
||||
EnableRecording bool
|
||||
}
|
||||
}
|
||||
|
||||
var _ streamcontrol.StreamController[StreamProfile] = (*OBS)(nil)
|
||||
|
||||
func New(
|
||||
ctx context.Context,
|
||||
cfg Config,
|
||||
) (*OBS, error) {
|
||||
if cfg.Config.Host == "" {
|
||||
return nil, fmt.Errorf("'host' is not set")
|
||||
}
|
||||
if cfg.Config.Port == 0 {
|
||||
return nil, fmt.Errorf("'port' is not set")
|
||||
}
|
||||
|
||||
return &OBS{
|
||||
Config: cfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (obs *OBS) getClient() (*goobs.Client, error) {
|
||||
var opts []goobs.Option
|
||||
if obs.Config.Config.Password != "" {
|
||||
opts = append(opts, goobs.WithPassword(obs.Config.Config.Password))
|
||||
}
|
||||
return goobs.New(
|
||||
fmt.Sprintf("%s:%d", obs.Config.Config.Host, obs.Config.Config.Port),
|
||||
opts...,
|
||||
)
|
||||
}
|
||||
|
||||
func (obs *OBS) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (obs *OBS) ApplyProfile(
|
||||
ctx context.Context,
|
||||
profile StreamProfile,
|
||||
customArgs ...any,
|
||||
) error {
|
||||
return fmt.Errorf("not supported")
|
||||
}
|
||||
|
||||
func (obs *OBS) SetTitle(
|
||||
ctx context.Context,
|
||||
title string,
|
||||
) error {
|
||||
// So nothing to do here:
|
||||
return nil
|
||||
}
|
||||
|
||||
func (obs *OBS) SetDescription(
|
||||
ctx context.Context,
|
||||
description string,
|
||||
) error {
|
||||
// So nothing to do here:
|
||||
return nil
|
||||
}
|
||||
|
||||
func (obs *OBS) InsertAdsCuePoint(
|
||||
ctx context.Context,
|
||||
ts time.Time,
|
||||
duration time.Duration,
|
||||
) error {
|
||||
// So nothing to do here:
|
||||
return nil
|
||||
}
|
||||
|
||||
func (obs *OBS) Flush(
|
||||
ctx context.Context,
|
||||
) error {
|
||||
// So nothing to do here:
|
||||
return nil
|
||||
}
|
||||
|
||||
func (obs *OBS) StartStream(
|
||||
ctx context.Context,
|
||||
title string,
|
||||
description string,
|
||||
profile StreamProfile,
|
||||
customArgs ...any,
|
||||
) error {
|
||||
client, err := obs.getClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to initialize client to OBS: %w", err)
|
||||
}
|
||||
defer client.Disconnect()
|
||||
|
||||
streamStatus, err := client.Stream.GetStreamStatus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get current stream status: %w", err)
|
||||
}
|
||||
|
||||
recordingStarted := false
|
||||
if profile.EnableRecording {
|
||||
recordStatus, err := client.Record.GetRecordStatus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get current recording status: %w", err)
|
||||
}
|
||||
|
||||
if !recordStatus.OutputActive {
|
||||
_, recordStartErr := client.Record.StartRecord()
|
||||
if recordStartErr == nil {
|
||||
recordingStarted = true
|
||||
} else {
|
||||
err = multierror.Append(err, recordStartErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !streamStatus.OutputActive {
|
||||
_, streamStartErr := client.Stream.StartStream()
|
||||
if streamStartErr != nil {
|
||||
err = multierror.Append(err, streamStartErr)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if recordingStarted {
|
||||
_, e0 := client.Record.StopRecord()
|
||||
logger.Debugf(ctx, "StopRecord result: %v", e0)
|
||||
}
|
||||
_, e1 := client.Stream.StopStream()
|
||||
logger.Debugf(ctx, "StopStream result: %v", e1)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
obs.CurrentStream.EnableRecording = profile.EnableRecording
|
||||
return nil
|
||||
}
|
||||
|
||||
func (obs *OBS) EndStream(
|
||||
ctx context.Context,
|
||||
) error {
|
||||
client, err := obs.getClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to initialize client to OBS: %w", err)
|
||||
}
|
||||
defer client.Disconnect()
|
||||
|
||||
streamStatus, err := client.Stream.GetStreamStatus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get current stream status: %w", err)
|
||||
}
|
||||
|
||||
if obs.CurrentStream.EnableRecording {
|
||||
recordStatus, err := client.Record.GetRecordStatus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get current recording status: %w", err)
|
||||
}
|
||||
|
||||
if recordStatus.OutputActive {
|
||||
_, recordStopErr := client.Record.StopRecord()
|
||||
if recordStopErr != nil {
|
||||
err = multierror.Append(err, recordStopErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if streamStatus.OutputActive {
|
||||
_, streamStopErr := client.Stream.StopStream()
|
||||
if streamStopErr != nil {
|
||||
err = multierror.Append(err, streamStopErr)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -50,11 +51,12 @@ func GetStreamProfile[T StreamProfile](
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to serialize: %w: %#+v", err, v)
|
||||
}
|
||||
logger.Debugf(ctx, "JSON representation: <%s>", b)
|
||||
err = json.Unmarshal(b, &profile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to deserialize: %w: <%s>", err, b)
|
||||
}
|
||||
logger.Debugf(ctx, "converted %#+v to %#+v", v, profile)
|
||||
logger.Debugf(ctx, "converted %#+v (%s) to %#+v", v, v, profile)
|
||||
return &profile, nil
|
||||
}
|
||||
|
||||
@@ -68,12 +70,14 @@ func ConvertStreamProfiles[T StreamProfile](
|
||||
return err
|
||||
}
|
||||
m[k] = *profile
|
||||
logger.Debugf(ctx, "converted %#+v to %#+v", v, profile)
|
||||
logger.Debugf(ctx, "converted %#+v (%s) to %#+v", v, v, profile)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type StreamControllerCommons interface {
|
||||
io.Closer
|
||||
|
||||
SetTitle(ctx context.Context, title string) error
|
||||
SetDescription(ctx context.Context, description string) error
|
||||
InsertAdsCuePoint(ctx context.Context, ts time.Time, duration time.Duration) error
|
||||
@@ -101,6 +105,10 @@ type abstractStreamController struct {
|
||||
StreamProfileTypeValue reflect.Type
|
||||
}
|
||||
|
||||
func (c *abstractStreamController) Close() error {
|
||||
return c.StreamController.Close()
|
||||
}
|
||||
|
||||
func (c *abstractStreamController) GetImplementation() StreamControllerCommons {
|
||||
return c.StreamController
|
||||
}
|
||||
|
@@ -5,6 +5,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/facebookincubator/go-belt/tool/experimental/errmon"
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
@@ -65,6 +67,10 @@ func getUserID(
|
||||
return resp.Data.Users[0].ID, nil
|
||||
}
|
||||
|
||||
func (t *Twitch) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Twitch) editChannelInfo(
|
||||
ctx context.Context,
|
||||
params *helix.EditChannelInformationParams,
|
||||
@@ -88,6 +94,32 @@ type SaveProfileHandler interface {
|
||||
SaveProfile(context.Context, StreamProfile) error
|
||||
}
|
||||
|
||||
func removeNonAlphanumeric(input string) string {
|
||||
var builder strings.Builder
|
||||
for _, r := range input {
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
||||
builder.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// truncateStringByByteLength
|
||||
func truncateStringByByteLength(input string, byteLength int) string {
|
||||
byteSlice := []byte(input)
|
||||
|
||||
if len(byteSlice) <= byteLength {
|
||||
return input
|
||||
}
|
||||
|
||||
truncationPoint := byteLength
|
||||
for !utf8.Valid(byteSlice[:truncationPoint]) {
|
||||
truncationPoint--
|
||||
}
|
||||
|
||||
return string(byteSlice[:truncationPoint])
|
||||
}
|
||||
|
||||
func (t *Twitch) ApplyProfile(
|
||||
ctx context.Context,
|
||||
profile StreamProfile,
|
||||
@@ -109,11 +141,11 @@ func (t *Twitch) ApplyProfile(
|
||||
|
||||
tags := make([]string, 0, len(profile.Tags))
|
||||
for _, tag := range profile.Tags {
|
||||
tag = strings.ReplaceAll(tag, " ", "")
|
||||
tag = strings.ReplaceAll(tag, "-", "")
|
||||
tag = removeNonAlphanumeric(tag)
|
||||
if tag == "" {
|
||||
continue
|
||||
}
|
||||
tag = truncateStringByByteLength(tag, 25) // see also: https://github.com/twitchdev/issues/issues/789
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
|
||||
|
@@ -27,6 +27,7 @@ const copyThumbnail = false
|
||||
|
||||
type YouTube struct {
|
||||
YouTubeService *youtube.Service
|
||||
CancelFunc context.CancelFunc
|
||||
}
|
||||
|
||||
var _ streamcontrol.StreamController[StreamProfile] = (*YouTube)(nil)
|
||||
@@ -40,6 +41,8 @@ func New(
|
||||
return nil, fmt.Errorf("'clientid' or/and 'clientsecret' is/are not set; go to https://console.cloud.google.com/apis/credentials and create an app if it not created, yet")
|
||||
}
|
||||
|
||||
ctx, cancelFn := context.WithCancel(ctx)
|
||||
|
||||
isNewToken := false
|
||||
getNewToken := func() error {
|
||||
t, err := getToken(ctx, cfg)
|
||||
@@ -100,6 +103,7 @@ func New(
|
||||
|
||||
yt := &YouTube{
|
||||
YouTubeService: youtubeService,
|
||||
CancelFunc: cancelFn,
|
||||
}
|
||||
|
||||
go func() {
|
||||
@@ -159,6 +163,11 @@ func getToken(ctx context.Context, cfg Config) (*oauth2.Token, error) {
|
||||
return tok, nil
|
||||
}
|
||||
|
||||
func (yt *YouTube) Close() error {
|
||||
yt.CancelFunc()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (yt *YouTube) iterateActiveBroadcasts(
|
||||
ctx context.Context,
|
||||
callback func(broadcast *youtube.LiveBroadcast) error,
|
||||
|
@@ -12,9 +12,11 @@ import (
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
fyneapp "fyne.io/fyne/v2/app"
|
||||
@@ -26,6 +28,7 @@ import (
|
||||
"github.com/go-ng/xmath"
|
||||
"github.com/xaionaro-go/streamctl/pkg/oauthhandler"
|
||||
"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"
|
||||
)
|
||||
@@ -45,6 +48,7 @@ type Panel struct {
|
||||
startStopMutex sync.Mutex
|
||||
updateTimerHandler *updateTimerHandler
|
||||
streamControllers struct {
|
||||
OBS *obs.OBS
|
||||
Twitch *twitch.Twitch
|
||||
YouTube *youtube.YouTube
|
||||
}
|
||||
@@ -62,6 +66,7 @@ type Panel struct {
|
||||
dataPath string
|
||||
filterValue string
|
||||
|
||||
obsCheck *widget.Check
|
||||
youtubeCheck *widget.Check
|
||||
twitchCheck *widget.Check
|
||||
|
||||
@@ -297,6 +302,94 @@ func (p *Panel) savePlatformConfig(
|
||||
return p.saveData(ctx)
|
||||
}
|
||||
|
||||
func removeNonDigits(input string) string {
|
||||
var result []rune
|
||||
for _, r := range input {
|
||||
if unicode.IsDigit(r) {
|
||||
result = append(result, r)
|
||||
}
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func (p *Panel) inputOBSConnectInfo(
|
||||
ctx context.Context,
|
||||
cfg *streamcontrol.PlatformConfig[obs.PlatformSpecificConfig, obs.StreamProfile],
|
||||
) (bool, error) {
|
||||
w := p.app.NewWindow("Input Twitch user info")
|
||||
resizeWindow(w, fyne.NewSize(600, 200))
|
||||
|
||||
hostField := widget.NewEntry()
|
||||
hostField.SetPlaceHolder("OBS hostname, e.g. 192.168.0.134")
|
||||
portField := widget.NewEntry()
|
||||
portField.OnChanged = func(s string) {
|
||||
filtered := removeNonDigits(s)
|
||||
if s != filtered {
|
||||
portField.SetText(filtered)
|
||||
}
|
||||
}
|
||||
portField.SetPlaceHolder("OBS port, usually it is 4455")
|
||||
passField := widget.NewEntry()
|
||||
passField.SetPlaceHolder("OBS password")
|
||||
instructionText := widget.NewRichText(
|
||||
&widget.ListSegment{Items: []widget.RichTextSegment{
|
||||
&widget.TextSegment{Text: `Open OBS`},
|
||||
&widget.TextSegment{Text: `Click "Tools" on the top menu`},
|
||||
&widget.TextSegment{Text: `Select "WebSocket Server Settings"`},
|
||||
&widget.TextSegment{Text: `Check the "Enable WebSocket server" checkbox`},
|
||||
&widget.TextSegment{Text: `In the window click "Show Connect Info"`},
|
||||
&widget.TextSegment{Text: `Copy the data from the connect info to the fields above`},
|
||||
}},
|
||||
)
|
||||
instructionText.Wrapping = fyne.TextWrapWord
|
||||
|
||||
waitCh := make(chan struct{})
|
||||
skip := false
|
||||
skipButton := widget.NewButtonWithIcon("Skip", theme.ConfirmIcon(), func() {
|
||||
skip = true
|
||||
close(waitCh)
|
||||
})
|
||||
|
||||
var port uint64
|
||||
okButton := widget.NewButtonWithIcon("OK", theme.ConfirmIcon(), func() {
|
||||
var err error
|
||||
port, err = strconv.ParseUint(portField.Text, 10, 16)
|
||||
if err != nil {
|
||||
p.displayError(fmt.Errorf("unable to parse port '%s': %w", portField.Text, err))
|
||||
return
|
||||
}
|
||||
|
||||
close(waitCh)
|
||||
})
|
||||
|
||||
w.SetContent(container.NewBorder(
|
||||
widget.NewRichTextWithText("Enter OBS user info:"),
|
||||
container.NewHBox(skipButton, okButton),
|
||||
nil,
|
||||
nil,
|
||||
container.NewVBox(
|
||||
hostField,
|
||||
portField,
|
||||
passField,
|
||||
instructionText,
|
||||
),
|
||||
))
|
||||
w.Show()
|
||||
<-waitCh
|
||||
w.Hide()
|
||||
|
||||
if skip {
|
||||
cfg.Enable = ptr(false)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
cfg.Config.Host = hostField.Text
|
||||
cfg.Config.Port = uint16(port)
|
||||
cfg.Config.Password = passField.Text
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (p *Panel) oauthHandlerTwitch(ctx context.Context, arg oauthhandler.OAuthHandlerArgument) error {
|
||||
logger.Infof(ctx, "oauthHandlerTwitch: %#+v", arg)
|
||||
defer logger.Infof(ctx, "/oauthHandlerTwitch")
|
||||
@@ -506,6 +599,8 @@ func (p *Panel) initStreamControllers(ctx context.Context) error {
|
||||
for _, platName := range platNames {
|
||||
var err error
|
||||
switch strings.ToLower(string(platName)) {
|
||||
case strings.ToLower(string(obs.ID)):
|
||||
err = p.initOBSBackend(ctx)
|
||||
case strings.ToLower(string(twitch.ID)):
|
||||
err = p.initTwitchBackend(ctx)
|
||||
case strings.ToLower(string(youtube.ID)):
|
||||
@@ -793,6 +888,13 @@ func (p *Panel) openSettingsWindow(ctx context.Context) {
|
||||
templateInstruction := widget.NewRichTextFromMarkdown("Commands support [Go templates](https://pkg.go.dev/text/template) with two custom functions predefined:\n* `devnull` nullifies any inputs\n* `httpGET` makes an HTTP GET request and inserts the response body")
|
||||
templateInstruction.Wrapping = fyne.TextWrapWord
|
||||
|
||||
obsAlreadyLoggedIn := widget.NewLabel("")
|
||||
if p.streamControllers.OBS == nil {
|
||||
obsAlreadyLoggedIn.SetText("(not logged in)")
|
||||
} else {
|
||||
obsAlreadyLoggedIn.SetText("(already logged in)")
|
||||
}
|
||||
|
||||
twitchAlreadyLoggedIn := widget.NewLabel("")
|
||||
if p.streamControllers.Twitch == nil {
|
||||
twitchAlreadyLoggedIn.SetText("(not logged in)")
|
||||
@@ -819,8 +921,30 @@ func (p *Panel) openSettingsWindow(ctx context.Context) {
|
||||
widget.NewSeparator(),
|
||||
widget.NewSeparator(),
|
||||
widget.NewRichTextFromMarkdown(`# Streaming platforms`),
|
||||
container.NewHBox(
|
||||
widget.NewButtonWithIcon("(Re-)login in OBS", theme.LoginIcon(), func() {
|
||||
if p.data.Backends[obs.ID] == nil {
|
||||
obs.InitConfig(p.data.Backends)
|
||||
}
|
||||
oldEnable := p.data.Backends[obs.ID].Enable
|
||||
oldCfg := p.data.Backends[obs.ID].Config
|
||||
p.data.Backends[obs.ID].Enable = nil
|
||||
p.data.Backends[obs.ID].Config = obs.PlatformSpecificConfig{}
|
||||
err := p.initOBSBackend(ctx)
|
||||
if err != nil {
|
||||
p.displayError(err)
|
||||
p.data.Backends[obs.ID].Enable = oldEnable
|
||||
p.data.Backends[obs.ID].Config = oldCfg
|
||||
return
|
||||
}
|
||||
}),
|
||||
obsAlreadyLoggedIn,
|
||||
),
|
||||
container.NewHBox(
|
||||
widget.NewButtonWithIcon("(Re-)login in Twitch", theme.LoginIcon(), func() {
|
||||
if p.data.Backends[twitch.ID] == nil {
|
||||
twitch.InitConfig(p.data.Backends)
|
||||
}
|
||||
oldEnable := p.data.Backends[twitch.ID].Enable
|
||||
oldCfg := p.data.Backends[twitch.ID].Config
|
||||
p.data.Backends[twitch.ID].Enable = nil
|
||||
@@ -837,6 +961,9 @@ func (p *Panel) openSettingsWindow(ctx context.Context) {
|
||||
),
|
||||
container.NewHBox(
|
||||
widget.NewButtonWithIcon("(Re-)login in YouTube", theme.LoginIcon(), func() {
|
||||
if p.data.Backends[youtube.ID] == nil {
|
||||
youtube.InitConfig(p.data.Backends)
|
||||
}
|
||||
oldEnable := p.data.Backends[youtube.ID].Enable
|
||||
oldCfg := p.data.Backends[youtube.ID].Config
|
||||
p.data.Backends[youtube.ID].Config = youtube.PlatformSpecificConfig{}
|
||||
@@ -890,39 +1017,6 @@ func (p *Panel) openSettingsWindow(ctx context.Context) {
|
||||
w.Show()
|
||||
}
|
||||
|
||||
func (p *Panel) initTwitchBackend(ctx context.Context) error {
|
||||
twitch, err := newTwitch(
|
||||
ctx,
|
||||
p.data.Backends[twitch.ID],
|
||||
p.inputTwitchUserInfo,
|
||||
func(cfg *streamcontrol.AbstractPlatformConfig) error {
|
||||
return p.savePlatformConfig(ctx, twitch.ID, cfg)
|
||||
},
|
||||
p.oauthHandlerTwitch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.streamControllers.Twitch = twitch
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Panel) initYouTubeBackend(ctx context.Context) error {
|
||||
youTube, err := newYouTube(
|
||||
ctx,
|
||||
p.data.Backends[youtube.ID],
|
||||
p.inputYouTubeUserInfo,
|
||||
func(cfg *streamcontrol.AbstractPlatformConfig) error {
|
||||
return p.savePlatformConfig(ctx, youtube.ID, cfg)
|
||||
},
|
||||
p.oauthHandlerYouTube,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.streamControllers.YouTube = youTube
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Panel) resetCache(ctx context.Context) {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
@@ -1062,6 +1156,12 @@ func (p *Panel) initMainWindow(ctx context.Context) {
|
||||
p.startStopButton.OnTapped()
|
||||
}
|
||||
|
||||
p.obsCheck = widget.NewCheck("OBS", nil)
|
||||
p.obsCheck.SetChecked(true)
|
||||
if p.streamControllers.OBS == nil {
|
||||
p.obsCheck.SetChecked(false)
|
||||
p.obsCheck.Disable()
|
||||
}
|
||||
p.twitchCheck = widget.NewCheck("Twitch", nil)
|
||||
p.twitchCheck.SetChecked(true)
|
||||
if p.streamControllers.Twitch == nil {
|
||||
@@ -1081,7 +1181,7 @@ func (p *Panel) initMainWindow(ctx context.Context) {
|
||||
container.NewBorder(
|
||||
nil,
|
||||
nil,
|
||||
container.NewHBox(p.twitchCheck, p.youtubeCheck),
|
||||
container.NewHBox(p.obsCheck, p.twitchCheck, p.youtubeCheck),
|
||||
nil,
|
||||
p.startStopButton,
|
||||
),
|
||||
@@ -1152,6 +1252,7 @@ func (p *Panel) startStream(ctx context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
p.obsCheck.Disable()
|
||||
p.twitchCheck.Disable()
|
||||
p.youtubeCheck.Disable()
|
||||
|
||||
@@ -1164,10 +1265,20 @@ func (p *Panel) startStream(ctx context.Context) {
|
||||
p.updateTimerHandler = newUpdateTimerHandler(p.startStopButton)
|
||||
profile := p.getSelectedProfile()
|
||||
|
||||
var obsProfile *obs.StreamProfile
|
||||
if p.twitchCheck.Checked && p.streamControllers.Twitch != nil {
|
||||
var err error
|
||||
obsProfile, err = streamcontrol.GetStreamProfile[obs.StreamProfile](ctx, profile.PerPlatform[obs.ID])
|
||||
if err != nil {
|
||||
p.displayError(fmt.Errorf("unable to get the streaming profile for OBS: %w", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var twitchProfile *twitch.StreamProfile
|
||||
if p.twitchCheck.Checked && p.streamControllers.Twitch != nil {
|
||||
var err error
|
||||
twitchProfile, err = streamcontrol.GetStreamProfile[twitch.StreamProfile](p.defaultContext, profile.PerPlatform[twitch.ID])
|
||||
twitchProfile, err = streamcontrol.GetStreamProfile[twitch.StreamProfile](ctx, profile.PerPlatform[twitch.ID])
|
||||
if err != nil {
|
||||
p.displayError(fmt.Errorf("unable to get the streaming profile for Twitch: %w", err))
|
||||
return
|
||||
@@ -1177,7 +1288,7 @@ func (p *Panel) startStream(ctx context.Context) {
|
||||
var youtubeProfile *youtube.StreamProfile
|
||||
if p.youtubeCheck.Checked && p.streamControllers.YouTube != nil {
|
||||
var err error
|
||||
youtubeProfile, err = streamcontrol.GetStreamProfile[youtube.StreamProfile](p.defaultContext, profile.PerPlatform[youtube.ID])
|
||||
youtubeProfile, err = streamcontrol.GetStreamProfile[youtube.StreamProfile](ctx, profile.PerPlatform[youtube.ID])
|
||||
if err != nil {
|
||||
p.displayError(fmt.Errorf("unable to get the streaming profile for YouTube: %w", err))
|
||||
return
|
||||
@@ -1186,7 +1297,7 @@ func (p *Panel) startStream(ctx context.Context) {
|
||||
|
||||
if p.twitchCheck.Checked && p.streamControllers.Twitch != nil {
|
||||
err := p.streamControllers.Twitch.StartStream(
|
||||
p.defaultContext,
|
||||
ctx,
|
||||
p.streamTitleField.Text,
|
||||
p.streamDescriptionField.Text,
|
||||
*twitchProfile,
|
||||
@@ -1198,7 +1309,7 @@ func (p *Panel) startStream(ctx context.Context) {
|
||||
|
||||
if p.youtubeCheck.Checked && p.streamControllers.YouTube != nil {
|
||||
err := p.streamControllers.YouTube.StartStream(
|
||||
p.defaultContext,
|
||||
ctx,
|
||||
p.streamTitleField.Text,
|
||||
p.streamDescriptionField.Text,
|
||||
*youtubeProfile,
|
||||
@@ -1208,6 +1319,18 @@ func (p *Panel) startStream(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if p.obsCheck.Checked && p.streamControllers.OBS != nil {
|
||||
err := p.streamControllers.OBS.StartStream(
|
||||
ctx,
|
||||
p.streamTitleField.Text,
|
||||
p.streamDescriptionField.Text,
|
||||
*obsProfile,
|
||||
)
|
||||
if err != nil {
|
||||
p.displayError(fmt.Errorf("unable to start the stream on OBS: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
p.execCommand(ctx, p.data.Commands.OnStartStream)
|
||||
|
||||
p.startStopButton.Refresh()
|
||||
@@ -1217,6 +1340,9 @@ func (p *Panel) stopStream(ctx context.Context) {
|
||||
p.startStopMutex.Lock()
|
||||
defer p.startStopMutex.Unlock()
|
||||
|
||||
if p.streamControllers.OBS != nil {
|
||||
p.obsCheck.Enable()
|
||||
}
|
||||
if p.streamControllers.Twitch != nil {
|
||||
p.twitchCheck.Enable()
|
||||
}
|
||||
@@ -1232,9 +1358,17 @@ func (p *Panel) stopStream(ctx context.Context) {
|
||||
}
|
||||
p.updateTimerHandler = nil
|
||||
|
||||
if p.streamControllers.OBS != nil {
|
||||
p.startStopButton.SetText("Stopping OBS...")
|
||||
err := p.streamControllers.OBS.EndStream(ctx)
|
||||
if err != nil {
|
||||
p.displayError(fmt.Errorf("unable to stop the stream on OBS: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
if p.streamControllers.Twitch != nil {
|
||||
p.startStopButton.SetText("Stopping Twitch...")
|
||||
err := p.streamControllers.Twitch.EndStream(p.defaultContext)
|
||||
err := p.streamControllers.Twitch.EndStream(ctx)
|
||||
if err != nil {
|
||||
p.displayError(fmt.Errorf("unable to stop the stream on Twitch: %w", err))
|
||||
}
|
||||
@@ -1242,7 +1376,7 @@ func (p *Panel) stopStream(ctx context.Context) {
|
||||
|
||||
if p.streamControllers.YouTube != nil {
|
||||
p.startStopButton.SetText("Stopping YouTube...")
|
||||
err := p.streamControllers.YouTube.EndStream(p.defaultContext)
|
||||
err := p.streamControllers.YouTube.EndStream(ctx)
|
||||
if err != nil {
|
||||
p.displayError(fmt.Errorf("unable to stop the stream on YouTube: %w", err))
|
||||
}
|
||||
@@ -1385,6 +1519,7 @@ func (p *Panel) profileWindow(
|
||||
commitFn func(context.Context, Profile) error,
|
||||
) fyne.Window {
|
||||
var (
|
||||
obsProfile *obs.StreamProfile
|
||||
twitchProfile *twitch.StreamProfile
|
||||
youtubeProfile *youtube.StreamProfile
|
||||
)
|
||||
@@ -1517,6 +1652,22 @@ func (p *Panel) profileWindow(
|
||||
|
||||
var bottomContent []fyne.CanvasObject
|
||||
|
||||
bottomContent = append(bottomContent, widget.NewSeparator())
|
||||
bottomContent = append(bottomContent, widget.NewRichTextFromMarkdown("# OBS:"))
|
||||
if p.streamControllers.OBS != nil {
|
||||
if platProfile := values.PerPlatform[obs.ID]; platProfile != nil {
|
||||
obsProfile = ptr(streamcontrol.GetPlatformSpecificConfig[obs.StreamProfile](ctx, platProfile))
|
||||
} else {
|
||||
obsProfile = &obs.StreamProfile{}
|
||||
}
|
||||
|
||||
enableRecordingCheck := widget.NewCheck("Enable recording", func(b bool) {
|
||||
obsProfile.EnableRecording = b
|
||||
})
|
||||
enableRecordingCheck.SetChecked(obsProfile.EnableRecording)
|
||||
bottomContent = append(bottomContent, enableRecordingCheck)
|
||||
}
|
||||
|
||||
bottomContent = append(bottomContent, widget.NewSeparator())
|
||||
bottomContent = append(bottomContent, widget.NewRichTextFromMarkdown("# Twitch:"))
|
||||
if p.streamControllers.Twitch != nil {
|
||||
@@ -1622,7 +1773,6 @@ func (p *Panel) profileWindow(
|
||||
autoNumerateCheck := widget.NewCheck("Auto-numerate", func(b bool) {
|
||||
youtubeProfile.AutoNumerate = b
|
||||
})
|
||||
autoNumerateCheck.MouseOut()
|
||||
autoNumerateCheck.SetChecked(youtubeProfile.AutoNumerate)
|
||||
autoNumerateHint := NewHintWidget(w, "When enabled, it adds the number of the stream to the stream's title.\n\nFor example 'Watching presidential debate' -> 'Watching presidential debate [#52]'.")
|
||||
bottomContent = append(bottomContent, container.NewHBox(autoNumerateCheck, autoNumerateHint))
|
||||
@@ -1729,6 +1879,9 @@ func (p *Panel) profileWindow(
|
||||
MaxOrder: 0,
|
||||
},
|
||||
}
|
||||
if obsProfile != nil {
|
||||
profile.PerPlatform[obs.ID] = obsProfile
|
||||
}
|
||||
if twitchProfile != nil {
|
||||
for i := 0; i < len(twitchProfile.Tags); i++ {
|
||||
var v string
|
||||
|
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/nicklaw5/helix/v2"
|
||||
"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"
|
||||
)
|
||||
@@ -53,6 +54,7 @@ type panelData struct {
|
||||
|
||||
func newPanelData() panelData {
|
||||
cfg := streamcontrol.Config{}
|
||||
obs.InitConfig(cfg)
|
||||
twitch.InitConfig(cfg)
|
||||
youtube.InitConfig(cfg)
|
||||
return panelData{
|
||||
@@ -63,6 +65,7 @@ func newPanelData() panelData {
|
||||
|
||||
func newSamplePanelData() panelData {
|
||||
cfg := newPanelData()
|
||||
cfg.Backends[obs.ID].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{"some_profile": obs.StreamProfile{}}
|
||||
cfg.Backends[twitch.ID].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{"some_profile": twitch.StreamProfile{}}
|
||||
cfg.Backends[youtube.ID].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{"some_profile": youtube.StreamProfile{}}
|
||||
return cfg
|
||||
@@ -97,6 +100,14 @@ func readPanelData(
|
||||
cfg.Backends = streamcontrol.Config{}
|
||||
}
|
||||
|
||||
if cfg.Backends[obs.ID] != nil {
|
||||
err = streamcontrol.ConvertStreamProfiles[obs.StreamProfile](ctx, cfg.Backends[obs.ID].StreamProfiles)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to convert stream profiles of OBS: %w: <%s>", err, b)
|
||||
}
|
||||
logger.Debugf(ctx, "final stream profiles of OBS: %#+v", cfg.Backends[obs.ID].StreamProfiles)
|
||||
}
|
||||
|
||||
if cfg.Backends[twitch.ID] != nil {
|
||||
err = streamcontrol.ConvertStreamProfiles[twitch.StreamProfile](ctx, cfg.Backends[twitch.ID].StreamProfiles)
|
||||
if err != nil {
|
||||
|
@@ -7,12 +7,69 @@ import (
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
var ErrSkipBackend = errors.New("backend was skipped")
|
||||
|
||||
func newOBS(
|
||||
ctx context.Context,
|
||||
cfg *streamcontrol.AbstractPlatformConfig,
|
||||
setConnectionInfo func(context.Context, *streamcontrol.PlatformConfig[obs.PlatformSpecificConfig, obs.StreamProfile]) (bool, error),
|
||||
saveCfgFunc func(*streamcontrol.AbstractPlatformConfig) error,
|
||||
) (
|
||||
*obs.OBS,
|
||||
error,
|
||||
) {
|
||||
platCfg := streamcontrol.ConvertPlatformConfig[obs.PlatformSpecificConfig, obs.StreamProfile](
|
||||
ctx, cfg,
|
||||
)
|
||||
if platCfg == nil {
|
||||
return nil, fmt.Errorf("OBS config was not found")
|
||||
}
|
||||
|
||||
if cfg.Enable != nil && !*cfg.Enable {
|
||||
return nil, ErrSkipBackend
|
||||
}
|
||||
|
||||
hadSetNewConnectionInfo := false
|
||||
if platCfg.Config.Host == "" || platCfg.Config.Port == 0 {
|
||||
ok, err := setConnectionInfo(ctx, platCfg)
|
||||
if !ok {
|
||||
err := saveCfgFunc(&streamcontrol.AbstractPlatformConfig{
|
||||
Enable: platCfg.Enable,
|
||||
Config: platCfg.Config,
|
||||
StreamProfiles: streamcontrol.ToAbstractStreamProfiles(platCfg.StreamProfiles),
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(ctx, err)
|
||||
}
|
||||
return nil, ErrSkipBackend
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to set connection info: %w", err)
|
||||
}
|
||||
hadSetNewConnectionInfo = true
|
||||
}
|
||||
|
||||
logger.Debugf(ctx, "OBS config: %#+v", platCfg)
|
||||
cfg = streamcontrol.ToAbstractPlatformConfig(ctx, platCfg)
|
||||
obs, err := obs.New(ctx, *platCfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to initialize OBS client: %w", err)
|
||||
}
|
||||
if hadSetNewConnectionInfo {
|
||||
logger.Debugf(ctx, "confirmed new OBS connection info, saving it")
|
||||
if err := saveCfgFunc(cfg); err != nil {
|
||||
return nil, fmt.Errorf("unable to save the configuration: %w", err)
|
||||
}
|
||||
}
|
||||
return obs, nil
|
||||
|
||||
}
|
||||
|
||||
func newTwitch(
|
||||
ctx context.Context,
|
||||
cfg *streamcontrol.AbstractPlatformConfig,
|
||||
@@ -142,3 +199,52 @@ func newYouTube(
|
||||
}
|
||||
return yt, nil
|
||||
}
|
||||
|
||||
func (p *Panel) initOBSBackend(ctx context.Context) error {
|
||||
obs, err := newOBS(
|
||||
ctx,
|
||||
p.data.Backends[obs.ID],
|
||||
p.inputOBSConnectInfo,
|
||||
func(cfg *streamcontrol.AbstractPlatformConfig) error {
|
||||
return p.savePlatformConfig(ctx, obs.ID, cfg)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.streamControllers.OBS = obs
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Panel) initTwitchBackend(ctx context.Context) error {
|
||||
twitch, err := newTwitch(
|
||||
ctx,
|
||||
p.data.Backends[twitch.ID],
|
||||
p.inputTwitchUserInfo,
|
||||
func(cfg *streamcontrol.AbstractPlatformConfig) error {
|
||||
return p.savePlatformConfig(ctx, twitch.ID, cfg)
|
||||
},
|
||||
p.oauthHandlerTwitch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.streamControllers.Twitch = twitch
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Panel) initYouTubeBackend(ctx context.Context) error {
|
||||
youTube, err := newYouTube(
|
||||
ctx,
|
||||
p.data.Backends[youtube.ID],
|
||||
p.inputYouTubeUserInfo,
|
||||
func(cfg *streamcontrol.AbstractPlatformConfig) error {
|
||||
return p.savePlatformConfig(ctx, youtube.ID, cfg)
|
||||
},
|
||||
p.oauthHandlerYouTube,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.streamControllers.YouTube = youTube
|
||||
return nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user