Files
streamctl/pkg/streamd/config/monitor_source.go
2024-08-26 01:53:01 +01:00

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),
}
}