mirror of
https://github.com/xaionaro-go/streamctl.git
synced 2025-09-26 19:41:17 +08:00
Optimize Dashboard images display
This commit is contained in:
@@ -63,3 +63,7 @@ Flags:
|
||||
|
||||
Use "/tmp/go-build2502186757/b001/exe/streamctl [command] --help" for more information about a command.
|
||||
```
|
||||
|
||||
# See also
|
||||
|
||||
* [OBS Blade](https://github.com/Kounex/obs_blade) is a quick and nice tool to control your OBS remotely.
|
5
go.mod
5
go.mod
@@ -11,7 +11,7 @@ replace github.com/andreykaipov/goobs v1.4.1 => github.com/xaionaro-go/goobs v0.
|
||||
|
||||
replace github.com/adrg/libvlc-go/v3 v3.1.5 => github.com/xaionaro-go/libvlc-go/v3 v3.0.0-20241011194409-0fe4e2a9d901
|
||||
|
||||
replace fyne.io/fyne/v2 v2.5.0 => github.com/xaionaro-go/fyne/v2 v2.0.0-20241020235352-fd61e4920f24
|
||||
replace fyne.io/fyne/v2 v2.5.4 => github.com/xaionaro-go/fyne/v2 v2.0.0-20250215180758-399edb421067
|
||||
|
||||
replace code.cloudfoundry.org/bytefmt => github.com/cloudfoundry/bytefmt v0.0.0-20211005130812-5bb3c17173e5
|
||||
|
||||
@@ -146,6 +146,7 @@ require (
|
||||
github.com/mmcloughlin/profile v0.1.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0 // indirect
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
@@ -219,7 +220,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
fyne.io/fyne/v2 v2.5.3
|
||||
fyne.io/fyne/v2 v2.5.4
|
||||
github.com/AgustinSRG/go-child-process-manager v1.0.1
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802
|
||||
github.com/abhinavxd/youtube-live-chat-downloader/v2 v2.0.3
|
||||
|
6
go.sum
6
go.sum
@@ -63,8 +63,6 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
fyne.io/fyne/v2 v2.5.3 h1:k6LjZx6EzRZhClsuzy6vucLZBstdH2USDGHSGWq8ly8=
|
||||
fyne.io/fyne/v2 v2.5.3/go.mod h1:0GOXKqyvNwk3DLmsFu9v0oYM0ZcD1ysGnlHCerKoAmo=
|
||||
fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
|
||||
fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||
github.com/AgustinSRG/go-child-process-manager v1.0.1 h1:wZpPE0LAXc5rzlU028CDt/K/Woc2IhgCV4/g+9x2WHo=
|
||||
@@ -686,6 +684,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/nicklaw5/helix/v2 v2.30.1-0.20240715193454-0151ccccf980 h1:PhRVFPI7eXzLajhhZuReQUy2cuViKqkASRKXV7o2G7I=
|
||||
github.com/nicklaw5/helix/v2 v2.30.1-0.20240715193454-0151ccccf980/go.mod h1:e1GsZq4NDk9sQlPJ0Nr3+14R9cizqg09VAk7/IonpOU=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
|
||||
@@ -936,6 +936,8 @@ github.com/xaionaro-go/audio v0.0.0-20250111142716-aa10611bd8a0 h1:m3CEneWBmYz2m
|
||||
github.com/xaionaro-go/audio v0.0.0-20250111142716-aa10611bd8a0/go.mod h1:vo8MOC0grCD/ZUSP06SxXJBeDukqBCC1uD2PHmEH9YM=
|
||||
github.com/xaionaro-go/datacounter v1.0.4 h1:+QMZLmu73R5WGkQfUPwlXF/JFN+Weo4iuDZkiL2wVm8=
|
||||
github.com/xaionaro-go/datacounter v1.0.4/go.mod h1:Sf9vBevuV6w5iE6K3qJ9pWVKcyS60clWBUSQLjt5++c=
|
||||
github.com/xaionaro-go/fyne/v2 v2.0.0-20250215180758-399edb421067 h1:58GgTbQcOCjv1ZZ46m6WQ8zqv0KEJe5C6D5Ls1oSHvc=
|
||||
github.com/xaionaro-go/fyne/v2 v2.0.0-20250215180758-399edb421067/go.mod h1:0GOXKqyvNwk3DLmsFu9v0oYM0ZcD1ysGnlHCerKoAmo=
|
||||
github.com/xaionaro-go/go-rtmp v0.0.0-20241009130244-1e3160f27f42 h1:izCjREd+62HDF9FRYqUI7dgJNdUxAIysEuqed8lBcDY=
|
||||
github.com/xaionaro-go/go-rtmp v0.0.0-20241009130244-1e3160f27f42/go.mod h1:IuQWd+hy/tLuvuqFX0N9SMZrzOprM8Jvvdu+42RJwk4=
|
||||
github.com/xaionaro-go/goobs v0.0.0-20241103210141-030e538ac440 h1:hzQ+65oWq54XAqheyJ9E6wt+WH75051w+eLP5zWlD68=
|
||||
|
@@ -8,23 +8,27 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/xaionaro-go/obs-grpc-proxy/protobuf/go/obs_grpc"
|
||||
"github.com/xaionaro-go/recoder"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamtypes"
|
||||
)
|
||||
|
||||
type DashboardSourceImageType string
|
||||
|
||||
const (
|
||||
DashboardSourceImageTypeUndefined = DashboardSourceImageType("")
|
||||
DashboardSourceImageTypeDummy = DashboardSourceImageType("dummy")
|
||||
DashboardSourceImageTypeOBSVideo = DashboardSourceImageType("obs_video") // rename to `obs_screenshot`
|
||||
DashboardSourceImageTypeOBSVolume = DashboardSourceImageType("obs_volume")
|
||||
DashboardSourceImageTypeUndefined = DashboardSourceImageType("")
|
||||
DashboardSourceImageTypeDummy = DashboardSourceImageType("dummy")
|
||||
DashboardSourceImageTypeStreamScreenshot = DashboardSourceImageType("stream_screenshot")
|
||||
DashboardSourceImageTypeOBSScreenshot = DashboardSourceImageType("obs_screenshot")
|
||||
DashboardSourceImageTypeOBSVolume = DashboardSourceImageType("obs_volume")
|
||||
)
|
||||
|
||||
func (mst DashboardSourceImageType) New() SourceImage {
|
||||
switch mst {
|
||||
case DashboardSourceImageTypeDummy:
|
||||
return &DashboardSourceImageDummy{}
|
||||
case DashboardSourceImageTypeOBSVideo:
|
||||
case DashboardSourceImageTypeStreamScreenshot:
|
||||
return &DashboardSourceImageStreamScreenshot{}
|
||||
case DashboardSourceImageTypeOBSScreenshot:
|
||||
return &DashboardSourceImageOBSScreenshot{}
|
||||
case DashboardSourceImageTypeOBSVolume:
|
||||
return &DashboardSourceImageOBSVolume{}
|
||||
@@ -42,14 +46,6 @@ const (
|
||||
ImageFormatWebP = ImageFormat("webp")
|
||||
)
|
||||
|
||||
type GetImageByteser interface {
|
||||
GetImageBytes(
|
||||
ctx context.Context,
|
||||
obsServer obs_grpc.OBSServer,
|
||||
el DashboardElementConfig,
|
||||
) ([]byte, string, time.Time, error)
|
||||
}
|
||||
|
||||
type Duration time.Duration
|
||||
|
||||
func (d Duration) MarshalJSON() ([]byte, error) {
|
||||
@@ -80,17 +76,30 @@ func (d *Duration) UnmarshalJSON(b []byte) error {
|
||||
|
||||
var _ SourceImage = (*DashboardSourceImageDummy)(nil)
|
||||
|
||||
type ImageDataProvider interface {
|
||||
GetOBSServer(context.Context) (obs_grpc.OBSServer, error)
|
||||
GetOBSState(context.Context) (*streamtypes.OBSState, error)
|
||||
GetCurrentStreamFrame(context.Context, streamtypes.StreamID) ([]byte, recoder.VideoCodec, error)
|
||||
}
|
||||
|
||||
type SourceImage interface {
|
||||
GetImage(
|
||||
ctx context.Context,
|
||||
obsServer obs_grpc.OBSServer,
|
||||
el DashboardElementConfig,
|
||||
obsState *streamtypes.OBSState,
|
||||
imageDataProvider ImageDataProvider,
|
||||
) (image.Image, time.Time, error)
|
||||
|
||||
SourceType() DashboardSourceImageType
|
||||
}
|
||||
|
||||
type GetImageBytes interface {
|
||||
GetImageBytes(
|
||||
ctx context.Context,
|
||||
el DashboardElementConfig,
|
||||
imageDataProvider ImageDataProvider,
|
||||
) ([]byte, string, time.Time, error)
|
||||
}
|
||||
|
||||
type serializableSourceImage struct {
|
||||
Type DashboardSourceImageType `yaml:"type"`
|
||||
Config map[string]any `yaml:"config,omitempty"`
|
||||
|
@@ -4,18 +4,14 @@ import (
|
||||
"context"
|
||||
"image"
|
||||
"time"
|
||||
|
||||
"github.com/xaionaro-go/obs-grpc-proxy/protobuf/go/obs_grpc"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamtypes"
|
||||
)
|
||||
|
||||
type DashboardSourceImageDummy struct{}
|
||||
|
||||
func (*DashboardSourceImageDummy) GetImage(
|
||||
ctx context.Context,
|
||||
obsServer obs_grpc.OBSServer,
|
||||
el DashboardElementConfig,
|
||||
obsState *streamtypes.OBSState,
|
||||
_ ImageDataProvider,
|
||||
) (image.Image, time.Time, error) {
|
||||
img := image.NewRGBA(image.Rectangle{
|
||||
Min: image.Point{
|
||||
|
27
pkg/streamd/config/dashboard_source_image_obs.go
Normal file
27
pkg/streamd/config/dashboard_source_image_obs.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"image"
|
||||
"time"
|
||||
|
||||
"github.com/xaionaro-go/obs-grpc-proxy/protobuf/go/obs_grpc"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamtypes"
|
||||
)
|
||||
|
||||
type GetImageBytesFromOBSer interface {
|
||||
GetImageBytesFromOBS(
|
||||
ctx context.Context,
|
||||
obsServer obs_grpc.OBSServer,
|
||||
el DashboardElementConfig,
|
||||
) ([]byte, string, time.Time, error)
|
||||
}
|
||||
|
||||
type GetImageFromOBSer interface {
|
||||
GetImageFromOBS(
|
||||
ctx context.Context,
|
||||
obsServer obs_grpc.OBSServer,
|
||||
el DashboardElementConfig,
|
||||
obsState *streamtypes.OBSState,
|
||||
) (image.Image, time.Time, error)
|
||||
}
|
@@ -26,20 +26,21 @@ type DashboardSourceImageOBSScreenshot struct {
|
||||
}
|
||||
|
||||
var _ SourceImage = (*DashboardSourceImageOBSScreenshot)(nil)
|
||||
var _ GetImageByteser = (*DashboardSourceImageOBSScreenshot)(nil)
|
||||
var _ GetImageFromOBSer = (*DashboardSourceImageOBSScreenshot)(nil)
|
||||
var _ GetImageBytesFromOBSer = (*DashboardSourceImageOBSScreenshot)(nil)
|
||||
|
||||
func (*DashboardSourceImageOBSScreenshot) SourceType() DashboardSourceImageType {
|
||||
return DashboardSourceImageTypeOBSVideo
|
||||
return DashboardSourceImageTypeOBSScreenshot
|
||||
}
|
||||
|
||||
func obsGetImage(
|
||||
ctx context.Context,
|
||||
getImageByteser GetImageByteser,
|
||||
getImageByteser GetImageBytesFromOBSer,
|
||||
obsServer obs_grpc.OBSServer,
|
||||
el DashboardElementConfig,
|
||||
_ *streamtypes.OBSState,
|
||||
) (image.Image, time.Time, error) {
|
||||
b, mimeType, nextUpdateTS, err := getImageByteser.GetImageBytes(ctx, obsServer, el)
|
||||
b, mimeType, nextUpdateTS, err := getImageByteser.GetImageBytesFromOBS(ctx, obsServer, el)
|
||||
if err != nil {
|
||||
return nil, nextUpdateTS, fmt.Errorf("unable to get the image from OBS: %w", err)
|
||||
}
|
||||
@@ -62,7 +63,7 @@ func obsGetImage(
|
||||
return img, nextUpdateTS, nil
|
||||
}
|
||||
|
||||
func (s *DashboardSourceImageOBSScreenshot) GetImage(
|
||||
func (s *DashboardSourceImageOBSScreenshot) GetImageFromOBS(
|
||||
ctx context.Context,
|
||||
obsServer obs_grpc.OBSServer,
|
||||
el DashboardElementConfig,
|
||||
@@ -71,7 +72,7 @@ func (s *DashboardSourceImageOBSScreenshot) GetImage(
|
||||
return obsGetImage(ctx, s, obsServer, el, obsState)
|
||||
}
|
||||
|
||||
func (s *DashboardSourceImageOBSScreenshot) GetImageBytes(
|
||||
func (s *DashboardSourceImageOBSScreenshot) GetImageBytesFromOBS(
|
||||
ctx context.Context,
|
||||
obsServer obs_grpc.OBSServer,
|
||||
el DashboardElementConfig,
|
||||
@@ -133,3 +134,31 @@ func (s *DashboardSourceImageOBSScreenshot) GetImageBytes(
|
||||
)
|
||||
return imgBytes, mimeType, time.Now().Add(time.Duration(s.UpdateInterval)), nil
|
||||
}
|
||||
|
||||
func (s *DashboardSourceImageOBSScreenshot) GetImageBytes(
|
||||
ctx context.Context,
|
||||
el DashboardElementConfig,
|
||||
dataProvider ImageDataProvider,
|
||||
) ([]byte, string, time.Time, error) {
|
||||
obsServer, err := dataProvider.GetOBSServer(ctx)
|
||||
if err != nil {
|
||||
return nil, "", time.Time{}, fmt.Errorf("unable to get the OBS server: %w", err)
|
||||
}
|
||||
return s.GetImageBytesFromOBS(ctx, obsServer, el)
|
||||
}
|
||||
|
||||
func (s *DashboardSourceImageOBSScreenshot) GetImage(
|
||||
ctx context.Context,
|
||||
el DashboardElementConfig,
|
||||
dataProvider ImageDataProvider,
|
||||
) (image.Image, time.Time, error) {
|
||||
obsServer, err := dataProvider.GetOBSServer(ctx)
|
||||
if err != nil {
|
||||
return nil, time.Time{}, fmt.Errorf("unable to get the OBS server: %w", err)
|
||||
}
|
||||
obsState, err := dataProvider.GetOBSState(ctx)
|
||||
if err != nil {
|
||||
return nil, time.Time{}, fmt.Errorf("unable to get the OBS state: %w", err)
|
||||
}
|
||||
return s.GetImageFromOBS(ctx, obsServer, el, obsState)
|
||||
}
|
||||
|
@@ -23,7 +23,7 @@ type DashboardSourceImageOBSVolume struct {
|
||||
|
||||
var _ SourceImage = (*DashboardSourceImageOBSVolume)(nil)
|
||||
|
||||
func (s *DashboardSourceImageOBSVolume) GetImage(
|
||||
func (s *DashboardSourceImageOBSVolume) GetImageFromOBS(
|
||||
ctx context.Context,
|
||||
obsServer obs_grpc.OBSServer,
|
||||
el DashboardElementConfig,
|
||||
@@ -94,3 +94,19 @@ func (s *DashboardSourceImageOBSVolume) GetImage(
|
||||
func (*DashboardSourceImageOBSVolume) SourceType() DashboardSourceImageType {
|
||||
return DashboardSourceImageTypeOBSVolume
|
||||
}
|
||||
|
||||
func (s *DashboardSourceImageOBSVolume) GetImage(
|
||||
ctx context.Context,
|
||||
el DashboardElementConfig,
|
||||
dataProvider ImageDataProvider,
|
||||
) (image.Image, time.Time, error) {
|
||||
obsServer, err := dataProvider.GetOBSServer(ctx)
|
||||
if err != nil {
|
||||
return nil, time.Time{}, fmt.Errorf("unable to get the OBS server: %w", err)
|
||||
}
|
||||
obsState, err := dataProvider.GetOBSState(ctx)
|
||||
if err != nil {
|
||||
return nil, time.Time{}, fmt.Errorf("unable to get the OBS state: %w", err)
|
||||
}
|
||||
return s.GetImageFromOBS(ctx, obsServer, el, obsState)
|
||||
}
|
||||
|
@@ -0,0 +1,37 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DashboardSourceImageStreamScreenshot struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Width float64 `yaml:"width" json:"width"`
|
||||
Height float64 `yaml:"height" json:"height"`
|
||||
UpdateInterval Duration `yaml:"update_interval" json:"update_interval"`
|
||||
}
|
||||
|
||||
var _ SourceImage = (*DashboardSourceImageOBSScreenshot)(nil)
|
||||
var _ GetImageBytesFromOBSer = (*DashboardSourceImageOBSScreenshot)(nil)
|
||||
|
||||
func (*DashboardSourceImageStreamScreenshot) SourceType() DashboardSourceImageType {
|
||||
return DashboardSourceImageTypeStreamScreenshot
|
||||
}
|
||||
|
||||
func (s *DashboardSourceImageStreamScreenshot) GetImage(
|
||||
ctx context.Context,
|
||||
el DashboardElementConfig,
|
||||
dp ImageDataProvider,
|
||||
) (image.Image, time.Time, error) {
|
||||
return nil, time.Time{}, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (s *DashboardSourceImageStreamScreenshot) GetImageBytes(
|
||||
ctx context.Context,
|
||||
el DashboardElementConfig,
|
||||
) ([]byte, string, time.Time, error) {
|
||||
return nil, "", time.Time{}, fmt.Errorf("not implemented")
|
||||
}
|
47
pkg/streamd/image_data_provider.go
Normal file
47
pkg/streamd/image_data_provider.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package streamd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/xaionaro-go/obs-grpc-proxy/protobuf/go/obs_grpc"
|
||||
"github.com/xaionaro-go/recoder"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/config"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamtypes"
|
||||
)
|
||||
|
||||
type imageDataProvider struct {
|
||||
OBSServer obs_grpc.OBSServer
|
||||
OBSState *OBSState
|
||||
}
|
||||
|
||||
var _ config.ImageDataProvider = (*imageDataProvider)(nil)
|
||||
|
||||
func newImageDataProvider(
|
||||
obsServer obs_grpc.OBSServer,
|
||||
obsState *OBSState,
|
||||
) *imageDataProvider {
|
||||
return &imageDataProvider{
|
||||
OBSServer: obsServer,
|
||||
OBSState: obsState,
|
||||
}
|
||||
}
|
||||
|
||||
func (img *imageDataProvider) GetOBSServer(
|
||||
ctx context.Context,
|
||||
) (obs_grpc.OBSServer, error) {
|
||||
return img.OBSServer, nil
|
||||
}
|
||||
|
||||
func (img *imageDataProvider) GetOBSState(
|
||||
ctx context.Context,
|
||||
) (*streamtypes.OBSState, error) {
|
||||
return img.OBSState, nil
|
||||
}
|
||||
|
||||
func (img *imageDataProvider) GetCurrentStreamFrame(
|
||||
ctx context.Context,
|
||||
streamID streamtypes.StreamID,
|
||||
) ([]byte, recoder.VideoCodec, error) {
|
||||
return nil, 0, fmt.Errorf("not implemented")
|
||||
}
|
174
pkg/streamd/image_taker.go
Normal file
174
pkg/streamd/image_taker.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package streamd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/chai2010/webp"
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
"github.com/xaionaro-go/observability"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/config"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/consts"
|
||||
"github.com/xaionaro-go/xsync"
|
||||
)
|
||||
|
||||
func (d *StreamD) getImageBytes(
|
||||
ctx context.Context,
|
||||
elName string,
|
||||
el config.DashboardElementConfig,
|
||||
dataProvider config.ImageDataProvider,
|
||||
) ([]byte, time.Time, error) {
|
||||
|
||||
src := el.Source
|
||||
if getImageByteser, ok := src.(config.GetImageBytes); ok {
|
||||
bytes, _, nextUpdateAt, err := getImageByteser.GetImageBytes(ctx, el, dataProvider)
|
||||
if err != nil {
|
||||
return nil, time.Now().Add(time.Second), fmt.Errorf("unable to get the image from the source using GetImageByteser: %w", err)
|
||||
}
|
||||
return bytes, nextUpdateAt, nil
|
||||
}
|
||||
|
||||
img, nextUpdateAt, err := el.Source.GetImage(ctx, el, dataProvider)
|
||||
if err != nil {
|
||||
return nil, time.Now().Add(time.Second), fmt.Errorf("unable to get the image from the source: %w", err)
|
||||
}
|
||||
|
||||
if imgHash, err := newImageHash(img); err == nil {
|
||||
if imgOldHash, ok := d.ImageHash.Swap(elName, imgHash); ok {
|
||||
if imgHash == imgOldHash {
|
||||
return nil, nextUpdateAt, ErrNotChanged
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, filter := range el.Filters {
|
||||
img = filter.Filter(ctx, img)
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
err = webp.Encode(&out, img, &webp.Options{
|
||||
Lossless: el.ImageLossless,
|
||||
Quality: float32(el.ImageQuality),
|
||||
Exact: false,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, time.Now().Add(time.Second), fmt.Errorf("unable to encode the image: %w", err)
|
||||
}
|
||||
|
||||
return out.Bytes(), nextUpdateAt, nil
|
||||
}
|
||||
|
||||
func (d *StreamD) initImageTaker(ctx context.Context) error {
|
||||
observability.Go(ctx, func() {
|
||||
defer logger.Debugf(ctx, "/imageTaker")
|
||||
ch, err := d.SubscribeToDashboardChanges(ctx)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "unable to subscribe to dashboard changes: %v", err)
|
||||
return
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ch:
|
||||
d.restartImageTaker(ctx)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return d.restartImageTaker(ctx)
|
||||
}
|
||||
|
||||
func (d *StreamD) restartImageTaker(ctx context.Context) error {
|
||||
return xsync.DoA1R1(ctx, &d.imageTakerLocker, d.restartImageTakerNoLock, ctx)
|
||||
}
|
||||
|
||||
func (d *StreamD) restartImageTakerNoLock(ctx context.Context) error {
|
||||
if d.imageTakerCancel != nil {
|
||||
d.imageTakerCancel()
|
||||
d.imageTakerCancel = nil
|
||||
d.imageTakerWG.Wait()
|
||||
}
|
||||
|
||||
ctx, cancelFn := context.WithCancel(ctx)
|
||||
d.imageTakerCancel = cancelFn
|
||||
|
||||
for elName, el := range d.Config.Dashboard.Elements {
|
||||
if el.Source == nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := el.Source.(*config.DashboardSourceImageDummy); ok {
|
||||
continue
|
||||
}
|
||||
{
|
||||
elName, el := elName, el
|
||||
_ = el
|
||||
d.imageTakerWG.Add(1)
|
||||
observability.Go(ctx, func() {
|
||||
defer d.imageTakerWG.Done()
|
||||
logger.Debugf(ctx, "taker of image '%s'", elName)
|
||||
defer logger.Debugf(ctx, "/taker of image '%s'", elName)
|
||||
|
||||
obsServer, obsServerClose, err := d.OBS(ctx)
|
||||
if obsServerClose != nil {
|
||||
defer obsServerClose()
|
||||
}
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "unable to init connection with OBS: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
imageDataProvider := newImageDataProvider(obsServer, &d.OBSState)
|
||||
|
||||
for {
|
||||
var (
|
||||
imgBytes []byte
|
||||
nextUpdateAt time.Time
|
||||
err error
|
||||
)
|
||||
|
||||
waitUntilNextIteration := func() bool {
|
||||
if nextUpdateAt.IsZero() {
|
||||
return false
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
case <-time.After(time.Until(nextUpdateAt)):
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
imgBytes, nextUpdateAt, err = d.getImageBytes(ctx, elName, el, imageDataProvider)
|
||||
if err != nil {
|
||||
if err != ErrNotChanged {
|
||||
logger.Tracef(ctx, "the image have not changed of '%s'", elName)
|
||||
} else {
|
||||
logger.Errorf(ctx, "unable to get the image of '%s': %v", elName, err)
|
||||
}
|
||||
if !waitUntilNextIteration() {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
err = d.SetVariable(ctx, consts.VarKeyImage(consts.ImageID(elName)), imgBytes)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "unable to save the image of '%s': %v", elName, err)
|
||||
if !waitUntilNextIteration() {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if !waitUntilNextIteration() {
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -1,22 +1,15 @@
|
||||
package streamd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/andreykaipov/goobs"
|
||||
"github.com/chai2010/webp"
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
"github.com/xaionaro-go/obs-grpc-proxy/pkg/obsgrpcproxy"
|
||||
"github.com/xaionaro-go/obs-grpc-proxy/protobuf/go/obs_grpc"
|
||||
"github.com/xaionaro-go/observability"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/obs"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/config"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamd/consts"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamtypes"
|
||||
"github.com/xaionaro-go/xsync"
|
||||
)
|
||||
|
||||
@@ -59,164 +52,6 @@ func (d *StreamD) OBS(
|
||||
|
||||
var ErrNotChanged = errors.New("not changed")
|
||||
|
||||
func (d *StreamD) getOBSImageBytes(
|
||||
ctx context.Context,
|
||||
obsServer obs_grpc.OBSServer,
|
||||
elName string,
|
||||
el config.DashboardElementConfig,
|
||||
obsState *streamtypes.OBSState,
|
||||
) ([]byte, time.Time, error) {
|
||||
|
||||
src := el.Source
|
||||
if getImageByteser, ok := src.(config.GetImageByteser); ok {
|
||||
bytes, _, nextUpdateAt, err := getImageByteser.GetImageBytes(ctx, obsServer, el)
|
||||
if err != nil {
|
||||
return nil, time.Now().Add(time.Second), fmt.Errorf("unable to get the image from the source using GetImageByteser: %w", err)
|
||||
}
|
||||
return bytes, nextUpdateAt, nil
|
||||
}
|
||||
|
||||
img, nextUpdateAt, err := el.Source.GetImage(ctx, obsServer, el, obsState)
|
||||
if err != nil {
|
||||
return nil, time.Now().Add(time.Second), fmt.Errorf("unable to get the image from the source: %w", err)
|
||||
}
|
||||
|
||||
if imgHash, err := newImageHash(img); err == nil {
|
||||
if imgOldHash, ok := d.ImageHash.Swap(elName, imgHash); ok {
|
||||
if imgHash == imgOldHash {
|
||||
return nil, nextUpdateAt, ErrNotChanged
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, filter := range el.Filters {
|
||||
img = filter.Filter(ctx, img)
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
err = webp.Encode(&out, img, &webp.Options{
|
||||
Lossless: el.ImageLossless,
|
||||
Quality: float32(el.ImageQuality),
|
||||
Exact: false,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, time.Now().Add(time.Second), fmt.Errorf("unable to encode the image: %w", err)
|
||||
}
|
||||
|
||||
return out.Bytes(), nextUpdateAt, nil
|
||||
}
|
||||
|
||||
func (d *StreamD) initImageTaker(ctx context.Context) error {
|
||||
observability.Go(ctx, func() {
|
||||
defer logger.Debugf(ctx, "/imageTaker")
|
||||
ch, err := d.SubscribeToDashboardChanges(ctx)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "unable to subscribe to dashboard changes: %v", err)
|
||||
return
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ch:
|
||||
d.restartImageTaker(ctx)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return d.restartImageTaker(ctx)
|
||||
}
|
||||
|
||||
func (d *StreamD) restartImageTaker(ctx context.Context) error {
|
||||
return xsync.DoA1R1(ctx, &d.imageTakerLocker, d.restartImageTakerNoLock, ctx)
|
||||
}
|
||||
|
||||
func (d *StreamD) restartImageTakerNoLock(ctx context.Context) error {
|
||||
if d.imageTakerCancel != nil {
|
||||
d.imageTakerCancel()
|
||||
d.imageTakerCancel = nil
|
||||
d.imageTakerWG.Wait()
|
||||
}
|
||||
|
||||
ctx, cancelFn := context.WithCancel(ctx)
|
||||
d.imageTakerCancel = cancelFn
|
||||
|
||||
for elName, el := range d.Config.Dashboard.Elements {
|
||||
if el.Source == nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := el.Source.(*config.DashboardSourceImageDummy); ok {
|
||||
continue
|
||||
}
|
||||
{
|
||||
elName, el := elName, el
|
||||
_ = el
|
||||
d.imageTakerWG.Add(1)
|
||||
observability.Go(ctx, func() {
|
||||
defer d.imageTakerWG.Done()
|
||||
logger.Debugf(ctx, "taker of image '%s'", elName)
|
||||
defer logger.Debugf(ctx, "/taker of image '%s'", elName)
|
||||
|
||||
obsServer, obsServerClose, err := d.OBS(ctx)
|
||||
if obsServerClose != nil {
|
||||
defer obsServerClose()
|
||||
}
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "unable to init connection with OBS: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
var (
|
||||
imgBytes []byte
|
||||
nextUpdateAt time.Time
|
||||
err error
|
||||
)
|
||||
|
||||
waitUntilNextIteration := func() bool {
|
||||
if nextUpdateAt.IsZero() {
|
||||
return false
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
case <-time.After(time.Until(nextUpdateAt)):
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
imgBytes, nextUpdateAt, err = d.getOBSImageBytes(ctx, obsServer, elName, el, &d.OBSState)
|
||||
if err != nil {
|
||||
if err != ErrNotChanged {
|
||||
logger.Tracef(ctx, "the image have not changed of '%s'", elName)
|
||||
} else {
|
||||
logger.Errorf(ctx, "unable to get the image of '%s': %v", elName, err)
|
||||
}
|
||||
if !waitUntilNextIteration() {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
err = d.SetVariable(ctx, consts.VarKeyImage(consts.ImageID(elName)), imgBytes)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "unable to save the image of '%s': %v", elName, err)
|
||||
if !waitUntilNextIteration() {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if !waitUntilNextIteration() {
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SceneElementIdentifier struct {
|
||||
Name *string
|
||||
UUID *string
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -35,6 +36,7 @@ import (
|
||||
streamdconfig "github.com/xaionaro-go/streamctl/pkg/streamd/config"
|
||||
streamdconsts "github.com/xaionaro-go/streamctl/pkg/streamd/consts"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streampanel/consts"
|
||||
"github.com/xaionaro-go/streamctl/pkg/ximage"
|
||||
xfyne "github.com/xaionaro-go/xfyne/widget"
|
||||
"github.com/xaionaro-go/xsync"
|
||||
)
|
||||
@@ -64,11 +66,20 @@ type dashboardWindow struct {
|
||||
lastWinSize fyne.Size
|
||||
lastOrientation fyne.DeviceOrientation
|
||||
screenshotContainer *fyne.Container
|
||||
layersContainer *fyne.Container
|
||||
imagesLocker xsync.RWMutex
|
||||
imagesLayerObj *canvas.Raster
|
||||
images []imageInfo
|
||||
renderedImagesLayer *image.NRGBA
|
||||
streamStatus map[streamcontrol.PlatformName]*widget.Label
|
||||
streamStatusLocker xsync.Mutex
|
||||
}
|
||||
|
||||
type imageInfo struct {
|
||||
ElementName string
|
||||
streamdconfig.DashboardElementConfig
|
||||
Image *image.NRGBA
|
||||
}
|
||||
|
||||
func (w *dashboardWindow) renderStreamStatus(ctx context.Context) {
|
||||
w.streamStatusLocker.Do(ctx, func() {
|
||||
streamDClient, ok := w.StreamD.(*client.Client)
|
||||
@@ -187,18 +198,198 @@ func (p *Panel) newDashboardWindow(
|
||||
nil,
|
||||
streamInfoItems,
|
||||
)
|
||||
w.layersContainer = container.NewStack()
|
||||
w.imagesLayerObj = canvas.NewRaster(w.imagesLayer)
|
||||
|
||||
w.Window.SetContent(container.NewStack(
|
||||
bgFyne,
|
||||
w.screenshotContainer,
|
||||
w.layersContainer,
|
||||
w.imagesLayerObj,
|
||||
streamInfoContainer,
|
||||
))
|
||||
w.Window.Show()
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *dashboardWindow) imagesLayer(width, height int) (_ret image.Image) {
|
||||
ctx := context.TODO()
|
||||
logger.Tracef(ctx, "imagesLayer(%d, %d)", width, height)
|
||||
defer func() { logger.Tracef(ctx, "/imagesLayer(%d, %d): size:%v", width, height, _ret.Bounds()) }()
|
||||
return xsync.DoR1(xsync.WithNoLogging(ctx, true), &w.imagesLocker, func() image.Image {
|
||||
return w.renderImagesNoLock(ctx, width, height)
|
||||
})
|
||||
}
|
||||
|
||||
func (w *dashboardWindow) renderImagesNoLock(
|
||||
ctx context.Context,
|
||||
width, height int,
|
||||
) image.Image {
|
||||
if w.renderedImagesLayer == nil {
|
||||
w.renderedImagesLayer = image.NewNRGBA(image.Rectangle{
|
||||
Min: image.Point{},
|
||||
Max: image.Point{
|
||||
X: width,
|
||||
Y: height,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
dstImg := w.renderedImagesLayer
|
||||
size := dstImg.Bounds().Size()
|
||||
if size.X != width || size.Y != height {
|
||||
w.renderedImagesLayer = image.NewNRGBA(image.Rectangle{
|
||||
Min: image.Point{},
|
||||
Max: image.Point{
|
||||
X: width,
|
||||
Y: height,
|
||||
},
|
||||
})
|
||||
dstImg = w.renderedImagesLayer
|
||||
size = dstImg.Bounds().Size()
|
||||
}
|
||||
|
||||
canvasRatio := float64(size.X) / float64(size.Y)
|
||||
|
||||
transformedImages := make([]*ximage.Transform, 0, len(w.images))
|
||||
for idx, img := range w.images {
|
||||
if img.Image == nil {
|
||||
continue
|
||||
}
|
||||
imgSize := img.Image.Bounds().Size()
|
||||
imgRatio := float64(imgSize.X) / float64(imgSize.Y)
|
||||
imgCanvasRatio := imgRatio / canvasRatio
|
||||
imgWidth := img.Width
|
||||
imgHeight := img.Height
|
||||
switch {
|
||||
case imgCanvasRatio == 1:
|
||||
case imgCanvasRatio > 1:
|
||||
imgHeight /= imgCanvasRatio
|
||||
case imgCanvasRatio < 1:
|
||||
imgWidth *= imgCanvasRatio
|
||||
}
|
||||
var xMin, yMin float64
|
||||
switch img.AlignX {
|
||||
case streamdconsts.AlignXLeft:
|
||||
xMin = img.OffsetX
|
||||
case streamdconsts.AlignXMiddle:
|
||||
xMin = (100-imgWidth)/2 + img.OffsetX
|
||||
case streamdconsts.AlignXRight:
|
||||
xMin = (100 - imgWidth) + img.OffsetX
|
||||
}
|
||||
xMax := xMin + imgWidth
|
||||
switch img.AlignY {
|
||||
case streamdconsts.AlignYTop:
|
||||
yMin = img.OffsetY
|
||||
case streamdconsts.AlignYMiddle:
|
||||
yMin = (100-imgHeight)/2 + img.OffsetY
|
||||
case streamdconsts.AlignYBottom:
|
||||
xMin = (100 - imgHeight) + img.OffsetY
|
||||
}
|
||||
yMax := yMin + imgHeight
|
||||
rectangle := ximage.RectangleFloat64{
|
||||
Min: ximage.PointFloat64{
|
||||
X: xMin / 100,
|
||||
Y: yMin / 100,
|
||||
},
|
||||
Max: ximage.PointFloat64{
|
||||
X: xMax / 100,
|
||||
Y: yMax / 100,
|
||||
},
|
||||
}
|
||||
logger.Tracef(ctx, "transformation rectangle for %d (%#+v) is %v", idx, img.DashboardElementConfig, rectangle)
|
||||
transformedImages = append(transformedImages, ximage.NewTransform(img.Image, color.NRGBAModel, rectangle))
|
||||
}
|
||||
for idx := range dstImg.Pix {
|
||||
dstImg.Pix[idx] = 0
|
||||
}
|
||||
for y := 0; y < height; y++ {
|
||||
yF := (float64(y) + 0.5) / float64(height)
|
||||
idxY := (y - dstImg.Rect.Min.Y) * dstImg.Stride
|
||||
var xBoundaries []float64
|
||||
for _, tImg := range transformedImages {
|
||||
xBoundaries = append(
|
||||
xBoundaries,
|
||||
(float64(tImg.To.Min.X)+0.5)/float64(width),
|
||||
(float64(tImg.To.Max.X)+0.5)/float64(width),
|
||||
)
|
||||
}
|
||||
boundaryIdx := -1
|
||||
nextSwitchX := float64(-1)
|
||||
var tImg *ximage.Transform
|
||||
var dup float64
|
||||
for x := 0; x < width; x++ {
|
||||
idxX := (x - dstImg.Rect.Min.X) * 4
|
||||
idx := idxY + idxX
|
||||
if dstImg.Pix[idx+3] != 0 {
|
||||
continue
|
||||
}
|
||||
xF := (float64(x) + 0.5) / float64(width)
|
||||
if xF > nextSwitchX {
|
||||
tImg = nil
|
||||
dup = 0
|
||||
for _, _tImg := range transformedImages {
|
||||
c := _tImg.To.AtFloat64(xF, yF)
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
tImg = _tImg
|
||||
dstSize := dstImg.Bounds().Size()
|
||||
dup = min(
|
||||
(float64(dstSize.X)*tImg.To.Size().X)/(float64(tImg.ImageSize.X)),
|
||||
(float64(dstSize.Y)*tImg.To.Size().Y)/(float64(tImg.ImageSize.Y)),
|
||||
)
|
||||
break
|
||||
}
|
||||
boundaryIdx++
|
||||
if boundaryIdx < len(xBoundaries) {
|
||||
nextSwitchX = xBoundaries[boundaryIdx]
|
||||
}
|
||||
}
|
||||
if tImg == nil {
|
||||
continue
|
||||
}
|
||||
srcX, srcY, ok := tImg.Coords(xF, yF)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
srcImg := tImg.Image.(*image.NRGBA)
|
||||
srcOffset := srcImg.PixOffset(srcX, srcY)
|
||||
if srcOffset < 0 || srcOffset+4 >= len(srcImg.Pix) {
|
||||
continue
|
||||
}
|
||||
|
||||
dstOffset := dstImg.PixOffset(x, y)
|
||||
if dstOffset < 0 || dstOffset+4 >= len(dstImg.Pix) {
|
||||
continue
|
||||
}
|
||||
copy(
|
||||
dstImg.Pix[dstOffset:dstOffset+4:dstOffset+4],
|
||||
srcImg.Pix[srcOffset:srcOffset+4:srcOffset+4],
|
||||
)
|
||||
if dup > 1 {
|
||||
xE := int(float64(x+1)*dup) - int(float64(x)*dup) - 1
|
||||
yE := int(float64(y+1)*dup) - int(float64(y)*dup) - 1
|
||||
|
||||
for i := 0; i <= yE; i++ {
|
||||
for j := 0; j <= xE; j++ {
|
||||
if i == 0 && j == 0 {
|
||||
continue
|
||||
}
|
||||
dstOffset := dstImg.PixOffset(x+j, y+i)
|
||||
if dstOffset < 0 || dstOffset+4 >= len(dstImg.Pix) {
|
||||
continue
|
||||
}
|
||||
copy(
|
||||
dstImg.Pix[dstOffset:dstOffset+4:dstOffset+4],
|
||||
srcImg.Pix[srcOffset:srcOffset+4:srcOffset+4],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return dstImg
|
||||
}
|
||||
|
||||
func (w *dashboardWindow) startUpdating(
|
||||
ctx context.Context,
|
||||
) {
|
||||
@@ -326,15 +517,9 @@ func (w *dashboardWindow) updateImagesNoLock(
|
||||
logger.Debugf(ctx, "window size changed %#+v -> %#+v", lastWinSize, winSize)
|
||||
}
|
||||
|
||||
type elementType struct {
|
||||
ElementName string
|
||||
streamdconfig.DashboardElementConfig
|
||||
NewImage *canvas.Image
|
||||
}
|
||||
|
||||
elements := make([]elementType, 0, len(dashboardCfg.Elements))
|
||||
elements := make([]imageInfo, 0, len(dashboardCfg.Elements))
|
||||
for elName, el := range dashboardCfg.Elements {
|
||||
elements = append(elements, elementType{
|
||||
elements = append(elements, imageInfo{
|
||||
ElementName: elName,
|
||||
DashboardElementConfig: el,
|
||||
})
|
||||
@@ -346,16 +531,30 @@ func (w *dashboardWindow) updateImagesNoLock(
|
||||
return elements[i].ElementName < elements[j].ElementName
|
||||
})
|
||||
|
||||
elementsMap := map[string]*imageInfo{}
|
||||
for idx := range elements {
|
||||
item := &elements[idx]
|
||||
elementsMap[item.ElementName] = item
|
||||
}
|
||||
|
||||
for _, item := range w.images {
|
||||
obj := elementsMap[item.ElementName]
|
||||
if obj == nil {
|
||||
continue
|
||||
}
|
||||
obj.Image = item.Image
|
||||
}
|
||||
w.imagesLocker.Do(xsync.WithNoLogging(ctx, true), func() {
|
||||
w.images = elements
|
||||
})
|
||||
|
||||
var changeCount atomic.Uint64
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for idx := range elements {
|
||||
wg.Add(1)
|
||||
{
|
||||
el := &elements[idx]
|
||||
var oldObj fyne.CanvasObject
|
||||
if len(w.layersContainer.Objects) > idx {
|
||||
oldObj = w.layersContainer.Objects[idx]
|
||||
}
|
||||
observability.Go(ctx, func() {
|
||||
defer wg.Done()
|
||||
img, changed, err := w.getImage(ctx, streamdconsts.ImageID(el.ElementName))
|
||||
@@ -374,33 +573,13 @@ func (w *dashboardWindow) updateImagesNoLock(
|
||||
lastWinSize,
|
||||
winSize,
|
||||
)
|
||||
imgSize := image.Point{
|
||||
X: int(winSize.Width * float32(el.Width) / 100),
|
||||
Y: int(winSize.Height * float32(el.Height) / 100),
|
||||
}
|
||||
offset := image.Point{
|
||||
X: int(winSize.Width * float32(el.OffsetX) / 100),
|
||||
Y: int(winSize.Height * float32(el.OffsetY) / 100),
|
||||
}
|
||||
var oldImg image.Image
|
||||
if oldCanvasImg, ok := oldObj.(*canvas.Image); ok {
|
||||
oldImg = oldCanvasImg.Image
|
||||
}
|
||||
img = imgFillTo(
|
||||
ctx,
|
||||
oldImg,
|
||||
img,
|
||||
image.Point{X: int(winSize.Width), Y: int(winSize.Height)},
|
||||
imgSize,
|
||||
offset,
|
||||
el.AlignX,
|
||||
el.AlignY,
|
||||
)
|
||||
imgFyne := canvas.NewImageFromImage(img)
|
||||
imgFyne.FillMode = canvas.ImageFillContain
|
||||
imgFyne.SetMinSize(fyne.NewSize(1, 1))
|
||||
logger.Tracef(ctx, "image '%s' size: %#+v", el.ElementName, img.Bounds().Size())
|
||||
el.NewImage = imgFyne
|
||||
b := img.Bounds()
|
||||
m := image.NewNRGBA(image.Rect(0, 0, b.Dx(), b.Dy()))
|
||||
draw.Draw(m, m.Bounds(), img, b.Min, draw.Src)
|
||||
w.imagesLocker.Do(xsync.WithNoLogging(ctx, true), func() {
|
||||
el.Image = m
|
||||
})
|
||||
changeCount.Add(1)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -442,37 +621,19 @@ func (w *dashboardWindow) updateImagesNoLock(
|
||||
)
|
||||
img = adjust.Brightness(img, -0.5)
|
||||
imgFyne := canvas.NewImageFromImage(img)
|
||||
imgFyne.FillMode = canvas.ImageFillContain
|
||||
imgFyne.FillMode = canvas.ImageFillOriginal
|
||||
logger.Tracef(ctx, "screenshot image size: %#+v", img.Bounds().Size())
|
||||
|
||||
w.screenshotContainer.Objects = w.screenshotContainer.Objects[:0]
|
||||
w.screenshotContainer.Objects = append(w.screenshotContainer.Objects, imgFyne)
|
||||
w.screenshotContainer.Refresh()
|
||||
changeCount.Add(1)
|
||||
})
|
||||
wg.Wait()
|
||||
|
||||
if len(w.layersContainer.Objects) != len(elements) {
|
||||
w.layersContainer.Objects = w.layersContainer.Objects[:0]
|
||||
img := image.NewRGBA(image.Rectangle{
|
||||
Max: image.Point{
|
||||
X: 1,
|
||||
Y: 1,
|
||||
},
|
||||
})
|
||||
for len(w.layersContainer.Objects) < len(elements) {
|
||||
w.layersContainer.Objects = append(
|
||||
w.layersContainer.Objects,
|
||||
canvas.NewImageFromImage(img),
|
||||
)
|
||||
}
|
||||
if changeCount.Load() > 0 {
|
||||
w.imagesLayerObj.Refresh()
|
||||
}
|
||||
for idx, el := range elements {
|
||||
if el.NewImage == nil {
|
||||
continue
|
||||
}
|
||||
w.layersContainer.Objects[idx] = el.NewImage
|
||||
}
|
||||
w.layersContainer.Refresh()
|
||||
}
|
||||
|
||||
func (p *Panel) newDashboardSettingsWindow(ctx context.Context) {
|
||||
@@ -970,7 +1131,7 @@ func (p *Panel) editDashboardElementWindow(
|
||||
|
||||
var volumeColorActiveParsed color.Color
|
||||
if volumeColorActiveParsed, err = colorx.Parse(obsVolumeSource.ColorActive); err != nil {
|
||||
volumeColorActiveParsed = color.RGBA{R: 0, G: 255, B: 0, A: 255}
|
||||
volumeColorActiveParsed = color.NRGBA{R: 0, G: 255, B: 0, A: 255}
|
||||
}
|
||||
volumeColorActive := colorpicker.NewColorSelectModalRect(
|
||||
w,
|
||||
@@ -985,7 +1146,7 @@ func (p *Panel) editDashboardElementWindow(
|
||||
|
||||
var volumeColorPassiveParsed color.Color
|
||||
if volumeColorPassiveParsed, err = colorx.Parse(obsVolumeSource.ColorPassive); err != nil {
|
||||
volumeColorPassiveParsed = color.RGBA{R: 0, G: 0, B: 0, A: 0}
|
||||
volumeColorPassiveParsed = color.NRGBA{R: 0, G: 0, B: 0, A: 0}
|
||||
}
|
||||
volumeColorPassive := colorpicker.NewColorSelectModalRect(
|
||||
w,
|
||||
@@ -1011,12 +1172,13 @@ func (p *Panel) editDashboardElementWindow(
|
||||
)
|
||||
|
||||
sourceTypeSelect := widget.NewSelect([]string{
|
||||
string(streamdconfig.DashboardSourceImageTypeOBSVideo),
|
||||
string(streamdconfig.DashboardSourceImageTypeStreamScreenshot),
|
||||
string(streamdconfig.DashboardSourceImageTypeOBSScreenshot),
|
||||
string(streamdconfig.DashboardSourceImageTypeOBSVolume),
|
||||
string(streamdconfig.DashboardSourceImageTypeDummy),
|
||||
}, func(s string) {
|
||||
switch streamdconfig.DashboardSourceImageType(s) {
|
||||
case streamdconfig.DashboardSourceImageTypeOBSVideo:
|
||||
case streamdconfig.DashboardSourceImageTypeOBSScreenshot:
|
||||
sourceOBSVolumeConfig.Hide()
|
||||
sourceOBSVideoConfig.Show()
|
||||
case streamdconfig.DashboardSourceImageTypeOBSVolume:
|
||||
@@ -1027,7 +1189,7 @@ func (p *Panel) editDashboardElementWindow(
|
||||
sourceOBSVolumeConfig.Hide()
|
||||
}
|
||||
})
|
||||
sourceTypeSelect.SetSelected(string(streamdconfig.DashboardSourceImageTypeOBSVideo))
|
||||
sourceTypeSelect.SetSelected(string(streamdconfig.DashboardSourceImageTypeOBSScreenshot))
|
||||
|
||||
saveButton := widget.NewButtonWithIcon("Save", theme.DocumentSaveIcon(), func() {
|
||||
if elementName.Text == "" {
|
||||
@@ -1035,7 +1197,7 @@ func (p *Panel) editDashboardElementWindow(
|
||||
return
|
||||
}
|
||||
switch streamdconfig.DashboardSourceImageType(sourceTypeSelect.Selected) {
|
||||
case streamdconfig.DashboardSourceImageTypeOBSVideo:
|
||||
case streamdconfig.DashboardSourceImageTypeOBSScreenshot:
|
||||
cfg.Source = obsVideoSource
|
||||
case streamdconfig.DashboardSourceImageTypeOBSVolume:
|
||||
cfg.Source = obsVolumeSource
|
||||
|
@@ -163,7 +163,11 @@ func imgFillTo(
|
||||
offset image.Point,
|
||||
alignX streamdconsts.AlignX,
|
||||
alignY streamdconsts.AlignY,
|
||||
) image.Image {
|
||||
) (_ret image.Image) {
|
||||
logger.Tracef(ctx, "imgFillTo(ctx, %T, %T, %v, %v, %v, %v, %v)", dstReuse, src, canvasSize, outSize, offset, alignX, alignY)
|
||||
defer func() {
|
||||
logger.Tracef(ctx, "/imgFillTo(ctx, %T, %T, %v, %v, %v, %v, %v): %T:%s", dstReuse, src, canvasSize, outSize, offset, alignX, alignY, _ret, _ret.Bounds())
|
||||
}()
|
||||
|
||||
sizeCur := src.Bounds().Size()
|
||||
ratioCur := float64(sizeCur.X) / float64(sizeCur.Y)
|
||||
|
108
pkg/ximage/transform.go
Normal file
108
pkg/ximage/transform.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package ximage
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
)
|
||||
|
||||
type PointFloat64 struct {
|
||||
X float64
|
||||
Y float64
|
||||
}
|
||||
|
||||
type RectangleFloat64 struct {
|
||||
Min PointFloat64
|
||||
Max PointFloat64
|
||||
}
|
||||
|
||||
func (t *RectangleFloat64) Size() PointFloat64 {
|
||||
return PointFloat64{
|
||||
X: math.Abs(t.Max.X - t.Min.X),
|
||||
Y: math.Abs(t.Max.Y - t.Min.Y),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *RectangleFloat64) At(x, y int) color.Color {
|
||||
return t.AtFloat64(float64(x), float64(y))
|
||||
}
|
||||
|
||||
func (t *RectangleFloat64) AtFloat64(x, y float64) color.Color {
|
||||
if x < t.Min.X || x > t.Max.X {
|
||||
return nil
|
||||
}
|
||||
if y < t.Min.Y || y > t.Max.Y {
|
||||
return nil
|
||||
}
|
||||
return color.Gray{Y: 255}
|
||||
}
|
||||
|
||||
type Transform struct {
|
||||
image.Image
|
||||
ImageBounds image.Rectangle
|
||||
ImageSize image.Point
|
||||
ColorModelValue color.Model
|
||||
To RectangleFloat64
|
||||
}
|
||||
|
||||
var _ image.Image = (*Transform)(nil)
|
||||
|
||||
func NewTransform(
|
||||
img image.Image,
|
||||
colorModel color.Model,
|
||||
transform RectangleFloat64,
|
||||
) *Transform {
|
||||
if img == nil {
|
||||
return nil
|
||||
}
|
||||
return &Transform{
|
||||
Image: img,
|
||||
ImageBounds: img.Bounds(),
|
||||
ImageSize: img.Bounds().Size(),
|
||||
ColorModelValue: colorModel,
|
||||
To: transform,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transform) ColorModel() color.Model {
|
||||
return t.ColorModelValue
|
||||
}
|
||||
|
||||
func (t *Transform) Bounds() image.Rectangle {
|
||||
return image.Rectangle{
|
||||
Min: image.Point{
|
||||
X: int(t.To.Min.X),
|
||||
Y: int(t.To.Min.Y),
|
||||
},
|
||||
Max: image.Point{
|
||||
X: int(t.To.Max.X),
|
||||
Y: int(t.To.Max.Y),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transform) At(x, y int) color.Color {
|
||||
return t.AtFloat64(float64(x), float64(y))
|
||||
}
|
||||
|
||||
func (t *Transform) Coords(x, y float64) (int, int, bool) {
|
||||
if x < t.To.Min.X || x > t.To.Max.X {
|
||||
return 0, 0, false
|
||||
}
|
||||
if y < t.To.Min.Y || y > t.To.Max.Y {
|
||||
return 0, 0, false
|
||||
}
|
||||
x = (x - t.To.Min.X) / (t.To.Max.X - t.To.Min.X)
|
||||
y = (y - t.To.Min.Y) / (t.To.Max.Y - t.To.Min.Y)
|
||||
srcX := t.ImageBounds.Min.X + int(x*float64(t.ImageSize.X))
|
||||
srcY := t.ImageBounds.Min.Y + int(y*float64(t.ImageSize.Y))
|
||||
return srcX, srcY, true
|
||||
}
|
||||
|
||||
func (t *Transform) AtFloat64(x, y float64) color.Color {
|
||||
srcX, srcY, ok := t.Coords(x, y)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return t.Image.At(srcX, srcY)
|
||||
}
|
Reference in New Issue
Block a user