Initial commit, pt. 39

This commit is contained in:
Dmitrii Okunev
2024-07-07 21:43:01 +01:00
parent 3cb9359362
commit 708d156d96
9 changed files with 312 additions and 23 deletions

View File

@@ -25,5 +25,8 @@ streampanel-windows: builddir
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
builddir:
mkdir -p build

View File

@@ -1,9 +1,11 @@
package commands
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"os"
"github.com/facebookincubator/go-belt/tool/logger"
@@ -13,6 +15,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/client"
"github.com/xaionaro-go/streamctl/pkg/streampanel/consts"
)
var (
@@ -33,24 +36,49 @@ var (
},
}
Stream = &cobra.Command{
Use: "stream",
}
StreamSetup = &cobra.Command{
Use: "stream-setup",
Use: "setup",
Args: cobra.ExactArgs(0),
Run: streamSetup,
}
StreamStatus = &cobra.Command{
Use: "stream-status",
Use: "status",
Args: cobra.ExactArgs(0),
Run: streamStatus,
}
Variables = &cobra.Command{
Use: "variables",
}
VariablesGet = &cobra.Command{
Use: "get",
Args: cobra.ExactArgs(1),
Run: variablesGet,
}
VariablesSet = &cobra.Command{
Use: "set",
Args: cobra.ExactArgs(1),
Run: variablesSet,
}
LoggerLevel = logger.LevelWarning
)
func init() {
Root.AddCommand(StreamSetup)
Root.AddCommand(StreamStatus)
Root.AddCommand(Stream)
Stream.AddCommand(StreamSetup)
Stream.AddCommand(StreamStatus)
Root.AddCommand(Variables)
Variables.AddCommand(VariablesGet)
Variables.AddCommand(VariablesSet)
Root.PersistentFlags().Var(&LoggerLevel, "log-level", "")
Root.PersistentFlags().String("remote-addr", "localhost:3594", "the path to the config file")
@@ -132,3 +160,33 @@ func streamStatus(cmd *cobra.Command, args []string) {
fmt.Printf("%10s: %s\n", platID, statusJSON)
}
}
func variablesGet(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.GetVariable(ctx, consts.VarKey(variableKey))
assertNoError(ctx, err)
_, err = io.Copy(os.Stdout, bytes.NewReader(b))
assertNoError(ctx, err)
}
func variablesSet(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)
value, err := io.ReadAll(os.Stdin)
assertNoError(ctx, err)
err = streamD.SetVariable(ctx, consts.VarKey(variableKey), value)
assertNoError(ctx, err)
}

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE QtCreatorProject>
<!-- Written by QtDesignStudio 4.5.1, 2024-07-02T22:36:24. -->
<!-- Written by QtDesignStudio 4.5.1, 2024-07-07T20:06:09. -->
<qtcreator>
<data>
<variable>EnvironmentId</variable>

View File

