mirror of
https://github.com/xaionaro-go/streamctl.git
synced 2025-09-27 03:45:52 +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.
|
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 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
|
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/mmcloughlin/profile v0.1.1 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // 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/nicksnyder/go-i18n/v2 v2.4.0 // indirect
|
||||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
|
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||||
@@ -219,7 +220,7 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
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/AgustinSRG/go-child-process-manager v1.0.1
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802
|
||||||
github.com/abhinavxd/youtube-live-chat-downloader/v2 v2.0.3
|
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 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
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=
|
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 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
|
||||||
fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
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=
|
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/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/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/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 h1:PhRVFPI7eXzLajhhZuReQUy2cuViKqkASRKXV7o2G7I=
|
||||||
github.com/nicklaw5/helix/v2 v2.30.1-0.20240715193454-0151ccccf980/go.mod h1:e1GsZq4NDk9sQlPJ0Nr3+14R9cizqg09VAk7/IonpOU=
|
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=
|
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/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 h1:+QMZLmu73R5WGkQfUPwlXF/JFN+Weo4iuDZkiL2wVm8=
|
||||||
github.com/xaionaro-go/datacounter v1.0.4/go.mod h1:Sf9vBevuV6w5iE6K3qJ9pWVKcyS60clWBUSQLjt5++c=
|
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 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/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=
|
github.com/xaionaro-go/goobs v0.0.0-20241103210141-030e538ac440 h1:hzQ+65oWq54XAqheyJ9E6wt+WH75051w+eLP5zWlD68=
|
||||||
|
@@ -8,23 +8,27 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/xaionaro-go/obs-grpc-proxy/protobuf/go/obs_grpc"
|
"github.com/xaionaro-go/obs-grpc-proxy/protobuf/go/obs_grpc"
|
||||||
|
"github.com/xaionaro-go/recoder"
|
||||||
"github.com/xaionaro-go/streamctl/pkg/streamtypes"
|
"github.com/xaionaro-go/streamctl/pkg/streamtypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DashboardSourceImageType string
|
type DashboardSourceImageType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DashboardSourceImageTypeUndefined = DashboardSourceImageType("")
|
DashboardSourceImageTypeUndefined = DashboardSourceImageType("")
|
||||||
DashboardSourceImageTypeDummy = DashboardSourceImageType("dummy")
|
DashboardSourceImageTypeDummy = DashboardSourceImageType("dummy")
|
||||||
DashboardSourceImageTypeOBSVideo = DashboardSourceImageType("obs_video") // rename to `obs_screenshot`
|
DashboardSourceImageTypeStreamScreenshot = DashboardSourceImageType("stream_screenshot")
|
||||||
DashboardSourceImageTypeOBSVolume = DashboardSourceImageType("obs_volume")
|
DashboardSourceImageTypeOBSScreenshot = DashboardSourceImageType("obs_screenshot")
|
||||||
|
DashboardSourceImageTypeOBSVolume = DashboardSourceImageType("obs_volume")
|
||||||
)
|
)
|
||||||
|
|
||||||
func (mst DashboardSourceImageType) New() SourceImage {
|
func (mst DashboardSourceImageType) New() SourceImage {
|
||||||
switch mst {
|
switch mst {
|
||||||
case DashboardSourceImageTypeDummy:
|
case DashboardSourceImageTypeDummy:
|
||||||
return &DashboardSourceImageDummy{}
|
return &DashboardSourceImageDummy{}
|
||||||
case DashboardSourceImageTypeOBSVideo:
|
case DashboardSourceImageTypeStreamScreenshot:
|
||||||
|
return &DashboardSourceImageStreamScreenshot{}
|
||||||
|
case DashboardSourceImageTypeOBSScreenshot:
|
||||||
return &DashboardSourceImageOBSScreenshot{}
|
return &DashboardSourceImageOBSScreenshot{}
|
||||||
case DashboardSourceImageTypeOBSVolume:
|
case DashboardSourceImageTypeOBSVolume:
|
||||||
return &DashboardSourceImageOBSVolume{}
|
return &DashboardSourceImageOBSVolume{}
|
||||||
@@ -42,14 +46,6 @@ const (
|
|||||||
ImageFormatWebP = ImageFormat("webp")
|
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
|
type Duration time.Duration
|
||||||
|
|
||||||
func (d Duration) MarshalJSON() ([]byte, error) {
|
func (d Duration) MarshalJSON() ([]byte, error) {
|
||||||
@@ -80,17 +76,30 @@ func (d *Duration) UnmarshalJSON(b []byte) error {
|
|||||||
|
|
||||||
var _ SourceImage = (*DashboardSourceImageDummy)(nil)
|
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 {
|
type SourceImage interface {
|
||||||
GetImage(
|
GetImage(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
obsServer obs_grpc.OBSServer,
|
|
||||||
el DashboardElementConfig,
|
el DashboardElementConfig,
|
||||||
obsState *streamtypes.OBSState,
|
imageDataProvider ImageDataProvider,
|
||||||
) (image.Image, time.Time, error)
|
) (image.Image, time.Time, error)
|
||||||
|
|
||||||
SourceType() DashboardSourceImageType
|
SourceType() DashboardSourceImageType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GetImageBytes interface {
|
||||||
|
GetImageBytes(
|
||||||
|
ctx context.Context,
|
||||||
|
el DashboardElementConfig,
|
||||||
|
imageDataProvider ImageDataProvider,
|
||||||
|
) ([]byte, string, time.Time, error)
|
||||||
|
}
|
||||||
|
|
||||||
type serializableSourceImage struct {
|
type serializableSourceImage struct {
|
||||||
Type DashboardSourceImageType `yaml:"type"`
|
Type DashboardSourceImageType `yaml:"type"`
|
||||||
Config map[string]any `yaml:"config,omitempty"`
|
Config map[string]any `yaml:"config,omitempty"`
|
||||||
|
@@ -4,18 +4,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"image"
|
"image"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/xaionaro-go/obs-grpc-proxy/protobuf/go/obs_grpc"
|
|
||||||
"github.com/xaionaro-go/streamctl/pkg/streamtypes"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DashboardSourceImageDummy struct{}
|
type DashboardSourceImageDummy struct{}
|
||||||
|
|
||||||
func (*DashboardSourceImageDummy) GetImage(
|
func (*DashboardSourceImageDummy) GetImage(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
obsServer obs_grpc.OBSServer,
|
|
||||||
el DashboardElementConfig,
|
el DashboardElementConfig,
|
||||||
obsState *streamtypes.OBSState,
|
_ ImageDataProvider,
|
||||||
) (image.Image, time.Time, error) {
|
) (image.Image, time.Time, error) {
|
||||||
img := image.NewRGBA(image.Rectangle{
|
img := image.NewRGBA(image.Rectangle{
|
||||||
Min: image.Point{
|
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 _ SourceImage = (*DashboardSourceImageOBSScreenshot)(nil)
|
||||||
var _ GetImageByteser = (*DashboardSourceImageOBSScreenshot)(nil)
|
var _ GetImageFromOBSer = (*DashboardSourceImageOBSScreenshot)(nil)
|
||||||
|
var _ GetImageBytesFromOBSer = (*DashboardSourceImageOBSScreenshot)(nil)
|
||||||
|
|
||||||
func (*DashboardSourceImageOBSScreenshot) SourceType() DashboardSourceImageType {
|
func (*DashboardSourceImageOBSScreenshot) SourceType() DashboardSourceImageType {
|
||||||
return DashboardSourceImageTypeOBSVideo
|
return DashboardSourceImageTypeOBSScreenshot
|
||||||
}
|
}
|
||||||
|
|
||||||
func obsGetImage(
|
func obsGetImage(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
getImageByteser GetImageByteser,
|
getImageByteser GetImageBytesFromOBSer,
|
||||||
obsServer obs_grpc.OBSServer,
|
obsServer obs_grpc.OBSServer,
|
||||||
el DashboardElementConfig,
|
el DashboardElementConfig,
|
||||||
_ *streamtypes.OBSState,
|
_ *streamtypes.OBSState,
|
||||||
) (image.Image, time.Time, error) {
|
) (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 {
|
if err != nil {
|
||||||
return nil, nextUpdateTS, fmt.Errorf("unable to get the image from OBS: %w", err)
|
return nil, nextUpdateTS, fmt.Errorf("unable to get the image from OBS: %w", err)
|
||||||
}
|
}
|
||||||
@@ -62,7 +63,7 @@ func obsGetImage(
|
|||||||
return img, nextUpdateTS, nil
|
return img, nextUpdateTS, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DashboardSourceImageOBSScreenshot) GetImage(
|
func (s *DashboardSourceImageOBSScreenshot) GetImageFromOBS(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
obsServer obs_grpc.OBSServer,
|
obsServer obs_grpc.OBSServer,
|
||||||
el DashboardElementConfig,
|
el DashboardElementConfig,
|
||||||
@@ -71,7 +72,7 @@ func (s *DashboardSourceImageOBSScreenshot) GetImage(
|
|||||||
return obsGetImage(ctx, s, obsServer, el, obsState)
|
return obsGetImage(ctx, s, obsServer, el, obsState)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DashboardSourceImageOBSScreenshot) GetImageBytes(
|
func (s *DashboardSourceImageOBSScreenshot) GetImageBytesFromOBS(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
obsServer obs_grpc.OBSServer,
|
obsServer obs_grpc.OBSServer,
|
||||||
el DashboardElementConfig,
|
el DashboardElementConfig,
|
||||||
@@ -133,3 +134,31 @@ func (s *DashboardSourceImageOBSScreenshot) GetImageBytes(
|
|||||||
)
|
)
|
||||||
return imgBytes, mimeType, time.Now().Add(time.Duration(s.UpdateInterval)), nil
|
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)
|
var _ SourceImage = (*DashboardSourceImageOBSVolume)(nil)
|
||||||
|
|
||||||
func (s *DashboardSourceImageOBSVolume) GetImage(
|
func (s *DashboardSourceImageOBSVolume) GetImageFromOBS(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
obsServer obs_grpc.OBSServer,
|
obsServer obs_grpc.OBSServer,
|
||||||
el DashboardElementConfig,
|
el DashboardElementConfig,
|
||||||
@@ -94,3 +94,19 @@ func (s *DashboardSourceImageOBSVolume) GetImage(
|
|||||||
func (*DashboardSourceImageOBSVolume) SourceType() DashboardSourceImageType {
|
func (*DashboardSourceImageOBSVolume) SourceType() DashboardSourceImageType {
|
||||||
return DashboardSourceImageTypeOBSVolume
|
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
|
package streamd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/andreykaipov/goobs"
|
"github.com/andreykaipov/goobs"
|
||||||
"github.com/chai2010/webp"
|
|
||||||
"github.com/facebookincubator/go-belt/tool/logger"
|
"github.com/facebookincubator/go-belt/tool/logger"
|
||||||
"github.com/xaionaro-go/obs-grpc-proxy/pkg/obsgrpcproxy"
|
"github.com/xaionaro-go/obs-grpc-proxy/pkg/obsgrpcproxy"
|
||||||
"github.com/xaionaro-go/obs-grpc-proxy/protobuf/go/obs_grpc"
|
"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/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"
|
"github.com/xaionaro-go/xsync"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -59,164 +52,6 @@ func (d *StreamD) OBS(
|
|||||||
|
|
||||||
var ErrNotChanged = errors.New("not changed")
|
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 {
|
type SceneElementIdentifier struct {
|
||||||
Name *string
|
Name *string
|
||||||
UUID *string
|
UUID *string
|
||||||
|
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
"image/draw"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -35,6 +36,7 @@ import (
|
|||||||
streamdconfig "github.com/xaionaro-go/streamctl/pkg/streamd/config"
|
streamdconfig "github.com/xaionaro-go/streamctl/pkg/streamd/config"
|
||||||
streamdconsts "github.com/xaionaro-go/streamctl/pkg/streamd/consts"
|
streamdconsts "github.com/xaionaro-go/streamctl/pkg/streamd/consts"
|
||||||
"github.com/xaionaro-go/streamctl/pkg/streampanel/consts"
|
"github.com/xaionaro-go/streamctl/pkg/streampanel/consts"
|
||||||
|
"github.com/xaionaro-go/streamctl/pkg/ximage"
|
||||||
xfyne "github.com/xaionaro-go/xfyne/widget"
|
xfyne "github.com/xaionaro-go/xfyne/widget"
|
||||||
"github.com/xaionaro-go/xsync"
|
"github.com/xaionaro-go/xsync"
|
||||||
)
|
)
|
||||||
@@ -64,11 +66,20 @@ type dashboardWindow struct {
|
|||||||
lastWinSize fyne.Size
|
lastWinSize fyne.Size
|
||||||
lastOrientation fyne.DeviceOrientation
|
lastOrientation fyne.DeviceOrientation
|
||||||
screenshotContainer *fyne.Container
|
screenshotContainer *fyne.Container
|
||||||
layersContainer *fyne.Container
|
imagesLocker xsync.RWMutex
|
||||||
|
imagesLayerObj *canvas.Raster
|
||||||
|
images []imageInfo
|
||||||
|
renderedImagesLayer *image.NRGBA
|
||||||
streamStatus map[streamcontrol.PlatformName]*widget.Label
|
streamStatus map[streamcontrol.PlatformName]*widget.Label
|
||||||
streamStatusLocker xsync.Mutex
|
streamStatusLocker xsync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type imageInfo struct {
|
||||||
|
ElementName string
|
||||||
|
streamdconfig.DashboardElementConfig
|
||||||
|
Image *image.NRGBA
|
||||||
|
}
|
||||||
|
|
||||||
func (w *dashboardWindow) renderStreamStatus(ctx context.Context) {
|
func (w *dashboardWindow) renderStreamStatus(ctx context.Context) {
|
||||||
w.streamStatusLocker.Do(ctx, func() {
|
w.streamStatusLocker.Do(ctx, func() {
|
||||||
streamDClient, ok := w.StreamD.(*client.Client)
|
streamDClient, ok := w.StreamD.(*client.Client)
|
||||||
@@ -187,18 +198,198 @@ func (p *Panel) newDashboardWindow(
|
|||||||
nil,
|
nil,
|
||||||
streamInfoItems,
|
streamInfoItems,
|
||||||
)
|
)
|
||||||
w.layersContainer = container.NewStack()
|
w.imagesLayerObj = canvas.NewRaster(w.imagesLayer)
|
||||||
|
|
||||||
w.Window.SetContent(container.NewStack(
|
w.Window.SetContent(container.NewStack(
|
||||||
bgFyne,
|
bgFyne,
|
||||||
w.screenshotContainer,
|
w.screenshotContainer,
|
||||||
w.layersContainer,
|
w.imagesLayerObj,
|
||||||
streamInfoContainer,
|
streamInfoContainer,
|
||||||
))
|
))
|
||||||
w.Window.Show()
|
w.Window.Show()
|
||||||
return w
|
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(
|
func (w *dashboardWindow) startUpdating(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
) {
|
) {
|
||||||
@@ -326,15 +517,9 @@ func (w *dashboardWindow) updateImagesNoLock(
|
|||||||
logger.Debugf(ctx, "window size changed %#+v -> %#+v", lastWinSize, winSize)
|
logger.Debugf(ctx, "window size changed %#+v -> %#+v", lastWinSize, winSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
type elementType struct {
|
elements := make([]imageInfo, 0, len(dashboardCfg.Elements))
|
||||||
ElementName string
|
|
||||||
streamdconfig.DashboardElementConfig
|
|
||||||
NewImage *canvas.Image
|
|
||||||
}
|
|
||||||
|
|
||||||
elements := make([]elementType, 0, len(dashboardCfg.Elements))
|
|
||||||
for elName, el := range dashboardCfg.Elements {
|
for elName, el := range dashboardCfg.Elements {
|
||||||
elements = append(elements, elementType{
|
elements = append(elements, imageInfo{
|
||||||
ElementName: elName,
|
ElementName: elName,
|
||||||
DashboardElementConfig: el,
|
DashboardElementConfig: el,
|
||||||
})
|
})
|
||||||
@@ -346,16 +531,30 @@ func (w *dashboardWindow) updateImagesNoLock(
|
|||||||
return elements[i].ElementName < elements[j].ElementName
|
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
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
for idx := range elements {
|
for idx := range elements {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
{
|
{
|
||||||
el := &elements[idx]
|
el := &elements[idx]
|
||||||
var oldObj fyne.CanvasObject
|
|
||||||
if len(w.layersContainer.Objects) > idx {
|
|
||||||
oldObj = w.layersContainer.Objects[idx]
|
|
||||||
}
|
|
||||||
observability.Go(ctx, func() {
|
observability.Go(ctx, func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
img, changed, err := w.getImage(ctx, streamdconsts.ImageID(el.ElementName))
|
img, changed, err := w.getImage(ctx, streamdconsts.ImageID(el.ElementName))
|
||||||
@@ -374,33 +573,13 @@ func (w *dashboardWindow) updateImagesNoLock(
|
|||||||
lastWinSize,
|
lastWinSize,
|
||||||
winSize,
|
winSize,
|
||||||
)
|
)
|
||||||
imgSize := image.Point{
|
b := img.Bounds()
|
||||||
X: int(winSize.Width * float32(el.Width) / 100),
|
m := image.NewNRGBA(image.Rect(0, 0, b.Dx(), b.Dy()))
|
||||||
Y: int(winSize.Height * float32(el.Height) / 100),
|
draw.Draw(m, m.Bounds(), img, b.Min, draw.Src)
|
||||||
}
|
w.imagesLocker.Do(xsync.WithNoLogging(ctx, true), func() {
|
||||||
offset := image.Point{
|
el.Image = m
|
||||||
X: int(winSize.Width * float32(el.OffsetX) / 100),
|
})
|
||||||
Y: int(winSize.Height * float32(el.OffsetY) / 100),
|
changeCount.Add(1)
|
||||||
}
|
|
||||||
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
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -442,37 +621,19 @@ func (w *dashboardWindow) updateImagesNoLock(
|
|||||||
)
|
)
|
||||||
img = adjust.Brightness(img, -0.5)
|
img = adjust.Brightness(img, -0.5)
|
||||||
imgFyne := canvas.NewImageFromImage(img)
|
imgFyne := canvas.NewImageFromImage(img)
|
||||||
imgFyne.FillMode = canvas.ImageFillContain
|
imgFyne.FillMode = canvas.ImageFillOriginal
|
||||||
logger.Tracef(ctx, "screenshot image size: %#+v", img.Bounds().Size())
|
logger.Tracef(ctx, "screenshot image size: %#+v", img.Bounds().Size())
|
||||||
|
|
||||||
w.screenshotContainer.Objects = w.screenshotContainer.Objects[:0]
|
w.screenshotContainer.Objects = w.screenshotContainer.Objects[:0]
|
||||||
w.screenshotContainer.Objects = append(w.screenshotContainer.Objects, imgFyne)
|
w.screenshotContainer.Objects = append(w.screenshotContainer.Objects, imgFyne)
|
||||||
w.screenshotContainer.Refresh()
|
w.screenshotContainer.Refresh()
|
||||||
|
changeCount.Add(1)
|
||||||
})
|
})
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
if len(w.layersContainer.Objects) != len(elements) {
|
if changeCount.Load() > 0 {
|
||||||
w.layersContainer.Objects = w.layersContainer.Objects[:0]
|
w.imagesLayerObj.Refresh()
|
||||||
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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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) {
|
func (p *Panel) newDashboardSettingsWindow(ctx context.Context) {
|
||||||
@@ -970,7 +1131,7 @@ func (p *Panel) editDashboardElementWindow(
|
|||||||
|
|
||||||
var volumeColorActiveParsed color.Color
|
var volumeColorActiveParsed color.Color
|
||||||
if volumeColorActiveParsed, err = colorx.Parse(obsVolumeSource.ColorActive); err != nil {
|
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(
|
volumeColorActive := colorpicker.NewColorSelectModalRect(
|
||||||
w,
|
w,
|
||||||
@@ -985,7 +1146,7 @@ func (p *Panel) editDashboardElementWindow(
|
|||||||
|
|
||||||
var volumeColorPassiveParsed color.Color
|
var volumeColorPassiveParsed color.Color
|
||||||
if volumeColorPassiveParsed, err = colorx.Parse(obsVolumeSource.ColorPassive); err != nil {
|
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(
|
volumeColorPassive := colorpicker.NewColorSelectModalRect(
|
||||||
w,
|
w,
|
||||||
@@ -1011,12 +1172,13 @@ func (p *Panel) editDashboardElementWindow(
|
|||||||
)
|
)
|
||||||
|
|
||||||
sourceTypeSelect := widget.NewSelect([]string{
|
sourceTypeSelect := widget.NewSelect([]string{
|
||||||
string(streamdconfig.DashboardSourceImageTypeOBSVideo),
|
string(streamdconfig.DashboardSourceImageTypeStreamScreenshot),
|
||||||
|
string(streamdconfig.DashboardSourceImageTypeOBSScreenshot),
|
||||||
string(streamdconfig.DashboardSourceImageTypeOBSVolume),
|
string(streamdconfig.DashboardSourceImageTypeOBSVolume),
|
||||||
string(streamdconfig.DashboardSourceImageTypeDummy),
|
string(streamdconfig.DashboardSourceImageTypeDummy),
|
||||||
}, func(s string) {
|
}, func(s string) {
|
||||||
switch streamdconfig.DashboardSourceImageType(s) {
|
switch streamdconfig.DashboardSourceImageType(s) {
|
||||||
case streamdconfig.DashboardSourceImageTypeOBSVideo:
|
case streamdconfig.DashboardSourceImageTypeOBSScreenshot:
|
||||||
sourceOBSVolumeConfig.Hide()
|
sourceOBSVolumeConfig.Hide()
|
||||||
sourceOBSVideoConfig.Show()
|
sourceOBSVideoConfig.Show()
|
||||||
case streamdconfig.DashboardSourceImageTypeOBSVolume:
|
case streamdconfig.DashboardSourceImageTypeOBSVolume:
|
||||||
@@ -1027,7 +1189,7 @@ func (p *Panel) editDashboardElementWindow(
|
|||||||
sourceOBSVolumeConfig.Hide()
|
sourceOBSVolumeConfig.Hide()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
sourceTypeSelect.SetSelected(string(streamdconfig.DashboardSourceImageTypeOBSVideo))
|
sourceTypeSelect.SetSelected(string(streamdconfig.DashboardSourceImageTypeOBSScreenshot))
|
||||||
|
|
||||||
saveButton := widget.NewButtonWithIcon("Save", theme.DocumentSaveIcon(), func() {
|
saveButton := widget.NewButtonWithIcon("Save", theme.DocumentSaveIcon(), func() {
|
||||||
if elementName.Text == "" {
|
if elementName.Text == "" {
|
||||||
@@ -1035,7 +1197,7 @@ func (p *Panel) editDashboardElementWindow(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch streamdconfig.DashboardSourceImageType(sourceTypeSelect.Selected) {
|
switch streamdconfig.DashboardSourceImageType(sourceTypeSelect.Selected) {
|
||||||
case streamdconfig.DashboardSourceImageTypeOBSVideo:
|
case streamdconfig.DashboardSourceImageTypeOBSScreenshot:
|
||||||
cfg.Source = obsVideoSource
|
cfg.Source = obsVideoSource
|
||||||
case streamdconfig.DashboardSourceImageTypeOBSVolume:
|
case streamdconfig.DashboardSourceImageTypeOBSVolume:
|
||||||
cfg.Source = obsVolumeSource
|
cfg.Source = obsVolumeSource
|
||||||
|
@@ -163,7 +163,11 @@ func imgFillTo(
|
|||||||
offset image.Point,
|
offset image.Point,
|
||||||
alignX streamdconsts.AlignX,
|
alignX streamdconsts.AlignX,
|
||||||
alignY streamdconsts.AlignY,
|
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()
|
sizeCur := src.Bounds().Size()
|
||||||
ratioCur := float64(sizeCur.X) / float64(sizeCur.Y)
|
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