mirror of
https://github.com/xaionaro-go/streamctl.git
synced 2025-09-27 03:45:52 +08:00
305 lines
7.8 KiB
Go
305 lines
7.8 KiB
Go
package config
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"image/jpeg"
|
|
"image/png"
|
|
"math"
|
|
"time"
|
|
|
|
"github.com/chai2010/webp"
|
|
"github.com/facebookincubator/go-belt/tool/logger"
|
|
"github.com/xaionaro-go/obs-grpc-proxy/protobuf/go/obs_grpc"
|
|
"github.com/xaionaro-go/streamctl/pkg/colorx"
|
|
"github.com/xaionaro-go/streamctl/pkg/imgb64"
|
|
"github.com/xaionaro-go/streamctl/pkg/streamtypes"
|
|
)
|
|
|
|
type MonitorSourceType string
|
|
|
|
const (
|
|
MonitorSourceTypeUndefined = MonitorSourceType("")
|
|
MonitorSourceTypeDummy = MonitorSourceType("dummy")
|
|
MonitorSourceTypeOBSVideo = MonitorSourceType("obs_video")
|
|
MonitorSourceTypeOBSVolume = MonitorSourceType("obs_volume")
|
|
)
|
|
|
|
func (mst MonitorSourceType) New() Source {
|
|
switch mst {
|
|
case MonitorSourceTypeDummy:
|
|
return &MonitorSourceDummy{}
|
|
case MonitorSourceTypeOBSVideo:
|
|
return &MonitorSourceOBSVideo{}
|
|
case MonitorSourceTypeOBSVolume:
|
|
return &MonitorSourceOBSVolume{}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
type ImageFormat string
|
|
|
|
const (
|
|
ImageFormatUndefined = ImageFormat("")
|
|
ImageFormatPNG = ImageFormat("png")
|
|
ImageFormatJPEG = ImageFormat("jpeg")
|
|
ImageFormatWebP = ImageFormat("webp")
|
|
)
|
|
|
|
type GetImageByteser interface {
|
|
GetImageBytes(
|
|
ctx context.Context,
|
|
obsServer obs_grpc.OBSServer,
|
|
el MonitorElementConfig,
|
|
) ([]byte, string, time.Time, error)
|
|
}
|
|
|
|
type Duration time.Duration
|
|
|
|
func (d Duration) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(time.Duration(d).String())
|
|
}
|
|
|
|
func (d *Duration) UnmarshalJSON(b []byte) error {
|
|
var value any
|
|
if err := json.Unmarshal(b, &value); err != nil {
|
|
return fmt.Errorf("unable to un-JSON-ize '%s': %w", b, err)
|
|
}
|
|
|
|
switch value := value.(type) {
|
|
case float64:
|
|
*d = Duration(value)
|
|
return nil
|
|
case string:
|
|
duration, err := time.ParseDuration(value)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to parse '%s' as duration: %w", value, err)
|
|
}
|
|
*d = Duration(duration)
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("unexpected type: %T", value)
|
|
}
|
|
}
|
|
|
|
type MonitorSourceOBSVideo struct {
|
|
Name string `yaml:"name" json:"name"`
|
|
Width float64 `yaml:"width" json:"width"`
|
|
Height float64 `yaml:"height" json:"height"`
|
|
ImageFormat ImageFormat `yaml:"image_format" json:"image_format"`
|
|
UpdateInterval Duration `yaml:"update_interval" json:"update_interval"`
|
|
}
|
|
|
|
var _ Source = (*MonitorSourceOBSVideo)(nil)
|
|
var _ GetImageByteser = (*MonitorSourceOBSVideo)(nil)
|
|
|
|
func (*MonitorSourceOBSVideo) SourceType() MonitorSourceType {
|
|
return MonitorSourceTypeOBSVideo
|
|
}
|
|
|
|
func (s *MonitorSourceOBSVideo) GetImage(
|
|
ctx context.Context,
|
|
obsServer obs_grpc.OBSServer,
|
|
el MonitorElementConfig,
|
|
obsState *streamtypes.OBSState,
|
|
) (image.Image, time.Time, error) {
|
|
b, mimeType, nextUpdateTS, err := s.GetImageBytes(ctx, obsServer, el)
|
|
if err != nil {
|
|
return nil, nextUpdateTS, fmt.Errorf("unable to get the image from OBS: %w", err)
|
|
}
|
|
|
|
var img image.Image
|
|
switch mimeType {
|
|
case "image/png":
|
|
img, err = png.Decode(bytes.NewReader(b))
|
|
case "image/jpeg", "image/jpg":
|
|
img, err = jpeg.Decode(bytes.NewReader(b))
|
|
case "image/webp":
|
|
img, err = webp.Decode(bytes.NewReader(b))
|
|
default:
|
|
return nil, time.Time{}, fmt.Errorf("unexpected MIME type: '%s'", mimeType)
|
|
}
|
|
if err != nil {
|
|
return nil, time.Time{}, fmt.Errorf("unable to parse the image: %w", err)
|
|
}
|
|
|
|
return img, nextUpdateTS, nil
|
|
}
|
|
|
|
func (s *MonitorSourceOBSVideo) GetImageBytes(
|
|
ctx context.Context,
|
|
obsServer obs_grpc.OBSServer,
|
|
el MonitorElementConfig,
|
|
) ([]byte, string, time.Time, error) {
|
|
imageQuality := ptr(int64(el.ImageQuality))
|
|
if s.ImageFormat == ImageFormatJPEG && el.ImageQuality < 100 {
|
|
if s.ImageFormat == ImageFormatJPEG && el.ImageQuality < 50 {
|
|
imageQuality = ptr(int64(el.ImageQuality + 50))
|
|
} else {
|
|
imageQuality = ptr(int64(100))
|
|
}
|
|
}
|
|
|
|
req := &obs_grpc.GetSourceScreenshotRequest{
|
|
SourceName: &s.Name,
|
|
ImageFormat: []byte(s.ImageFormat),
|
|
ImageCompressionQuality: imageQuality,
|
|
}
|
|
if s.Width != 0 {
|
|
req.ImageWidth = ptr(int64(s.Width))
|
|
}
|
|
if s.Height != 0 {
|
|
req.ImageHeight = ptr(int64(s.Height))
|
|
}
|
|
if b, err := json.Marshal(req); err == nil {
|
|
logger.Tracef(ctx, "requesting a screenshot from OBS using %s", b)
|
|
}
|
|
resp, err := obsServer.GetSourceScreenshot(ctx, req)
|
|
if err != nil {
|
|
return nil, "", time.Now().Add(time.Second), fmt.Errorf("unable to get a screenshot of '%s': %w", s.Name, err)
|
|
}
|
|
|
|
imgB64 := resp.GetImageData()
|
|
imgBytes, mimeType, err := imgb64.Decode(string(imgB64))
|
|
if err != nil {
|
|
return nil, "", time.Time{}, fmt.Errorf("unable to decode the screenshot of '%s': %w", s.Name, err)
|
|
}
|
|
|
|
logger.Tracef(ctx, "the decoded image is of format '%s' (expected format: '%s') and size %d", mimeType, s.ImageFormat, len(imgBytes))
|
|
return imgBytes, mimeType, time.Now().Add(time.Duration(s.UpdateInterval)), nil
|
|
}
|
|
|
|
type MonitorSourceOBSVolume struct {
|
|
Name string `yaml:"name" json:"name"`
|
|
UpdateInterval Duration `yaml:"update_interval" json:"update_interval"`
|
|
ColorActive string `yaml:"color_active" json:"color_active"`
|
|
ColorPassive string `yaml:"color_passive" json:"color_passive"`
|
|
}
|
|
|
|
var _ Source = (*MonitorSourceOBSVolume)(nil)
|
|
|
|
func (s *MonitorSourceOBSVolume) GetImage(
|
|
ctx context.Context,
|
|
obsServer obs_grpc.OBSServer,
|
|
el MonitorElementConfig,
|
|
obsState *streamtypes.OBSState,
|
|
) (image.Image, time.Time, error) {
|
|
if obsState == nil {
|
|
return nil, time.Time{}, fmt.Errorf("obsState == nil")
|
|
}
|
|
obsState.Lock()
|
|
volumeMeters := obsState.VolumeMeters[s.Name]
|
|
obsState.Unlock()
|
|
|
|
if len(volumeMeters) == 0 {
|
|
return nil, time.Now().Add(time.Second), fmt.Errorf("no data for volume of '%s'", s.Name)
|
|
}
|
|
var volume float64
|
|
for _, s := range volumeMeters {
|
|
for _, cmp := range s {
|
|
volume = math.Max(volume, cmp)
|
|
}
|
|
}
|
|
|
|
img := image.NewRGBA(image.Rectangle{
|
|
Min: image.Point{
|
|
X: 0,
|
|
Y: 0,
|
|
},
|
|
Max: image.Point{
|
|
X: int(el.Width),
|
|
Y: int(el.Height),
|
|
},
|
|
})
|
|
|
|
colorActive, err := colorx.Parse(s.ColorActive)
|
|
if err != nil {
|
|
return nil, time.Time{}, fmt.Errorf("unable to parse the `color_active` value '%s': %w", s.ColorActive, err)
|
|
}
|
|
colorPassive, err := colorx.Parse(s.ColorPassive)
|
|
if err != nil {
|
|
return nil, time.Time{}, fmt.Errorf("unable to parse the `color_passive` value '%s': %w", s.ColorPassive, err)
|
|
}
|
|
|
|
size := img.Bounds().Size()
|
|
for x := 0; x < size.X; x++ {
|
|
volumeExpected := float64(x+1) / float64(size.X)
|
|
var c color.Color
|
|
if volumeExpected <= volume {
|
|
c = colorActive
|
|
} else {
|
|
c = colorPassive
|
|
}
|
|
for y := 0; y < size.Y; y++ {
|
|
img.Set(x, y, c)
|
|
}
|
|
}
|
|
|
|
return img, time.Now().Add(time.Duration(s.UpdateInterval)), nil
|
|
}
|
|
|
|
func (*MonitorSourceOBSVolume) SourceType() MonitorSourceType {
|
|
return MonitorSourceTypeOBSVolume
|
|
}
|
|
|
|
type MonitorSourceDummy struct{}
|
|
|
|
func (*MonitorSourceDummy) GetImage(
|
|
ctx context.Context,
|
|
obsServer obs_grpc.OBSServer,
|
|
el MonitorElementConfig,
|
|
obsState *streamtypes.OBSState,
|
|
) (image.Image, time.Time, error) {
|
|
img := image.NewRGBA(image.Rectangle{
|
|
Min: image.Point{
|
|
X: 0,
|
|
Y: 0,
|
|
},
|
|
Max: image.Point{
|
|
X: 0,
|
|
Y: 0,
|
|
},
|
|
})
|
|
return img, time.Time{}, nil
|
|
}
|
|
|
|
func (*MonitorSourceDummy) SourceType() MonitorSourceType {
|
|
return MonitorSourceTypeDummy
|
|
}
|
|
|
|
var _ Source = (*MonitorSourceDummy)(nil)
|
|
|
|
type Source interface {
|
|
GetImage(
|
|
ctx context.Context,
|
|
obsServer obs_grpc.OBSServer,
|
|
el MonitorElementConfig,
|
|
obsState *streamtypes.OBSState,
|
|
) (image.Image, time.Time, error)
|
|
|
|
SourceType() MonitorSourceType
|
|
}
|
|
|
|
type serializableSource struct {
|
|
Type MonitorSourceType `yaml:"type"`
|
|
Config map[string]any `yaml:"config,omitempty"`
|
|
}
|
|
|
|
func (s serializableSource) Unwrap() Source {
|
|
result := s.Type.New()
|
|
fromMap(s.Config, &result)
|
|
return result
|
|
}
|
|
|
|
func wrapSourceForYaml(source Source) serializableSource {
|
|
return serializableSource{
|
|
Type: source.SourceType(),
|
|
Config: toMap(source),
|
|
}
|
|
}
|