@@ -18,9 +18,16 @@ func Screenshot(cfg Config) (*image.RGBA, error) {
return nil, fmt.Errorf("unable to screenshot screen %d: %w", cfg.DisplayID, err)
}
rgbaCropped, ok := rgbaFull.SubImage(cfg.Bounds).(*image.RGBA)
if !ok {
return nil, fmt.Errorf("got type %T, but expected %T", rgbaCropped, (*image.RGBA)(nil))
var rgbaCropped *image.RGBA
resizeTo := cfg.Bounds
if resizeTo.Max.X > 0 && resizeTo.Max.Y > 0 {
var ok bool
rgbaCropped, ok = rgbaFull.SubImage(resizeTo).(*image.RGBA)
if !ok {
return nil, fmt.Errorf("got type %T, but expected %T", rgbaCropped, (*image.RGBA)(nil))
}
} else {
rgbaCropped = rgbaFull
}
return rgbaCropped, nil

View File

@@ -22,6 +22,7 @@ import (
"github.com/xaionaro-go/streamctl/pkg/streamd/api"
"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 {
@@ -474,3 +475,31 @@ func (grpc *GRPCServer) SubscribeToOAuthRequests(
func (grpc *GRPCServer) OpenOAuthURL(authURL string) {
grpc.OAuthURLBroadcaster.Submit(authURL)
}
func (grpc *GRPCServer) GetVariable(
ctx context.Context,
req *streamd_grpc.GetVariableRequest,
) (*streamd_grpc.GetVariableReply, error) {
key := consts.VarKey(req.GetKey())
b, err := grpc.StreamD.GetVariable(ctx, key)
if err != nil {
return nil, fmt.Errorf("unable to get variable '%s': %w", key, err)
}
return &streamd_grpc.GetVariableReply{
Key: string(key),
Value: b,
}, nil
}
func (grpc *GRPCServer) SetVariable(
ctx context.Context,
req *streamd_grpc.SetVariableRequest,
) (*streamd_grpc.SetVariableReply, error) {
key := consts.VarKey(req.GetKey())
err := grpc.StreamD.SetVariable(ctx, key, req.GetValue())
if err != nil {
return nil, fmt.Errorf("unable to set variable '%s': %w", key, err)
}
return &streamd_grpc.SetVariableReply{}, nil
}

View File

@@ -10,6 +10,7 @@ type ImageID string
const (
ImageScreenshot = ImageID("screenshot")
ImageChat = ImageID("chat")
)
type VarKey string

View File

@@ -5,10 +5,13 @@ import (
"context"
"fmt"
"image"
"image/png"
"math"
"net/http"
"time"
"github.com/chai2010/webp"
"github.com/facebookincubator/go-belt/tool/logger"
"github.com/nfnt/resize"
"github.com/xaionaro-go/streamctl/pkg/screenshot"
"github.com/xaionaro-go/streamctl/pkg/screenshoter"
@@ -57,7 +60,18 @@ func (p *Panel) getImage(
return nil, fmt.Errorf("unable to get a screenshot: %w", err)
}
img, err := webp.Decode(bytes.NewReader(b))
mimeType := http.DetectContentType(b)
var img image.Image
err = nil
switch mimeType {
case "image/png":
img, err = png.Decode(bytes.NewReader(b))
case "image/webp":
img, err = webp.Decode(bytes.NewReader(b))
default:
return nil, fmt.Errorf("unexpected image type %s", mimeType)
}
if err != nil {
return nil, fmt.Errorf("unable to decode the screenshot: %w", err)
}
@@ -75,6 +89,17 @@ func (p *Panel) setScreenshot(
screenshot image.Image,
) {
bounds := screenshot.Bounds()
logger.Tracef(ctx, "screenshot bounds: %#+v", bounds)
if bounds.Max.X == 0 || bounds.Max.Y == 0 {
p.DisplayError(fmt.Errorf("received an empty screenshot"))
p.screenshoterLocker.Lock()
if p.screenshoterClose != nil {
p.screenshoterClose()
}
p.screenshoterLocker.Unlock()
return
}
if bounds.Max.X > ScreenshotMaxWidth || bounds.Max.Y > ScreenshotMaxHeight {
factor := 1.0
factor = math.Min(factor, float64(ScreenshotMaxWidth)/float64(bounds.Max.X))
@@ -82,12 +107,16 @@ func (p *Panel) setScreenshot(
newWidth := uint(float64(bounds.Max.X) * factor)
newHeight := uint(float64(bounds.Max.Y) * factor)
screenshot = resize.Resize(newWidth, newHeight, screenshot, resize.Lanczos3)
logger.Tracef(ctx, "rescaled the screenshot from %#+v to %#+v", bounds, screenshot.Bounds())
}
p.setImage(ctx, consts.VarKeyImage(consts.ImageScreenshot), screenshot)
}
func (p *Panel) reinitScreenshoter(ctx context.Context) {
logger.Debugf(ctx, "reinitScreenshoter")
defer logger.Debugf(ctx, "/reinitScreenshoter")
p.screenshoterLocker.Lock()
defer p.screenshoterLocker.Unlock()
if p.screenshoterClose != nil {

View File

@@ -0,0 +1,90 @@
package streampanel
import (
"context"
"time"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/layout"
"github.com/facebookincubator/go-belt/tool/logger"
"github.com/xaionaro-go/streamctl/pkg/streampanel/consts"
)
func (p *Panel) startMonitorPage(
ctx context.Context,
) {
logger.Debugf(ctx, "startMonitorPage")
defer logger.Debugf(ctx, "/startMonitorPage")
p.monitorPageUpdaterLocker.Lock()
defer p.monitorPageUpdaterLocker.Unlock()
ctx, cancelFn := context.WithCancel(ctx)
p.monitorPageUpdaterCancel = cancelFn
go func(ctx context.Context) {
p.updateMonitorPage(ctx)
t := time.NewTicker(time.Second)
for {
select {
case <-ctx.Done():
return
case <-t.C:
}
p.updateMonitorPage(ctx)
}
}(ctx)
}
func (p *Panel) stopMonitorPage(
ctx context.Context,
) {
logger.Debugf(ctx, "stopMonitorPage")
defer logger.Debugf(ctx, "/stopMonitorPage")
p.monitorPageUpdaterLocker.Lock()
defer p.monitorPageUpdaterLocker.Unlock()
if p.monitorPageUpdaterCancel == nil {
return
}
p.monitorPageUpdaterCancel()
p.monitorPageUpdaterCancel = nil
}
func (p *Panel) updateMonitorPage(
ctx context.Context,
) {
logger.Tracef(ctx, "updateMonitorPage")
defer logger.Tracef(ctx, "/updateMonitorPage")
{
img, err := p.getImage(ctx, consts.VarKeyImage(consts.ImageScreenshot))
if err != nil {
logger.Error(ctx, err)
} else {
imgFyne := canvas.NewImageFromImage(img)
imgFyne.FillMode = canvas.ImageFillOriginal
p.screenshotContainer.Layout = layout.NewBorderLayout(imgFyne, nil, nil, nil)
p.screenshotContainer.Objects = p.screenshotContainer.Objects[:0]
p.screenshotContainer.Objects = append(p.screenshotContainer.Objects, imgFyne)
p.screenshotContainer.Refresh()
}
}
{
img, err := p.getImage(ctx, consts.VarKeyImage(consts.ImageChat))
if err != nil {
logger.Error(ctx, err)
} else {
imgFyne := canvas.NewImageFromImage(img)
imgFyne.FillMode = canvas.ImageFillContain
p.chatContainer.RemoveAll()
p.chatContainer.Add(imgFyne)
p.chatContainer.Refresh()
}
}
}

View File

@@ -5,6 +5,7 @@ import (
"context"
_ "embed"
"fmt"
"image"
"net/url"
"os"
"os/exec"
@@ -19,8 +20,10 @@ import (
"fyne.io/fyne/v2"
fyneapp "fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/facebookincubator/go-belt"
@@ -72,6 +75,11 @@ type Panel struct {
streamTitleField *widget.Entry
streamDescriptionField *widget.Entry
monitorPageUpdaterLocker sync.Mutex
monitorPageUpdaterCancel context.CancelFunc
screenshotContainer *fyne.Container
chatContainer *fyne.Container
filterValue string
youtubeCheck *widget.Check
@@ -89,6 +97,13 @@ type Panel struct {
waitWindow fyne.Window
}
type Page string
const (
PageControl = Page("Control")
PageMonitor = Page("Monitor")
)
func New(
configPath string,
opts ...Option,
@@ -835,12 +850,19 @@ func (p *Panel) openSettingsWindow(ctx context.Context) error {
afterStopStreamCommandEntry := widget.NewEntry()
afterStopStreamCommandEntry.SetText(cmdAfterStopStream)
oldScreenshoterEnabled := p.Config.Screenshot.Enabled != nil && *p.Config.Screenshot.Enabled
cancelButton := widget.NewButtonWithIcon("Cancel", theme.CancelIcon(), func() {
w.Close()
})
saveButton := widget.NewButtonWithIcon("Save", theme.DocumentSaveIcon(), func() {
if err := p.SaveConfig(ctx); err != nil {
p.DisplayError(fmt.Errorf("unable to save the local config: %w", err))
} else {
newScreenshotEnabled := p.Config.Screenshot.Enabled != nil && *p.Config.Screenshot.Enabled
if oldScreenshoterEnabled != newScreenshotEnabled {
p.reinitScreenshoter(ctx)
}
}
obsCfg := cfg.Backends[obs.ID]
@@ -1125,7 +1147,7 @@ func (p *Panel) getUpdatedStatus_startStopStreamButton(ctx context.Context) {
obsIsEnabled, err := p.StreamD.IsBackendEnabled(ctx, obs.ID)
if err != nil {
logger.Errorf(ctx, "unable to check if OBS is enabled: %w", err)
logger.Error(ctx, fmt.Errorf("unable to check if OBS is enabled: %w", err))
return
}
if !obsIsEnabled {
@@ -1142,7 +1164,7 @@ func (p *Panel) getUpdatedStatus_startStopStreamButton(ctx context.Context) {
obsStreamStatus, err := p.StreamD.GetStreamStatus(ctx, obs.ID)
if err != nil {
logger.Errorf(ctx, "unable to get stream status from OBS: %w", err)
logger.Error(ctx, fmt.Errorf("unable to get stream status from OBS: %w", err))
return
}
logger.Tracef(ctx, "obsStreamStatus == %#+v", obsStreamStatus)
@@ -1173,7 +1195,7 @@ func (p *Panel) getUpdatedStatus_startStopStreamButton(ctx context.Context) {
ytIsEnabled, err := p.StreamD.IsBackendEnabled(ctx, youtube.ID)
if err != nil {
logger.Errorf(ctx, "unable to check if YouTube is enabled: %w", err)
logger.Error(ctx, fmt.Errorf("unable to check if YouTube is enabled: %w", err))
return
}
@@ -1184,7 +1206,7 @@ func (p *Panel) getUpdatedStatus_startStopStreamButton(ctx context.Context) {
ytStreamStatus, err := p.StreamD.GetStreamStatus(ctx, youtube.ID)
if err != nil {
logger.Errorf(ctx, "unable to get stream status from YouTube: %w", err)
logger.Error(ctx, fmt.Errorf("unable to get stream status from YouTube: %w", err))
return
}
logger.Tracef(ctx, "ytStreamStatus == %#+v", ytStreamStatus)
@@ -1238,8 +1260,7 @@ func (p *Panel) initMainWindow(ctx context.Context) {
p.openMenuWindow(ctx)
})
buttonPanel := container.NewHBox(
menuButton,
profileControl := container.NewHBox(
widget.NewSeparator(),
widget.NewRichTextWithText("Profile:"),
widget.NewButtonWithIcon("", theme.ContentAddIcon(), func() {
@@ -1247,16 +1268,16 @@ func (p *Panel) initMainWindow(ctx context.Context) {
}),
)
topPanel := container.NewHBox(
menuButton,
profileControl,
)
for _, button := range selectedProfileButtons {
button.Disable()
buttonPanel.Add(button)
profileControl.Add(button)
}
topPanel := container.NewVBox(
buttonPanel,
profileFilter,
)
p.setupStreamButton = widget.NewButtonWithIcon(setupStreamString(), theme.SettingsIcon(), func() {
p.onSetupStreamButton(ctx)
})
@@ -1328,12 +1349,63 @@ func (p *Panel) initMainWindow(ctx context.Context) {
),
)
w.SetContent(container.NewBorder(
topPanel,
controlPage := container.NewBorder(
profileFilter,
bottomPanel,
nil,
nil,
profilesList,
)
monitorBackground := image.NewGray(image.Rect(0, 0, 1, 1))
monitorBackgroundFyne := canvas.NewImageFromImage(monitorBackground)
monitorBackgroundFyne.FillMode = canvas.ImageFillStretch
p.screenshotContainer = container.NewBorder(nil, nil, nil, nil)
p.chatContainer = container.NewBorder(nil, nil, nil, nil)
monitorPage := container.NewStack(
monitorBackgroundFyne,
p.screenshotContainer,
p.chatContainer,
)
setPage := func(page Page) {
logger.Debugf(ctx, "setPage(%s)", page)
defer logger.Debugf(ctx, "/setPage(%s)", page)
if page != PageMonitor {
p.stopMonitorPage(ctx)
}
switch page {
case PageControl:
monitorPage.Hide()
profileControl.Show()
controlPage.Show()
case PageMonitor:
controlPage.Hide()
profileControl.Hide()
monitorPage.Show()
p.startMonitorPage(ctx)
}
}
pageSelector := widget.NewSelect(
[]string{"Control", "Monitor"},
func(page string) {
setPage(Page(page))
},
)
pageSelector.SetSelected(string(PageControl))
topPanel.Add(layout.NewSpacer())
topPanel.Add(pageSelector)
w.SetContent(container.NewBorder(
topPanel,
nil,
nil,
nil,
container.NewStack(controlPage, monitorPage),
))
w.Show()