Skip to content

Commit

Permalink
chore: better filtergen
Browse files Browse the repository at this point in the history
  • Loading branch information
connerdouglass committed May 17, 2024
1 parent d129b8a commit 917a399
Show file tree
Hide file tree
Showing 284 changed files with 2,405 additions and 963 deletions.
55 changes: 55 additions & 0 deletions cmd/filtergen/filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ type FilterOption struct {
Type OptionType
Description string

Expression bool
InferredExpressionType OptionType

FlagEncodingParam bool
FlagDecodingParam bool
FlagFilteringParam bool
Expand Down Expand Up @@ -114,6 +117,10 @@ func getFilterInfo(ctx context.Context, filter *Filter) error {
if strings.TrimSpace(line) == "dynamic (depending on the options)" {
filter.NumOutputs = nil
break
} else if strings.TrimSpace(line) == "none (sink filter)" {
filter.NumOutputs = new(int)
*filter.NumOutputs = 0
break
} else {
if filter.NumOutputs == nil {
filter.NumOutputs = new(int)
Expand All @@ -138,6 +145,14 @@ func getFilterInfo(ctx context.Context, filter *Filter) error {
option.Type = OptionType(match[2])
option.Description = match[4]

option.Expression = option.Type == "string" && strings.Contains(option.Description, "expression")
if option.Expression {
inferred := inferExpressionType(option)
if inferred != "" {
option.InferredExpressionType = OptionType(inferred)
}
}

flags := match[3]
option.FlagEncodingParam = flags[0] == 'E'
option.FlagDecodingParam = flags[1] == 'D'
Expand Down Expand Up @@ -165,6 +180,46 @@ func getFilterInfo(ctx context.Context, filter *Filter) error {
}
}

if filter.FlagTimelineSupport {
filter.Options = append(filter.Options, FilterOption{
Name: "enable",
Type: "boolean",
Description: "expression to enable or disable the filter",
Expression: true,
})
}

// Parse the inputs
return nil
}

var (
defaultValueRegex = regexp.MustCompile(`\s+\(default "(.+)"\)$`)
integerRegex = regexp.MustCompile(`^\d+$`)
floatRegex = regexp.MustCompile(`^\d+\.\d+$`)
colorRegex = regexp.MustCompile(`^0x[0-9a-fA-F]{6,8}$`)
)

func inferExpressionType(option FilterOption) string {
defaultValueMatches := defaultValueRegex.FindStringSubmatch(option.Description)
if len(defaultValueMatches) == 0 {
return ""
}
defaultValue := defaultValueMatches[1]

if integerRegex.MatchString(defaultValue) {
return "int"
}
if floatRegex.MatchString(defaultValue) {
return "float"
}
if colorRegex.MatchString(defaultValue) {
return "color"
}

switch defaultValue {
case "iw", "ih", "(in_w-out_w)/2", "(in_h-out_h)/2":
return "int"
}
return ""
}
132 changes: 99 additions & 33 deletions cmd/filtergen/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,20 @@ import (
"strings"
)

// optionMappings creates better Go method names for some filter options that are ambiguous.
var optionMappings = map[string]map[string]string{
"hue": {
"h": "hue_degrees",
"H": "hue_radians",
"s": "saturation",
"b": "brightness",
},
"drawtext": {
"fontcolor_expr": "",
},
}

var excludedFilters = map[string]struct{}{
"abuffersink": {},
"buffersink": {},
}

func generateFilters(filters []Filter) error {
for _, filter := range filters {
if _, ok := excludedFilters[filter.Name]; ok {
// Exclude sink filters (abuffersink, buffersink)
if filter.NumOutputs != nil && *filter.NumOutputs == 0 {
continue
}
if err := generateFilter(filter); err != nil {
Expand Down Expand Up @@ -84,7 +78,7 @@ func writeFilterFile(file io.Writer, filter Filter) error {
stdlibImports := []string{}

// Compile a slice of all the option methods
var methods []optionMethod
var allMethods []optionMethod
for _, option := range filter.Options {
if option.Name == "outputs" {
continue
Expand All @@ -94,7 +88,27 @@ func writeFilterFile(file io.Writer, filter Filter) error {
continue
}
}
methods = append(methods, optionToMethods(filter, option)...)
allMethods = append(allMethods, optionToMethods(filter, option)...)
}

// Deduplicate the methods
var methods []optionMethod
methodsAdded := make(map[string]struct{})
for i, method := range allMethods {
if _, ok := methodsAdded[method.funcName]; ok {
continue
}
var hasHigherPriority bool
for j, other := range allMethods {
if i != j && other.funcName == method.funcName && other.priority > method.priority {
hasHigherPriority = true
break
}
}
if !hasHigherPriority {
methods = append(methods, method)
methodsAdded[method.funcName] = struct{}{}
}
}

// Combine the imports from all the methods
Expand Down Expand Up @@ -226,38 +240,84 @@ func ucfirst(str string) string {
}

type optionMethod struct {
option FilterOption
comment string
header string
toExpr string
imports []string
funcName string
option FilterOption
comment string
header string
toExpr string
imports []string
priority int
}

func optionToMethods(filter Filter, option FilterOption) []optionMethod {
var methods []optionMethod

// Add the base method
baseMethod, err := optionToMethod(filter, option)
if err != nil {
log.Printf("Filter (%s): %s", filter.Name, err)
} else {
methods = append(methods, baseMethod)
}

// If it supports expressions, add an expression-specific method
if option.FlagRuntimeParam {
exprMethod, err := optionExpressionToMethod(filter, option)
// The ffmpeg CLI doesn't tell us for sure if an option supports expression evaluation or not.
// We have to guess based on the option type and description.
// So we have three cases:
// 1. The option is definitely an expression.
// 2. The option might be an expression.
// 3. The option is definitely not an expression.

if option.Expression {
// If there is an inferred type
if option.InferredExpressionType != "" {
copiedOption := option
copiedOption.Type = option.InferredExpressionType
inferredExprMethod, err := optionToMethod(filter, copiedOption, "")
if err != nil {
log.Printf("Filter (%s): %s", filter.Name, err)
} else {
inferredExprMethod.priority = 0
methods = append(methods, inferredExprMethod)
}
}
// If it's definitely an expression, add one method:
// - "x" => X(expr.Expr)
suffix := ""
if option.InferredExpressionType != "" {
suffix = "Expr"
}
exprMethod, err := optionExpressionToMethod(filter, option, suffix)
if err != nil {
log.Printf("Filter (%s): %s", filter.Name, err)
} else {
exprMethod.priority = 1
methods = append(methods, exprMethod)
}
} else if option.FlagRuntimeParam {
// If it's maybe an expression, add both. For example:
// - Fontcolor(str string)
// - FontcolorExpr(expr.Expr)
baseMethod, err := optionToMethod(filter, option, "")
if err != nil {
log.Printf("Filter (%s): %s", filter.Name, err)
} else {
baseMethod.priority = 0
methods = append(methods, baseMethod)
}
exprMethod, err := optionExpressionToMethod(filter, option, "Expr")
if err != nil {
log.Printf("Filter (%s): %s", filter.Name, err)
} else {
exprMethod.priority = 0
methods = append(methods, exprMethod)
}
} else {
// If it's definitely not an expression
baseMethod, err := optionToMethod(filter, option, "")
if err != nil {
log.Printf("Filter (%s): %s", filter.Name, err)
} else {
baseMethod.priority = 1
methods = append(methods, baseMethod)
}
}
return methods
}

func optionToMethod(filter Filter, option FilterOption) (optionMethod, error) {
funcName := cleanFuncName(filter.Name, option.Name)
func optionToMethod(filter Filter, option FilterOption, suffix string) (optionMethod, error) {
funcName := cleanFuncName(filter.Name, option.Name) + suffix
argName := cleanArgName(snakeToCamel(option.Name))

goType, imports, err := option.Type.GoType()
Expand All @@ -270,6 +330,7 @@ func optionToMethod(filter Filter, option FilterOption) (optionMethod, error) {
}

var method optionMethod
method.funcName = funcName
method.option = option
method.comment = fmt.Sprintf("%s %s.", funcName, option.Description)
method.header = fmt.Sprintf("%s(%s %s)", funcName, argName, goType)
Expand All @@ -278,14 +339,15 @@ func optionToMethod(filter Filter, option FilterOption) (optionMethod, error) {
return method, nil
}

func optionExpressionToMethod(filter Filter, option FilterOption) (optionMethod, error) {
funcName := cleanFuncName(filter.Name, option.Name) + "Expr"
func optionExpressionToMethod(filter Filter, option FilterOption, suffix string) (optionMethod, error) {
funcName := cleanFuncName(filter.Name, option.Name) + suffix
argName := cleanArgName(snakeToCamel(option.Name))

goType := "expr.Expr"
toExpr := argName

var method optionMethod
method.funcName = funcName
method.option = option
method.comment = fmt.Sprintf("%s %s.", funcName, strings.TrimSuffix(option.Description, "."))
method.header = fmt.Sprintf("%s(%s %s)", funcName, argName, goType)
Expand All @@ -310,7 +372,7 @@ func (t OptionType) GoType() (string, []string, error) {
return "time.Duration", []string{"time"}, nil
case "boolean":
return "bool", nil, nil
case "string", "flags":
case "string":
return "string", nil, nil
case "image_size":
return "expr.Size", nil, nil
Expand All @@ -326,6 +388,8 @@ func (t OptionType) GoType() (string, []string, error) {
return "expr.ChannelLayout", nil, nil
case "dictionary":
return "expr.Dictionary", nil, nil
case "flags":
return "...string", nil, nil
default:
return "", nil, fmt.Errorf("unsupported option type: %s", t)
}
Expand All @@ -345,7 +409,7 @@ func (t OptionType) ToExpr(name string) (string, error) {
return fmt.Sprintf("expr.Duration(%s)", name), nil
case "boolean":
return fmt.Sprintf("expr.Bool(%s)", name), nil
case "string", "flags":
case "string":
return fmt.Sprintf("expr.String(%s)", name), nil
case "image_size":
return name, nil
Expand All @@ -361,6 +425,8 @@ func (t OptionType) ToExpr(name string) (string, error) {
return name, nil
case "dictionary":
return name, nil
case "flags":
return fmt.Sprintf("expr.Flags(%s)", name), nil
default:
return "", fmt.Errorf("unsupported option type: %s", t)
}
Expand Down
6 changes: 3 additions & 3 deletions examples/overlay/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@ func main() {
// Scale and rotate the foreground video
scaleAndRotate := spireav.Pipeline(
g.Filter(filters.Scale().WidthExpr(expr.Int(200)).HeightExpr(expr.Int(200))),
g.Filter(filters.Pad().WExpr(expr.Raw("max(iw,ih)*sqrt(2)")).HExpr(expr.Raw("max(ih,iw)*sqrt(2)")).X(-1).Y(-1).Color(expr.ColorTransparent)),
g.Filter(
filters.Rotate().
// AngleExpr("t*PI/4").
AngleExpr(expr.Div(expr.Mul(expr.Var("t"), expr.PI), expr.Int(4))).
Fillcolor(expr.ColorBlack.WithOpacity(0.0).String())),
Fillcolor(expr.ColorTransparent.String())),
)
foreground.Video(0).Pipe(scaleAndRotate, 0)

// Overlay the scaled and rotated foreground video on top of a solid background
overlayFilter := g.Filter(filters.Overlay().X("0").Y("0"))
overlayFilter := g.Filter(filters.Overlay().X(0).Y(0))
background := g.Filter(filters.Color().Color(expr.ColorRed).Duration(10 * time.Second).Size(expr.Size{Width: 1280, Height: 720}))
background.Pipe(overlayFilter, 0)
scaleAndRotate.Pipe(overlayFilter, 1)
Expand Down
62 changes: 62 additions & 0 deletions examples/timecode-overlay/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package main

import (
"context"
"fmt"
"time"

"github.com/spiretechnology/spireav"
"github.com/spiretechnology/spireav/filter/expr"
"github.com/spiretechnology/spireav/filter/filters"
"github.com/spiretechnology/spireav/output"
)

func main() {
// Create a new graph
g := spireav.New()
inputNode := g.Input("reference-media/BigBuckBunny.mp4")
outputNode := g.Output(
"reference-outputs/BigBuckBunny-timecode.mp4",
output.WithFormatMP4(),
)

// Overlay timecode on the video
timecode := g.Filter(
filters.Drawtext().
Timecode("00:00:00:00").
TimecodeRate(expr.RateFilm).
Fontcolor(expr.ColorWhite).
Box(true).
Boxcolor(expr.ColorBlack.WithOpacity(0.5)).
FontsizeExpr(expr.Div(expr.Var("h"), expr.Int(10))).
XExpr(expr.Div(expr.Sub(expr.Var("w"), expr.Var("tw")), expr.Int(2))).
YExpr(expr.Sub(expr.Var("h"), expr.Var("th"), expr.Int(20))),
)

// Pass the video into the pipeline
inputNode.Video(0).Pipe(timecode, 0)

// Pass the pipeline output to the output file
timecode.Pipe(outputNode, 0)

// Pass the audio directly to the output file
inputNode.Audio(0).Pipe(outputNode, 1)

// Create a progress handler function
progressFunc := func(progress spireav.Progress) {
fmt.Printf("%+v\n", progress)
}

// Create a context for the transcoding job
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
defer cancel()

// Create the process
runner := spireav.NewRunner(g, spireav.WithProgressCallback(progressFunc))

// Run the transcoding job
if err := runner.Run(ctx); err != nil {
fmt.Println(err.Error())
}
}
Loading

0 comments on commit 917a399

Please sign in to comment.