video run extraction
This commit is contained in:
+8
-204
@@ -5,7 +5,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -22,7 +21,6 @@ import (
|
|||||||
|
|
||||||
"mxl-pattern-generator/internal/audio"
|
"mxl-pattern-generator/internal/audio"
|
||||||
"mxl-pattern-generator/internal/flowdef"
|
"mxl-pattern-generator/internal/flowdef"
|
||||||
"mxl-pattern-generator/internal/generator"
|
|
||||||
"mxl-pattern-generator/internal/video"
|
"mxl-pattern-generator/internal/video"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -143,7 +141,7 @@ func validateFlowDefPath(label, path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateVideoArgs(args *appArgs) error {
|
func validateVideoArgs(args *appArgs) error {
|
||||||
if _, exists := patterns[args.pattern]; !exists {
|
if !video.HasPattern(args.pattern) {
|
||||||
return fmt.Errorf("unknown video pattern %q (use --list-patterns to see available patterns)", args.pattern)
|
return fmt.Errorf("unknown video pattern %q (use --list-patterns to see available patterns)", args.pattern)
|
||||||
}
|
}
|
||||||
if args.videoFlowDefFile != "" {
|
if args.videoFlowDefFile != "" {
|
||||||
@@ -223,76 +221,6 @@ func sortedMapKeys[V any](values map[string]V) string {
|
|||||||
return strings.Join(keys, ", ")
|
return strings.Join(keys, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
type pattern struct {
|
|
||||||
name string
|
|
||||||
description string
|
|
||||||
kernelPath string
|
|
||||||
motion bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var patterns = map[string]pattern{
|
|
||||||
"ebu75": {
|
|
||||||
name: "ebu75",
|
|
||||||
description: "EBU 75% Color Bar Signal",
|
|
||||||
kernelPath: "kernels/static/ebu75.wgsl",
|
|
||||||
motion: false,
|
|
||||||
},
|
|
||||||
"ebu75-move": {
|
|
||||||
name: "ebu75-move",
|
|
||||||
description: "EBU 75% Color Bar Signal with moving square",
|
|
||||||
kernelPath: "kernels/dynamic/ebu75.wgsl",
|
|
||||||
motion: true,
|
|
||||||
},
|
|
||||||
"ebu100": {
|
|
||||||
name: "ebu100",
|
|
||||||
description: "EBU 100% Color Bar Signal",
|
|
||||||
kernelPath: "kernels/static/ebu100.wgsl",
|
|
||||||
motion: false,
|
|
||||||
},
|
|
||||||
"ebu100-move": {
|
|
||||||
name: "ebu100-move",
|
|
||||||
description: "EBU 100% Color Bar Signal with moving square",
|
|
||||||
kernelPath: "kernels/dynamic/ebu100.wgsl",
|
|
||||||
motion: true,
|
|
||||||
},
|
|
||||||
"smpte": {
|
|
||||||
name: "smpte",
|
|
||||||
description: "SMPTE RP-219 Color Bar Signal",
|
|
||||||
kernelPath: "kernels/static/smpteBars.wgsl",
|
|
||||||
motion: false,
|
|
||||||
},
|
|
||||||
"smpte-move": {
|
|
||||||
name: "smpte-move",
|
|
||||||
description: "SMPTE RP-219 Color Bar Signal with moving square",
|
|
||||||
kernelPath: "kernels/dynamic/smpteBars.wgsl",
|
|
||||||
motion: true,
|
|
||||||
},
|
|
||||||
"gray-bars": {
|
|
||||||
name: "gray-bars",
|
|
||||||
description: "13-step grayscale bars (Y 64..940)",
|
|
||||||
kernelPath: "kernels/static/yBars.wgsl",
|
|
||||||
motion: false,
|
|
||||||
},
|
|
||||||
"gray-bars-move": {
|
|
||||||
name: "gray-bars-move",
|
|
||||||
description: "13-step grayscale bars (Y 64..940) with moving square",
|
|
||||||
kernelPath: "kernels/dynamic/yBars.wgsl",
|
|
||||||
motion: true,
|
|
||||||
},
|
|
||||||
"gray-ramp": {
|
|
||||||
name: "gray-ramp",
|
|
||||||
description: "Y gradient (black -> 100% white)",
|
|
||||||
kernelPath: "kernels/static/yRamp.wgsl",
|
|
||||||
motion: false,
|
|
||||||
},
|
|
||||||
"gray-ramp-move": {
|
|
||||||
name: "gray-ramp-move",
|
|
||||||
description: "Y gradient with moving square",
|
|
||||||
kernelPath: "kernels/dynamic/yRamp.wgsl",
|
|
||||||
motion: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var audioLevels = map[string]float64{
|
var audioLevels = map[string]float64{
|
||||||
"ebu": audio.LevelEBUDBFS,
|
"ebu": audio.LevelEBUDBFS,
|
||||||
"smpte": audio.LevelSMPTEDBFS,
|
"smpte": audio.LevelSMPTEDBFS,
|
||||||
@@ -300,19 +228,16 @@ var audioLevels = map[string]float64{
|
|||||||
|
|
||||||
func listPatterns(w io.Writer) {
|
func listPatterns(w io.Writer) {
|
||||||
fmt.Fprintln(w, "List of available video patterns:")
|
fmt.Fprintln(w, "List of available video patterns:")
|
||||||
names := make([]string, 0, len(patterns))
|
patterns := video.Patterns()
|
||||||
maxNameWidth := 0
|
maxNameWidth := 0
|
||||||
for name := range patterns {
|
for _, pattern := range patterns {
|
||||||
names = append(names, name)
|
l := len(pattern.Name)
|
||||||
l := len(name)
|
|
||||||
if l > maxNameWidth {
|
if l > maxNameWidth {
|
||||||
maxNameWidth = l
|
maxNameWidth = l
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sort.Strings(names)
|
for _, pattern := range patterns {
|
||||||
for _, name := range names {
|
fmt.Fprintf(w, " %-*s - %s\n", maxNameWidth, pattern.Name, pattern.Description)
|
||||||
p := patterns[name]
|
|
||||||
fmt.Fprintf(w, " %-*s - %s\n", maxNameWidth, name, p.description)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,7 +355,7 @@ func buildVideoConfig(args appArgs) (video.Config, error) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, ok := patterns[args.pattern]; !ok {
|
if !video.HasPattern(args.pattern) {
|
||||||
return video.Config{}, fmt.Errorf("unknown video pattern %q", args.pattern)
|
return video.Config{}, fmt.Errorf("unknown video pattern %q", args.pattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,7 +468,7 @@ func run(ctx context.Context, args appArgs) (runErr error) {
|
|||||||
{
|
{
|
||||||
name: "video",
|
name: "video",
|
||||||
run: func(ctx context.Context) error {
|
run: func(ctx context.Context) error {
|
||||||
return runVideo(ctx, inst, videoCfg)
|
return video.Run(ctx, inst, videoCfg)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -584,124 +509,3 @@ func runConcurrent(ctx context.Context, runners ...namedRunner) error {
|
|||||||
}
|
}
|
||||||
return resultErr
|
return resultErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func runVideo(ctx context.Context, inst *mxl.Instance, cfg video.Config) (runErr error) {
|
|
||||||
pattern, ok := patterns[cfg.Pattern]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("unknown video pattern %q", cfg.Pattern)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: fall back to a CPU generator if GPU initialization fails.
|
|
||||||
gen, err := generator.NewWGPUGenerator(cfg.Width(), cfg.Height(), pattern.kernelPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("initialize wgpu video generator: %w", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := gen.Close(); err != nil {
|
|
||||||
runErr = errors.Join(runErr, fmt.Errorf("close video generator: %w", err))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
overlay, err := buildTextOverlay(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
flowJSON, err := json.Marshal(cfg.Definition)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("marshal video flow definition: %w", err)
|
|
||||||
}
|
|
||||||
writer, isCreated, err := inst.NewWriter(string(flowJSON))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create video writer: %w", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := writer.Close(); err != nil {
|
|
||||||
runErr = errors.Join(runErr, fmt.Errorf("close video writer: %w", err))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
if !isCreated {
|
|
||||||
log.Printf("reusing existing video flow: %s", cfg.ID())
|
|
||||||
}
|
|
||||||
|
|
||||||
rate := writer.Config().Common.GrainRate
|
|
||||||
idx := mxl.CurrentIndex(rate)
|
|
||||||
log.Printf("writing video flow grainRate=%d/%d starting at idx=%d", rate.Num, rate.Den, idx)
|
|
||||||
|
|
||||||
var grainsWritten int64
|
|
||||||
var tick uint32 // Small animation counter; shaders cannot use the full grain index yet.
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
log.Printf("stopping video after %d grains", grainsWritten)
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
grain, err := writer.OpenGrain(idx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("open video grain %d: %w", idx, err)
|
|
||||||
}
|
|
||||||
if err := gen.GenerateFrame(grain.Payload, int(tick)); err != nil {
|
|
||||||
return cancelVideoGrain(grain, fmt.Errorf("generate frame for grain %d: %w", idx, err))
|
|
||||||
}
|
|
||||||
if overlay != nil {
|
|
||||||
if err := overlay.ApplyV210(grain.Payload); err != nil {
|
|
||||||
return cancelVideoGrain(grain, fmt.Errorf("apply text overlay to grain %d: %w", idx, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := grain.Commit(grain.TotalSlices, 0); err != nil {
|
|
||||||
return fmt.Errorf("commit video grain %d: %w", idx, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
grainsWritten++
|
|
||||||
idx++
|
|
||||||
tick++
|
|
||||||
if grainsWritten%100 == 0 {
|
|
||||||
log.Printf("video grains written=%d, next index=%d", grainsWritten, idx)
|
|
||||||
}
|
|
||||||
mxl.SleepNs(mxl.NsUntilIndex(idx, rate))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildTextOverlay(cfg video.Config) (overlay *generator.TextOverlay, resultErr error) {
|
|
||||||
if cfg.Overlay.Text == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
if cfg.Overlay.X < 0 || cfg.Overlay.X > int(cfg.Width()) ||
|
|
||||||
cfg.Overlay.Y < 0 || cfg.Overlay.Y > int(cfg.Height()) {
|
|
||||||
return nil, fmt.Errorf("text overlay position (%d, %d) is outside the %dx%d video frame",
|
|
||||||
cfg.Overlay.X, cfg.Overlay.Y, cfg.Width(), cfg.Height())
|
|
||||||
}
|
|
||||||
|
|
||||||
face, err := generator.LoadFace("assets/fonts/JetBrainsMonoNLNerdFontMono-Regular.ttf", 48)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("load text overlay font: %w", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := face.Close(); err != nil {
|
|
||||||
resultErr = errors.Join(resultErr, fmt.Errorf("close text overlay font: %w", err))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
overlay, err = generator.NewTextOverlay(
|
|
||||||
cfg.Overlay.Text,
|
|
||||||
int(cfg.Width()),
|
|
||||||
int(cfg.Height()),
|
|
||||||
cfg.Overlay.X,
|
|
||||||
cfg.Overlay.Y,
|
|
||||||
cfg.Overlay.Position,
|
|
||||||
face,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("create text overlay: %w", err)
|
|
||||||
}
|
|
||||||
return overlay, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func cancelVideoGrain(grain *mxl.GrainWriteAccess, cause error) error {
|
|
||||||
if err := grain.Cancel(); err != nil {
|
|
||||||
return errors.Join(cause, fmt.Errorf("cancel video grain: %w", err))
|
|
||||||
}
|
|
||||||
return cause
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package video
|
||||||
|
|
||||||
|
import "sort"
|
||||||
|
|
||||||
|
type PatternInfo struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
type pattern struct {
|
||||||
|
PatternInfo
|
||||||
|
kernelPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
var patterns = map[string]pattern{
|
||||||
|
"ebu75": {
|
||||||
|
PatternInfo: PatternInfo{Name: "ebu75", Description: "EBU 75% Color Bar Signal"},
|
||||||
|
kernelPath: "kernels/static/ebu75.wgsl",
|
||||||
|
},
|
||||||
|
"ebu75-move": {
|
||||||
|
PatternInfo: PatternInfo{Name: "ebu75-move", Description: "EBU 75% Color Bar Signal with moving square"},
|
||||||
|
kernelPath: "kernels/dynamic/ebu75.wgsl",
|
||||||
|
},
|
||||||
|
"ebu100": {
|
||||||
|
PatternInfo: PatternInfo{Name: "ebu100", Description: "EBU 100% Color Bar Signal"},
|
||||||
|
kernelPath: "kernels/static/ebu100.wgsl",
|
||||||
|
},
|
||||||
|
"ebu100-move": {
|
||||||
|
PatternInfo: PatternInfo{Name: "ebu100-move", Description: "EBU 100% Color Bar Signal with moving square"},
|
||||||
|
kernelPath: "kernels/dynamic/ebu100.wgsl",
|
||||||
|
},
|
||||||
|
"smpte": {
|
||||||
|
PatternInfo: PatternInfo{Name: "smpte", Description: "SMPTE RP-219 Color Bar Signal"},
|
||||||
|
kernelPath: "kernels/static/smpteBars.wgsl",
|
||||||
|
},
|
||||||
|
"smpte-move": {
|
||||||
|
PatternInfo: PatternInfo{Name: "smpte-move", Description: "SMPTE RP-219 Color Bar Signal with moving square"},
|
||||||
|
kernelPath: "kernels/dynamic/smpteBars.wgsl",
|
||||||
|
},
|
||||||
|
"gray-bars": {
|
||||||
|
PatternInfo: PatternInfo{Name: "gray-bars", Description: "13-step grayscale bars (Y 64..940)"},
|
||||||
|
kernelPath: "kernels/static/yBars.wgsl",
|
||||||
|
},
|
||||||
|
"gray-bars-move": {
|
||||||
|
PatternInfo: PatternInfo{Name: "gray-bars-move", Description: "13-step grayscale bars (Y 64..940) with moving square"},
|
||||||
|
kernelPath: "kernels/dynamic/yBars.wgsl",
|
||||||
|
},
|
||||||
|
"gray-ramp": {
|
||||||
|
PatternInfo: PatternInfo{Name: "gray-ramp", Description: "Y gradient (black -> 100% white)"},
|
||||||
|
kernelPath: "kernels/static/yRamp.wgsl",
|
||||||
|
},
|
||||||
|
"gray-ramp-move": {
|
||||||
|
PatternInfo: PatternInfo{Name: "gray-ramp-move", Description: "Y gradient with moving square"},
|
||||||
|
kernelPath: "kernels/dynamic/yRamp.wgsl",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func HasPattern(name string) bool {
|
||||||
|
_, ok := patterns[name]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func Patterns() []PatternInfo {
|
||||||
|
result := make([]PatternInfo, 0, len(patterns))
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
result = append(result, pattern.PatternInfo)
|
||||||
|
}
|
||||||
|
sort.Slice(result, func(i, j int) bool {
|
||||||
|
return result[i].Name < result[j].Name
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package video
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"mxl-pattern-generator/internal/generator"
|
||||||
|
|
||||||
|
"github.com/qvest-digital/go-mxl/mxl"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Run(ctx context.Context, inst *mxl.Instance, cfg Config) (runErr error) {
|
||||||
|
pattern, ok := patterns[cfg.Pattern]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unknown video pattern %q", cfg.Pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: fall back to a CPU generator if GPU initialization fails.
|
||||||
|
gen, err := generator.NewWGPUGenerator(cfg.Width(), cfg.Height(), pattern.kernelPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("initialize wgpu video generator: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := gen.Close(); err != nil {
|
||||||
|
runErr = errors.Join(runErr, fmt.Errorf("close video generator: %w", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
overlay, err := buildTextOverlay(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
flowJSON, err := json.Marshal(cfg.Definition)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal video flow definition: %w", err)
|
||||||
|
}
|
||||||
|
writer, isCreated, err := inst.NewWriter(string(flowJSON))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create video writer: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := writer.Close(); err != nil {
|
||||||
|
runErr = errors.Join(runErr, fmt.Errorf("close video writer: %w", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if !isCreated {
|
||||||
|
log.Printf("reusing existing video flow: %s", cfg.ID())
|
||||||
|
}
|
||||||
|
|
||||||
|
rate := writer.Config().Common.GrainRate
|
||||||
|
idx := mxl.CurrentIndex(rate)
|
||||||
|
log.Printf("writing video flow grainRate=%d/%d starting at idx=%d", rate.Num, rate.Den, idx)
|
||||||
|
|
||||||
|
var grainsWritten int64
|
||||||
|
var tick uint32 // Small animation counter; shaders cannot use the full grain index yet.
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.Printf("stopping video after %d grains", grainsWritten)
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
grain, err := writer.OpenGrain(idx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open video grain %d: %w", idx, err)
|
||||||
|
}
|
||||||
|
if err := gen.GenerateFrame(grain.Payload, int(tick)); err != nil {
|
||||||
|
return cancelGrain(grain, fmt.Errorf("generate frame for grain %d: %w", idx, err))
|
||||||
|
}
|
||||||
|
if overlay != nil {
|
||||||
|
if err := overlay.ApplyV210(grain.Payload); err != nil {
|
||||||
|
return cancelGrain(grain, fmt.Errorf("apply text overlay to grain %d: %w", idx, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := grain.Commit(grain.TotalSlices, 0); err != nil {
|
||||||
|
return fmt.Errorf("commit video grain %d: %w", idx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
grainsWritten++
|
||||||
|
idx++
|
||||||
|
tick++
|
||||||
|
if grainsWritten%100 == 0 {
|
||||||
|
log.Printf("video grains written=%d, next index=%d", grainsWritten, idx)
|
||||||
|
}
|
||||||
|
mxl.SleepNs(mxl.NsUntilIndex(idx, rate))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelGrain(grain *mxl.GrainWriteAccess, cause error) error {
|
||||||
|
if err := grain.Cancel(); err != nil {
|
||||||
|
return errors.Join(cause, fmt.Errorf("cancel video grain: %w", err))
|
||||||
|
}
|
||||||
|
return cause
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTextOverlay(cfg Config) (overlay *generator.TextOverlay, resultErr error) {
|
||||||
|
if cfg.Overlay.Text == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if cfg.Overlay.X < 0 || cfg.Overlay.X > int(cfg.Width()) ||
|
||||||
|
cfg.Overlay.Y < 0 || cfg.Overlay.Y > int(cfg.Height()) {
|
||||||
|
return nil, fmt.Errorf("text overlay position (%d, %d) is outside the %dx%d video frame",
|
||||||
|
cfg.Overlay.X, cfg.Overlay.Y, cfg.Width(), cfg.Height())
|
||||||
|
}
|
||||||
|
|
||||||
|
face, err := generator.LoadFace("assets/fonts/JetBrainsMonoNLNerdFontMono-Regular.ttf", 48)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load text overlay font: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := face.Close(); err != nil {
|
||||||
|
resultErr = errors.Join(resultErr, fmt.Errorf("close text overlay font: %w", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
overlay, err = generator.NewTextOverlay(
|
||||||
|
cfg.Overlay.Text,
|
||||||
|
int(cfg.Width()),
|
||||||
|
int(cfg.Height()),
|
||||||
|
cfg.Overlay.X,
|
||||||
|
cfg.Overlay.Y,
|
||||||
|
cfg.Overlay.Position,
|
||||||
|
face,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create text overlay: %w", err)
|
||||||
|
}
|
||||||
|
return overlay, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user