Initial commit, pt. 22

This commit is contained in:
Dmitrii Okunev
2024-06-23 19:31:00 +01:00
parent 5da165f9f5
commit bbe098fdfb
11 changed files with 600 additions and 47 deletions

View File

@@ -20,7 +20,7 @@ streampanel-ios: builddir
cd cmd/streampanel && fyne package -release -os ios && mv streampanel.ipa ../../build/ cd cmd/streampanel && fyne package -release -os ios && mv streampanel.ipa ../../build/
streampanel-windows: builddir 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: builddir:
mkdir -p build mkdir -p build

9
go.mod
View File

@@ -21,6 +21,7 @@ require (
fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e // indirect fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v1.0.0 // 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/cloudflare/circl v1.3.7 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/davecgh/go-spew v1.1.1 // 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/go-text/typesetting v0.1.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gopherjs/gopherjs v1.17.2 // 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/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect
github.com/kevinburke/ssh_config v1.2.0 // 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/pjbgf/sha1cd v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // 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 cloud.google.com/go/compute/metadata v0.2.3 // indirect
fyne.io/fyne/v2 v2.4.5 fyne.io/fyne/v2 v2.4.5
github.com/DataDog/gostackparse v0.6.0 // indirect 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/fatih/color v1.10.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-git/go-git/v5 v5.12.0 github.com/go-git/go-git/v5 v5.12.0
@@ -98,7 +105,7 @@ require (
go.uber.org/zap v1.24.0 // indirect go.uber.org/zap v1.24.0 // indirect
golang.org/x/crypto v0.21.0 golang.org/x/crypto v0.21.0
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect 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/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect

15
go.sum
View File

@@ -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/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 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= 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 h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 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= 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/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 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/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/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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 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 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 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= 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/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/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= 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.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/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/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/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/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 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 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.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 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/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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/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/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 h1:Qkc/R0eCDdWtUmnczk2g03+mObPUfc49Kz2Bt4B5d0g=
github.com/nicklaw5/helix/v2 v2.26.0/go.mod h1:zZcKsyyBWDli34x3QleYsVMiiNGMXPAEU5NjsiZDtvY= 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 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 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.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= 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.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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=

View 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"`
}

View 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
}

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"reflect" "reflect"
"sync" "sync"
"time" "time"
@@ -50,11 +51,12 @@ func GetStreamProfile[T StreamProfile](
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to serialize: %w: %#+v", err, v) return nil, fmt.Errorf("unable to serialize: %w: %#+v", err, v)
} }
logger.Debugf(ctx, "JSON representation: <%s>", b)
err = json.Unmarshal(b, &profile) err = json.Unmarshal(b, &profile)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to deserialize: %w: <%s>", err, b) 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 return &profile, nil
} }
@@ -68,12 +70,14 @@ func ConvertStreamProfiles[T StreamProfile](
return err return err
} }
m[k] = *profile 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 return nil
} }
type StreamControllerCommons interface { type StreamControllerCommons interface {
io.Closer
SetTitle(ctx context.Context, title string) error SetTitle(ctx context.Context, title string) error
SetDescription(ctx context.Context, description string) error SetDescription(ctx context.Context, description string) error
InsertAdsCuePoint(ctx context.Context, ts time.Time, duration time.Duration) error InsertAdsCuePoint(ctx context.Context, ts time.Time, duration time.Duration) error
@@ -101,6 +105,10 @@ type abstractStreamController struct {
StreamProfileTypeValue reflect.Type StreamProfileTypeValue reflect.Type
} }
func (c *abstractStreamController) Close() error {
return c.StreamController.Close()
}
func (c *abstractStreamController) GetImplementation() StreamControllerCommons { func (c *abstractStreamController) GetImplementation() StreamControllerCommons {
return c.StreamController return c.StreamController
} }

View File

@@ -5,6 +5,8 @@ import (
"fmt" "fmt"
"strings" "strings"
"time" "time"
"unicode"
"unicode/utf8"
"github.com/facebookincubator/go-belt/tool/experimental/errmon" "github.com/facebookincubator/go-belt/tool/experimental/errmon"
"github.com/facebookincubator/go-belt/tool/logger" "github.com/facebookincubator/go-belt/tool/logger"
@@ -65,6 +67,10 @@ func getUserID(
return resp.Data.Users[0].ID, nil return resp.Data.Users[0].ID, nil
} }
func (t *Twitch) Close() error {
return nil
}
func (t *Twitch) editChannelInfo( func (t *Twitch) editChannelInfo(
ctx context.Context, ctx context.Context,
params *helix.EditChannelInformationParams, params *helix.EditChannelInformationParams,
@@ -88,6 +94,32 @@ type SaveProfileHandler interface {
SaveProfile(context.Context, StreamProfile) error 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( func (t *Twitch) ApplyProfile(
ctx context.Context, ctx context.Context,
profile StreamProfile, profile StreamProfile,
@@ -109,11 +141,11 @@ func (t *Twitch) ApplyProfile(
tags := make([]string, 0, len(profile.Tags)) tags := make([]string, 0, len(profile.Tags))
for _, tag := range profile.Tags { for _, tag := range profile.Tags {
tag = strings.ReplaceAll(tag, " ", "") tag = removeNonAlphanumeric(tag)
tag = strings.ReplaceAll(tag, "-", "")
if tag == "" { if tag == "" {
continue continue
} }
tag = truncateStringByByteLength(tag, 25) // see also: https://github.com/twitchdev/issues/issues/789
tags = append(tags, tag) tags = append(tags, tag)
} }

View File

@@ -27,6 +27,7 @@ const copyThumbnail = false
type YouTube struct { type YouTube struct {
YouTubeService *youtube.Service YouTubeService *youtube.Service
CancelFunc context.CancelFunc
} }
var _ streamcontrol.StreamController[StreamProfile] = (*YouTube)(nil) 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") 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 isNewToken := false
getNewToken := func() error { getNewToken := func() error {
t, err := getToken(ctx, cfg) t, err := getToken(ctx, cfg)
@@ -100,6 +103,7 @@ func New(
yt := &YouTube{ yt := &YouTube{
YouTubeService: youtubeService, YouTubeService: youtubeService,
CancelFunc: cancelFn,
} }
go func() { go func() {
@@ -159,6 +163,11 @@ func getToken(ctx context.Context, cfg Config) (*oauth2.Token, error) {
return tok, nil return tok, nil
} }
func (yt *YouTube) Close() error {
yt.CancelFunc()
return nil
}
func (yt *YouTube) iterateActiveBroadcasts( func (yt *YouTube) iterateActiveBroadcasts(
ctx context.Context, ctx context.Context,
callback func(broadcast *youtube.LiveBroadcast) error, callback func(broadcast *youtube.LiveBroadcast) error,

View File

@@ -12,9 +12,11 @@ import (
"runtime" "runtime"
"runtime/debug" "runtime/debug"
"sort" "sort"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"unicode"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
fyneapp "fyne.io/fyne/v2/app" fyneapp "fyne.io/fyne/v2/app"
@@ -26,6 +28,7 @@ import (
"github.com/go-ng/xmath" "github.com/go-ng/xmath"
"github.com/xaionaro-go/streamctl/pkg/oauthhandler" "github.com/xaionaro-go/streamctl/pkg/oauthhandler"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol" "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/twitch"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/youtube" "github.com/xaionaro-go/streamctl/pkg/streamcontrol/youtube"
) )
@@ -45,6 +48,7 @@ type Panel struct {
startStopMutex sync.Mutex startStopMutex sync.Mutex
updateTimerHandler *updateTimerHandler updateTimerHandler *updateTimerHandler
streamControllers struct { streamControllers struct {
OBS *obs.OBS
Twitch *twitch.Twitch Twitch *twitch.Twitch
YouTube *youtube.YouTube YouTube *youtube.YouTube
} }
@@ -62,6 +66,7 @@ type Panel struct {
dataPath string dataPath string
filterValue string filterValue string
obsCheck *widget.Check
youtubeCheck *widget.Check youtubeCheck *widget.Check
twitchCheck *widget.Check twitchCheck *widget.Check
@@ -297,6 +302,94 @@ func (p *Panel) savePlatformConfig(
return p.saveData(ctx) 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 { func (p *Panel) oauthHandlerTwitch(ctx context.Context, arg oauthhandler.OAuthHandlerArgument) error {
logger.Infof(ctx, "oauthHandlerTwitch: %#+v", arg) logger.Infof(ctx, "oauthHandlerTwitch: %#+v", arg)
defer logger.Infof(ctx, "/oauthHandlerTwitch") defer logger.Infof(ctx, "/oauthHandlerTwitch")
@@ -506,6 +599,8 @@ func (p *Panel) initStreamControllers(ctx context.Context) error {
for _, platName := range platNames { for _, platName := range platNames {
var err error var err error
switch strings.ToLower(string(platName)) { switch strings.ToLower(string(platName)) {
case strings.ToLower(string(obs.ID)):
err = p.initOBSBackend(ctx)
case strings.ToLower(string(twitch.ID)): case strings.ToLower(string(twitch.ID)):
err = p.initTwitchBackend(ctx) err = p.initTwitchBackend(ctx)
case strings.ToLower(string(youtube.ID)): 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 := 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 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("") twitchAlreadyLoggedIn := widget.NewLabel("")
if p.streamControllers.Twitch == nil { if p.streamControllers.Twitch == nil {
twitchAlreadyLoggedIn.SetText("(not logged in)") twitchAlreadyLoggedIn.SetText("(not logged in)")
@@ -819,8 +921,30 @@ func (p *Panel) openSettingsWindow(ctx context.Context) {
widget.NewSeparator(), widget.NewSeparator(),
widget.NewSeparator(), widget.NewSeparator(),
widget.NewRichTextFromMarkdown(`# Streaming platforms`), 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( container.NewHBox(
widget.NewButtonWithIcon("(Re-)login in Twitch", theme.LoginIcon(), func() { 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 oldEnable := p.data.Backends[twitch.ID].Enable
oldCfg := p.data.Backends[twitch.ID].Config oldCfg := p.data.Backends[twitch.ID].Config
p.data.Backends[twitch.ID].Enable = nil p.data.Backends[twitch.ID].Enable = nil
@@ -837,6 +961,9 @@ func (p *Panel) openSettingsWindow(ctx context.Context) {
), ),
container.NewHBox( container.NewHBox(
widget.NewButtonWithIcon("(Re-)login in YouTube", theme.LoginIcon(), func() { 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 oldEnable := p.data.Backends[youtube.ID].Enable
oldCfg := p.data.Backends[youtube.ID].Config oldCfg := p.data.Backends[youtube.ID].Config
p.data.Backends[youtube.ID].Config = youtube.PlatformSpecificConfig{} p.data.Backends[youtube.ID].Config = youtube.PlatformSpecificConfig{}
@@ -890,39 +1017,6 @@ func (p *Panel) openSettingsWindow(ctx context.Context) {
w.Show() 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) { func (p *Panel) resetCache(ctx context.Context) {
var wg sync.WaitGroup var wg sync.WaitGroup
@@ -1062,6 +1156,12 @@ func (p *Panel) initMainWindow(ctx context.Context) {
p.startStopButton.OnTapped() 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 = widget.NewCheck("Twitch", nil)
p.twitchCheck.SetChecked(true) p.twitchCheck.SetChecked(true)
if p.streamControllers.Twitch == nil { if p.streamControllers.Twitch == nil {
@@ -1081,7 +1181,7 @@ func (p *Panel) initMainWindow(ctx context.Context) {
container.NewBorder( container.NewBorder(
nil, nil,
nil, nil,
container.NewHBox(p.twitchCheck, p.youtubeCheck), container.NewHBox(p.obsCheck, p.twitchCheck, p.youtubeCheck),
nil, nil,
p.startStopButton, p.startStopButton,
), ),
@@ -1152,6 +1252,7 @@ func (p *Panel) startStream(ctx context.Context) {
return return
} }
p.obsCheck.Disable()
p.twitchCheck.Disable() p.twitchCheck.Disable()
p.youtubeCheck.Disable() p.youtubeCheck.Disable()
@@ -1164,10 +1265,20 @@ func (p *Panel) startStream(ctx context.Context) {
p.updateTimerHandler = newUpdateTimerHandler(p.startStopButton) p.updateTimerHandler = newUpdateTimerHandler(p.startStopButton)
profile := p.getSelectedProfile() 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 var twitchProfile *twitch.StreamProfile
if p.twitchCheck.Checked && p.streamControllers.Twitch != nil { if p.twitchCheck.Checked && p.streamControllers.Twitch != nil {
var err error 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 { if err != nil {
p.displayError(fmt.Errorf("unable to get the streaming profile for Twitch: %w", err)) p.displayError(fmt.Errorf("unable to get the streaming profile for Twitch: %w", err))
return return
@@ -1177,7 +1288,7 @@ func (p *Panel) startStream(ctx context.Context) {
var youtubeProfile *youtube.StreamProfile var youtubeProfile *youtube.StreamProfile
if p.youtubeCheck.Checked && p.streamControllers.YouTube != nil { if p.youtubeCheck.Checked && p.streamControllers.YouTube != nil {
var err error 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 { if err != nil {
p.displayError(fmt.Errorf("unable to get the streaming profile for YouTube: %w", err)) p.displayError(fmt.Errorf("unable to get the streaming profile for YouTube: %w", err))
return return
@@ -1186,7 +1297,7 @@ func (p *Panel) startStream(ctx context.Context) {
if p.twitchCheck.Checked && p.streamControllers.Twitch != nil { if p.twitchCheck.Checked && p.streamControllers.Twitch != nil {
err := p.streamControllers.Twitch.StartStream( err := p.streamControllers.Twitch.StartStream(
p.defaultContext, ctx,
p.streamTitleField.Text, p.streamTitleField.Text,
p.streamDescriptionField.Text, p.streamDescriptionField.Text,
*twitchProfile, *twitchProfile,
@@ -1198,7 +1309,7 @@ func (p *Panel) startStream(ctx context.Context) {
if p.youtubeCheck.Checked && p.streamControllers.YouTube != nil { if p.youtubeCheck.Checked && p.streamControllers.YouTube != nil {
err := p.streamControllers.YouTube.StartStream( err := p.streamControllers.YouTube.StartStream(
p.defaultContext, ctx,
p.streamTitleField.Text, p.streamTitleField.Text,
p.streamDescriptionField.Text, p.streamDescriptionField.Text,
*youtubeProfile, *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.execCommand(ctx, p.data.Commands.OnStartStream)
p.startStopButton.Refresh() p.startStopButton.Refresh()
@@ -1217,6 +1340,9 @@ func (p *Panel) stopStream(ctx context.Context) {
p.startStopMutex.Lock() p.startStopMutex.Lock()
defer p.startStopMutex.Unlock() defer p.startStopMutex.Unlock()
if p.streamControllers.OBS != nil {
p.obsCheck.Enable()
}
if p.streamControllers.Twitch != nil { if p.streamControllers.Twitch != nil {
p.twitchCheck.Enable() p.twitchCheck.Enable()
} }
@@ -1232,9 +1358,17 @@ func (p *Panel) stopStream(ctx context.Context) {
} }
p.updateTimerHandler = nil 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 { if p.streamControllers.Twitch != nil {
p.startStopButton.SetText("Stopping Twitch...") p.startStopButton.SetText("Stopping Twitch...")
err := p.streamControllers.Twitch.EndStream(p.defaultContext) err := p.streamControllers.Twitch.EndStream(ctx)
if err != nil { if err != nil {
p.displayError(fmt.Errorf("unable to stop the stream on Twitch: %w", err)) 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 { if p.streamControllers.YouTube != nil {
p.startStopButton.SetText("Stopping YouTube...") p.startStopButton.SetText("Stopping YouTube...")
err := p.streamControllers.YouTube.EndStream(p.defaultContext) err := p.streamControllers.YouTube.EndStream(ctx)
if err != nil { if err != nil {
p.displayError(fmt.Errorf("unable to stop the stream on YouTube: %w", err)) 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, commitFn func(context.Context, Profile) error,
) fyne.Window { ) fyne.Window {
var ( var (
obsProfile *obs.StreamProfile
twitchProfile *twitch.StreamProfile twitchProfile *twitch.StreamProfile
youtubeProfile *youtube.StreamProfile youtubeProfile *youtube.StreamProfile
) )
@@ -1517,6 +1652,22 @@ func (p *Panel) profileWindow(
var bottomContent []fyne.CanvasObject 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.NewSeparator())
bottomContent = append(bottomContent, widget.NewRichTextFromMarkdown("# Twitch:")) bottomContent = append(bottomContent, widget.NewRichTextFromMarkdown("# Twitch:"))
if p.streamControllers.Twitch != nil { if p.streamControllers.Twitch != nil {
@@ -1622,7 +1773,6 @@ func (p *Panel) profileWindow(
autoNumerateCheck := widget.NewCheck("Auto-numerate", func(b bool) { autoNumerateCheck := widget.NewCheck("Auto-numerate", func(b bool) {
youtubeProfile.AutoNumerate = b youtubeProfile.AutoNumerate = b
}) })
autoNumerateCheck.MouseOut()
autoNumerateCheck.SetChecked(youtubeProfile.AutoNumerate) 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]'.") 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)) bottomContent = append(bottomContent, container.NewHBox(autoNumerateCheck, autoNumerateHint))
@@ -1729,6 +1879,9 @@ func (p *Panel) profileWindow(
MaxOrder: 0, MaxOrder: 0,
}, },
} }
if obsProfile != nil {
profile.PerPlatform[obs.ID] = obsProfile
}
if twitchProfile != nil { if twitchProfile != nil {
for i := 0; i < len(twitchProfile.Tags); i++ { for i := 0; i < len(twitchProfile.Tags); i++ {
var v string var v string

View File

@@ -12,6 +12,7 @@ import (
"github.com/goccy/go-yaml" "github.com/goccy/go-yaml"
"github.com/nicklaw5/helix/v2" "github.com/nicklaw5/helix/v2"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol" "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/twitch"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/youtube" "github.com/xaionaro-go/streamctl/pkg/streamcontrol/youtube"
) )
@@ -53,6 +54,7 @@ type panelData struct {
func newPanelData() panelData { func newPanelData() panelData {
cfg := streamcontrol.Config{} cfg := streamcontrol.Config{}
obs.InitConfig(cfg)
twitch.InitConfig(cfg) twitch.InitConfig(cfg)
youtube.InitConfig(cfg) youtube.InitConfig(cfg)
return panelData{ return panelData{
@@ -63,6 +65,7 @@ func newPanelData() panelData {
func newSamplePanelData() panelData { func newSamplePanelData() panelData {
cfg := newPanelData() 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[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{}} cfg.Backends[youtube.ID].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{"some_profile": youtube.StreamProfile{}}
return cfg return cfg
@@ -97,6 +100,14 @@ func readPanelData(
cfg.Backends = streamcontrol.Config{} 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 { if cfg.Backends[twitch.ID] != nil {
err = streamcontrol.ConvertStreamProfiles[twitch.StreamProfile](ctx, cfg.Backends[twitch.ID].StreamProfiles) err = streamcontrol.ConvertStreamProfiles[twitch.StreamProfile](ctx, cfg.Backends[twitch.ID].StreamProfiles)
if err != nil { if err != nil {

View File

@@ -7,12 +7,69 @@ import (
"github.com/facebookincubator/go-belt/tool/logger" "github.com/facebookincubator/go-belt/tool/logger"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol" "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/twitch"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/youtube" "github.com/xaionaro-go/streamctl/pkg/streamcontrol/youtube"
) )
var ErrSkipBackend = errors.New("backend was skipped") 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( func newTwitch(
ctx context.Context, ctx context.Context,
cfg *streamcontrol.AbstractPlatformConfig, cfg *streamcontrol.AbstractPlatformConfig,
@@ -142,3 +199,52 @@ func newYouTube(
} }
return yt, nil 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
}