From df456311c3b10100c995b4ce997998bf3a5534b0 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Wed, 21 May 2025 10:49:31 +0200 Subject: [PATCH 01/11] Added avfilter --- pkg/avfilter/graph.go | 48 ++++++++++++++++++++++++++++++++++++++++ pkg/avfilter/opt.go | 31 ++++++++++++++++++++++++++ sys/ffmpeg71/avfilter.go | 1 + 3 files changed, 80 insertions(+) create mode 100644 pkg/avfilter/graph.go create mode 100644 pkg/avfilter/opt.go diff --git a/pkg/avfilter/graph.go b/pkg/avfilter/graph.go new file mode 100644 index 0000000..7fd82a4 --- /dev/null +++ b/pkg/avfilter/graph.go @@ -0,0 +1,48 @@ +package avfilter + +import ( + // Packages + media "github.com/mutablelogic/go-media" + ff "github.com/mutablelogic/go-media/sys/ffmpeg71" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type Graph struct { + ctx *ff.AVFilterGraph +} + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// Allocates a new filter graph and returns it. +func NewGraph() *Graph { + graph := new(Graph) + if ctx := ff.AVFilterGraph_alloc(); ctx == nil { + return nil + } else { + graph.ctx = ctx + } + // Return the graph + return graph +} + +// Frees the filter graph and all its resources. +func (g *Graph) Close() error { + if g.ctx != nil { + ff.AVFilterGraph_free(g.ctx) + g.ctx = nil + } + // Return success + return nil +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Add a graph described by the input, with optional inputs and outputs +func (g *Graph) Parse(desc string, opts ...Opt) error { + // TODO + return media.ErrNotImplemented +} diff --git a/pkg/avfilter/opt.go b/pkg/avfilter/opt.go new file mode 100644 index 0000000..e24e189 --- /dev/null +++ b/pkg/avfilter/opt.go @@ -0,0 +1,31 @@ +package avfilter + +import ( + "github.com/mutablelogic/go-media" + ff "github.com/mutablelogic/go-media/sys/ffmpeg71" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type opt struct { + inputs []*ff.AVFilterInOut + outputs []*ff.AVFilterInOut +} + +type Opt func(*opt) error + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +func WithInput(filter string) Opt { + return func(o *opt) error { + return media.ErrNotImplemented + } +} + +func WithOutput(filter string) Opt { + return func(o *opt) error { + return media.ErrNotImplemented + } +} diff --git a/sys/ffmpeg71/avfilter.go b/sys/ffmpeg71/avfilter.go index 15789b1..a4b2da7 100644 --- a/sys/ffmpeg71/avfilter.go +++ b/sys/ffmpeg71/avfilter.go @@ -22,6 +22,7 @@ type ( AVFilter C.AVFilter AVFilterFlag C.int AVFilterGraph C.AVFilterGraph + AVFilterInOut C.AVFilterInOut ) //////////////////////////////////////////////////////////////////////////////// From 33ef690f57ef1c0923ce16010e88779b7a16982e Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Wed, 21 May 2025 10:50:09 +0200 Subject: [PATCH 02/11] Updated --- pkg/avfilter/opt.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/avfilter/opt.go b/pkg/avfilter/opt.go index e24e189..8382628 100644 --- a/pkg/avfilter/opt.go +++ b/pkg/avfilter/opt.go @@ -18,13 +18,13 @@ type Opt func(*opt) error /////////////////////////////////////////////////////////////////////////////// // LIFECYCLE -func WithInput(filter string) Opt { +func WithInput(filter, name string) Opt { return func(o *opt) error { return media.ErrNotImplemented } } -func WithOutput(filter string) Opt { +func WithOutput(filter, name string) Opt { return func(o *opt) error { return media.ErrNotImplemented } From 95ed56337d8079b26da1e78a15da3602d65e954a Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 23 May 2025 08:41:12 +0200 Subject: [PATCH 03/11] Updated AVFilter --- sys/ffmpeg71/avfilter.go | 116 ++++++++++++++++++++++++++-- sys/ffmpeg71/avfilter_graph.go | 57 +++++++++++++- sys/ffmpeg71/avfilter_graph_test.go | 25 +++++- 3 files changed, 187 insertions(+), 11 deletions(-) diff --git a/sys/ffmpeg71/avfilter.go b/sys/ffmpeg71/avfilter.go index a4b2da7..05c2bb8 100644 --- a/sys/ffmpeg71/avfilter.go +++ b/sys/ffmpeg71/avfilter.go @@ -49,8 +49,8 @@ func (ctx *AVFilter) MarshalJSON() ([]byte, error) { Name string `json:"name"` Description string `json:"description"` Flags AVFilterFlag `json:"flags,omitzero"` - Inputs uint `json:"inputs,omitempty"` - Outputs uint `json:"outputs,omitempty"` + Inputs uint `json:"num_inputs,omitempty"` + Outputs uint `json:"num_outputs,omitempty"` } if ctx == nil { return json.Marshal(nil) @@ -59,11 +59,65 @@ func (ctx *AVFilter) MarshalJSON() ([]byte, error) { Name: ctx.Name(), Description: ctx.Description(), Flags: ctx.Flags(), - Inputs: ctx.Inputs(), - Outputs: ctx.Outputs(), + Inputs: ctx.NumInputs(), + Outputs: ctx.NumOutputs(), }) } +func (ctx *AVFilterContext) MarshalJSON() ([]byte, error) { + type j struct { + Name string `json:"name"` + Filter *AVFilter `json:"filter"` + Inputs uint `json:"num_inputs,omitempty"` + Outputs uint `json:"num_outputs,omitempty"` + } + if ctx == nil { + return json.Marshal(nil) + } + return json.Marshal(j{ + Name: ctx.Name(), + Filter: ctx.Filter(), + Inputs: ctx.NumInputs(), + Outputs: ctx.NumOutputs(), + }) +} + +func (ctx *AVFilterInOut) MarshalJSON() ([]byte, error) { + type j struct { + Name string `json:"name"` + Filter *AVFilterContext `json:"filter"` + Pad int `json:"pad"` + } + if ctx == nil { + return json.Marshal(nil) + } + return json.Marshal(j{ + Name: ctx.Name(), + Filter: ctx.Filter(), + Pad: ctx.Pad(), + }) +} + +func (ctx *AVFilterGraph) MarshalJSON() ([]byte, error) { + type j struct { + Graph string `json:"graph"` + } + if ctx == nil { + return json.Marshal(nil) + } + return json.Marshal(j{ + Graph: AVFilterGraph_dump(ctx), + }) +} + +func (ctx *AVFilterGraph) String() string { + data, err := json.MarshalIndent(ctx, "", " ") + if err != nil { + return err.Error() + } + return string(data) +} + func (ctx *AVFilter) String() string { data, err := json.MarshalIndent(ctx, "", " ") if err != nil { @@ -72,6 +126,14 @@ func (ctx *AVFilter) String() string { return string(data) } +func (ctx *AVFilterInOut) String() string { + data, err := json.MarshalIndent(ctx, "", " ") + if err != nil { + return err.Error() + } + return string(data) +} + //////////////////////////////////////////////////////////////////////////////// // AVFilter @@ -87,14 +149,33 @@ func (c *AVFilter) Flags() AVFilterFlag { return AVFilterFlag(c.flags) } -func (c *AVFilter) Inputs() uint { +func (c *AVFilter) NumInputs() uint { return AVFilter_inputs(c) } -func (c *AVFilter) Outputs() uint { +func (c *AVFilter) NumOutputs() uint { return AVFilter_outputs(c) } +//////////////////////////////////////////////////////////////////////////////// +// AVFilterContext + +func (c *AVFilterContext) Name() string { + return C.GoString(c.name) +} + +func (c *AVFilterContext) Filter() *AVFilter { + return (*AVFilter)(c.filter) +} + +func (c *AVFilterContext) NumInputs() uint { + return uint(c.nb_inputs) +} + +func (c *AVFilterContext) NumOutputs() uint { + return uint(c.nb_outputs) +} + //////////////////////////////////////////////////////////////////////////////// // AVFilterFlag @@ -137,3 +218,26 @@ func (v AVFilterFlag) FlagString() string { return fmt.Sprintf("AVFilterFlag(0x%08X)", uint32(v)) } } + +//////////////////////////////////////////////////////////////////////////////// +// AVFilterInOut + +func (c *AVFilterInOut) Name() string { + return C.GoString(c.name) +} + +func (c *AVFilterInOut) Filter() *AVFilterContext { + return (*AVFilterContext)(c.filter_ctx) +} + +func (c *AVFilterInOut) Pad() int { + return int(c.pad_idx) +} + +func (c *AVFilterInOut) Next() *AVFilterInOut { + return (*AVFilterInOut)(c.next) +} + +func (c *AVFilterInOut) SetNext(next *AVFilterInOut) { + c.next = (*C.AVFilterInOut)(next) +} diff --git a/sys/ffmpeg71/avfilter_graph.go b/sys/ffmpeg71/avfilter_graph.go index 28e71cf..803c116 100644 --- a/sys/ffmpeg71/avfilter_graph.go +++ b/sys/ffmpeg71/avfilter_graph.go @@ -1,5 +1,10 @@ package ffmpeg +import ( + "fmt" + "unsafe" +) + //////////////////////////////////////////////////////////////////////////////// // CGO @@ -8,10 +13,6 @@ package ffmpeg #include */ import "C" -import ( - "fmt" - "unsafe" -) //////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS @@ -25,6 +26,33 @@ func AVFilterGraph_free(graph *AVFilterGraph) { C.avfilter_graph_free(&ctx) } +// Check validity and configure all the links and formats in the graph. +func AVFilterGraph_config(graph *AVFilterGraph) error { + if err := AVError(C.avfilter_graph_config((*C.AVFilterGraph)(graph), nil)); err != 0 { + return fmt.Errorf("avfilter_graph_config: %w", err) + } + return nil +} + +// Dump the graph out. +func AVFilterGraph_dump(graph *AVFilterGraph) string { + if graph == nil { + return "" + } + + // Check if the graph is valid + if err := AVFilterGraph_config(graph); err != nil { + return err.Error() + } + + // Dump the graph + cStr := C.avfilter_graph_dump((*C.AVFilterGraph)(graph), nil) + defer C.free(unsafe.Pointer(cStr)) + + // Return the string + return C.GoString(cStr) +} + // Allocates and initializes a filter in a single step. // The filter instance is created from the filter and inited with the parameter args. func AVFilterGraph_create_filter(graph *AVFilterGraph, filter *AVFilter, name, args string) (*AVFilterContext, error) { @@ -38,3 +66,24 @@ func AVFilterGraph_create_filter(graph *AVFilterGraph, filter *AVFilter, name, a } return (*AVFilterContext)(ctx), nil } + +// Add a graph described by a string to a graph,returning inputs and outputs. Will return an error +// if not all inputs and outputs are specified. The inputs and outputs should be freed +// with AVFilterInOut_free() when no longer needed. +func AVFilterGraph_parse(graph *AVFilterGraph, filters string) ([]*AVFilterInOut, []*AVFilterInOut, error) { + var ins, outs *AVFilterInOut + + cFilters := C.CString(filters) + defer C.free(unsafe.Pointer(cFilters)) + if err := AVError(C.avfilter_graph_parse_ptr((*C.AVFilterGraph)(graph), cFilters, (**C.AVFilterInOut)(unsafe.Pointer(&ins)), (**C.AVFilterInOut)(unsafe.Pointer(&outs)), nil)); err != 0 { + AVFilterInOut_free(ins) + AVFilterInOut_free(outs) + return nil, nil, fmt.Errorf("avfilter_graph_parse: %w", err) + } + + // TODO: If ins is 0 and outs is 0, we return the linked list of inputs and outputs + // Or else we try again with the ins and outs + + // Return success + return AVFilterInOut_list(ins), AVFilterInOut_list(outs), nil +} diff --git a/sys/ffmpeg71/avfilter_graph_test.go b/sys/ffmpeg71/avfilter_graph_test.go index 5c428bf..3e6779e 100644 --- a/sys/ffmpeg71/avfilter_graph_test.go +++ b/sys/ffmpeg71/avfilter_graph_test.go @@ -26,9 +26,32 @@ func Test_avfilter_graph_001(t *testing.T) { assert.NotNil(filter) // Create a filter context - ctx, err := ff.AVFilterGraph_create_filter(graph, filter, "null", "") + ctx, err := ff.AVFilterGraph_create_filter(graph, filter, "zzz", "") assert.NoError(err) assert.NotNil(ctx) // We don't need to free the filter context, as it is freed when the graph is freed } + +func Test_avfilter_graph_002(t *testing.T) { + assert := assert.New(t) + graph := ff.AVFilterGraph_alloc() + assert.NotNil(graph) + defer ff.AVFilterGraph_free(graph) + + // Parse a filter graph, and return the inputs and outputs, which should be + // freed when the graph is freed + in, out, err := ff.AVFilterGraph_parse(graph, "[a]null[b]") + assert.NoError(err) + defer ff.AVFilterInOut_list_free(in) + defer ff.AVFilterInOut_list_free(out) + + // Configure the graph + err = ff.AVFilterGraph_config(graph) + if !assert.NoError(err) { + t.FailNow() + } + + t.Log("graph=", graph) + +} From fb567550bfd2bda90c6aee1aaa7d64afefaf9f06 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 23 May 2025 08:46:33 +0200 Subject: [PATCH 04/11] Updated avfilter --- pkg/avfilter/filter.go | 50 +++++++++++++++++++++ pkg/avfilter/filter_test.go | 22 +++++++++ pkg/avfilter/graph_test.go | 17 +++++++ sys/ffmpeg71/avfilter_inout.go | 69 +++++++++++++++++++++++++++++ sys/ffmpeg71/avfilter_inout_test.go | 36 +++++++++++++++ 5 files changed, 194 insertions(+) create mode 100644 pkg/avfilter/filter.go create mode 100644 pkg/avfilter/filter_test.go create mode 100644 pkg/avfilter/graph_test.go create mode 100644 sys/ffmpeg71/avfilter_inout.go create mode 100644 sys/ffmpeg71/avfilter_inout_test.go diff --git a/pkg/avfilter/filter.go b/pkg/avfilter/filter.go new file mode 100644 index 0000000..1a60cc5 --- /dev/null +++ b/pkg/avfilter/filter.go @@ -0,0 +1,50 @@ +package avfilter + +import ( + media "github.com/mutablelogic/go-media" + "github.com/mutablelogic/go-media/pkg/ffmpeg" + ff "github.com/mutablelogic/go-media/sys/ffmpeg71" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type Filter struct { + ctx *ff.AVFilter +} + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// Return all filters +func Filters() []media.Metadata { + result := make([]media.Metadata, 0, 100) + var opaque uintptr + for { + filter := ff.AVFilter_iterate(&opaque) + if filter == nil { + break + } + result = append(result, ffmpeg.NewMetadata(filter.Name(), &Filter{filter})) + } + return result +} + +// Create a new filter by name +func NewFilter(name string) *Filter { + if filter := ff.AVFilter_get_by_name(name); filter != nil { + return &Filter{ctx: filter} + } + return nil +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +func (f *Filter) Name() string { + return f.ctx.Name() +} + +func (f *Filter) Description() string { + return f.ctx.Description() +} diff --git a/pkg/avfilter/filter_test.go b/pkg/avfilter/filter_test.go new file mode 100644 index 0000000..bc8e129 --- /dev/null +++ b/pkg/avfilter/filter_test.go @@ -0,0 +1,22 @@ +package avfilter_test + +import ( + "testing" + + // Packages + avfilter "github.com/mutablelogic/go-media/pkg/avfilter" + assert "github.com/stretchr/testify/assert" +) + +func Test_filter_001(t *testing.T) { + assert := assert.New(t) + + filters := avfilter.Filters() + assert.NotNil(filters) + assert.NotEmpty(filters) + for _, filter := range filters { + filter2 := avfilter.NewFilter(filter.Key()) + assert.NotNil(filter2) + assert.Equal(filter2, filter.Any()) + } +} diff --git a/pkg/avfilter/graph_test.go b/pkg/avfilter/graph_test.go new file mode 100644 index 0000000..e41a0a5 --- /dev/null +++ b/pkg/avfilter/graph_test.go @@ -0,0 +1,17 @@ +package avfilter_test + +import ( + "testing" + + // Packages + avfilter "github.com/mutablelogic/go-media/pkg/avfilter" + assert "github.com/stretchr/testify/assert" +) + +func Test_graph_001(t *testing.T) { + assert := assert.New(t) + + graph := avfilter.NewGraph() + assert.NotNil(graph) + assert.NoError(graph.Close()) +} diff --git a/sys/ffmpeg71/avfilter_inout.go b/sys/ffmpeg71/avfilter_inout.go new file mode 100644 index 0000000..7ad3d0a --- /dev/null +++ b/sys/ffmpeg71/avfilter_inout.go @@ -0,0 +1,69 @@ +package ffmpeg + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavfilter +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Allocate a single AVFilterInOut entry, with name, filter context and pad index. +func AVFilterInOut_alloc(name string, filter *AVFilterContext, pad int) *AVFilterInOut { + inout := (*C.AVFilterInOut)(C.avfilter_inout_alloc()) + if inout == nil { + return nil + } + inout.name = C.CString(name) + inout.filter_ctx = (*C.AVFilterContext)(filter) + inout.pad_idx = C.int(pad) + inout.next = nil + return (*AVFilterInOut)(inout) +} + +// Free a single AVFilterInOut entry, including its name. +func AVFilterInOut_free(inout *AVFilterInOut) { + ctx := (*C.AVFilterInOut)(inout) + C.avfilter_inout_free(&ctx) +} + +// Link an array of AVFilterInOut entries together, and return the first entry. +// If the array is empty, or any entry is nil, nil is returned. +func AVFilterInOut_link(inout ...*AVFilterInOut) *AVFilterInOut { + if len(inout) == 0 { + return nil + } + for i := 0; i < len(inout)-1; i++ { + if inout[i] == nil { + return nil + } + inout[i].SetNext(inout[i+1]) + } + inout[len(inout)-1].SetNext(nil) + return inout[0] +} + +// Return an array of AVFilterInOut entries, given the first entry. +// Returns nil if the first entry is nil. +func AVFilterInOut_list(head *AVFilterInOut) []*AVFilterInOut { + if head == nil { + return nil + } + var result []*AVFilterInOut + for inout := head; inout != nil; inout = inout.Next() { + result = append(result, inout) + } + return result +} + +// Free an array of AVFilterInOut entries, given the first entry. +func AVFilterInOut_list_free(list []*AVFilterInOut) { + if len(list) == 0 { + return + } + AVFilterInOut_free(list[0]) +} diff --git a/sys/ffmpeg71/avfilter_inout_test.go b/sys/ffmpeg71/avfilter_inout_test.go new file mode 100644 index 0000000..c00e595 --- /dev/null +++ b/sys/ffmpeg71/avfilter_inout_test.go @@ -0,0 +1,36 @@ +package ffmpeg_test + +import ( + "testing" + + // Packages + ff "github.com/mutablelogic/go-media/sys/ffmpeg71" + assert "github.com/stretchr/testify/assert" +) + +func Test_avfilter_inout_000(t *testing.T) { + assert := assert.New(t) + + inout := ff.AVFilterInOut_alloc("in", nil, 0) + assert.NotNil(inout) + assert.Equal("in", inout.Name()) + assert.Equal(0, inout.Pad()) + assert.Nil(inout.Filter()) + assert.Nil(inout.Next()) + + t.Log("inout=", inout) + + ff.AVFilterInOut_free(inout) +} + +func Test_avfilter_inout_001(t *testing.T) { + assert := assert.New(t) + + head := ff.AVFilterInOut_link(ff.AVFilterInOut_alloc("in1", nil, 0), ff.AVFilterInOut_alloc("in2", nil, 0)) + assert.NotNil(head) + + arr := ff.AVFilterInOut_list(head) + assert.Equal(2, len(arr)) + + t.Log("arr=", arr) +} From cbf950834fbce85cad7816bc3e7b514c302f38e8 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 23 May 2025 10:06:11 +0200 Subject: [PATCH 05/11] Updated --- pkg/avfilter/graph.go | 40 ++++++++++++++++++++++++++++++++------ pkg/avfilter/graph_test.go | 12 ++++++++++++ 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/pkg/avfilter/graph.go b/pkg/avfilter/graph.go index 7fd82a4..49bd3c7 100644 --- a/pkg/avfilter/graph.go +++ b/pkg/avfilter/graph.go @@ -11,6 +11,8 @@ import ( type Graph struct { ctx *ff.AVFilterGraph + in []*ff.AVFilterInOut + out []*ff.AVFilterInOut } /////////////////////////////////////////////////////////////////////////////// @@ -28,8 +30,40 @@ func NewGraph() *Graph { return graph } +// Parse a graph description and return it. +func ParseGraph(desc string) (*Graph, error) { + graph := NewGraph() + if graph == nil { + return nil, media.ErrInternalError.With("failed to allocate filter graph") + } + + // Parse the graph, and set the inputs and outputs + in, out, err := ff.AVFilterGraph_parse(graph.ctx, desc) + if err != nil { + return nil, graph.Close() + } else { + graph.in = in + graph.out = out + } + + // Validate the graph + if err := ff.AVFilterGraph_config(graph.ctx); err != nil { + return nil, graph.Close() + } + + // Return the graph + return graph, nil +} + // Frees the filter graph and all its resources. func (g *Graph) Close() error { + // Free the inputs and outputs + if g.in != nil { + ff.AVFilterInOut_list_free(g.in) + } + if g.out != nil { + ff.AVFilterInOut_list_free(g.out) + } if g.ctx != nil { ff.AVFilterGraph_free(g.ctx) g.ctx = nil @@ -40,9 +74,3 @@ func (g *Graph) Close() error { /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS - -// Add a graph described by the input, with optional inputs and outputs -func (g *Graph) Parse(desc string, opts ...Opt) error { - // TODO - return media.ErrNotImplemented -} diff --git a/pkg/avfilter/graph_test.go b/pkg/avfilter/graph_test.go index e41a0a5..1b21f38 100644 --- a/pkg/avfilter/graph_test.go +++ b/pkg/avfilter/graph_test.go @@ -15,3 +15,15 @@ func Test_graph_001(t *testing.T) { assert.NotNil(graph) assert.NoError(graph.Close()) } + +func Test_graph_002(t *testing.T) { + assert := assert.New(t) + + graph, err := avfilter.ParseGraph("[a]null[b]") + if !assert.NoError(err) { + t.FailNow() + } + assert.NotNil(graph) + t.Log(graph) + assert.NoError(graph.Close()) +} From 6fbf6675000fe30066929e937f7555c1ba7afaa4 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 23 May 2025 10:07:22 +0200 Subject: [PATCH 06/11] Updated --- pkg/avfilter/graph.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/avfilter/graph.go b/pkg/avfilter/graph.go index 49bd3c7..a7c1c78 100644 --- a/pkg/avfilter/graph.go +++ b/pkg/avfilter/graph.go @@ -2,6 +2,8 @@ package avfilter import ( // Packages + "errors" + media "github.com/mutablelogic/go-media" ff "github.com/mutablelogic/go-media/sys/ffmpeg71" ) @@ -40,7 +42,7 @@ func ParseGraph(desc string) (*Graph, error) { // Parse the graph, and set the inputs and outputs in, out, err := ff.AVFilterGraph_parse(graph.ctx, desc) if err != nil { - return nil, graph.Close() + return nil, errors.Join(err, graph.Close()) } else { graph.in = in graph.out = out @@ -48,7 +50,7 @@ func ParseGraph(desc string) (*Graph, error) { // Validate the graph if err := ff.AVFilterGraph_config(graph.ctx); err != nil { - return nil, graph.Close() + return nil, errors.Join(err, graph.Close()) } // Return the graph @@ -60,9 +62,11 @@ func (g *Graph) Close() error { // Free the inputs and outputs if g.in != nil { ff.AVFilterInOut_list_free(g.in) + g.in = nil } if g.out != nil { ff.AVFilterInOut_list_free(g.out) + g.out = nil } if g.ctx != nil { ff.AVFilterGraph_free(g.ctx) From 53a1700b7cb5e099c651172281586d85a99191f3 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 23 May 2025 10:07:45 +0200 Subject: [PATCH 07/11] Updated --- pkg/avfilter/graph.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/avfilter/graph.go b/pkg/avfilter/graph.go index a7c1c78..1ec103a 100644 --- a/pkg/avfilter/graph.go +++ b/pkg/avfilter/graph.go @@ -1,9 +1,9 @@ package avfilter import ( - // Packages "errors" + // Packages media "github.com/mutablelogic/go-media" ff "github.com/mutablelogic/go-media/sys/ffmpeg71" ) From 5b5d19820b7839ce0f590fdfd295d101019bfbe6 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 23 May 2025 13:05:54 +0200 Subject: [PATCH 08/11] Updated --- Makefile | 39 ++++++++++++++++++----------- pkg/avfilter/graph.go | 3 +++ sys/ffmpeg71/avfilter_graph.go | 6 ++--- sys/ffmpeg71/avfilter_graph_test.go | 15 +++++------ 4 files changed, 38 insertions(+), 25 deletions(-) diff --git a/Makefile b/Makefile index 06c2b5f..efcc2f0 100755 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ # Paths to packages GO=$(shell which go) DOCKER=$(shell which docker) +PKG_CONFIG=$(shell which pkg-config) # Source version FFMPEG_VERSION=ffmpeg-7.1.1 @@ -63,13 +64,12 @@ ${BUILD_DIR}/${FFMPEG_VERSION}: # Configure ffmpeg .PHONY: ffmpeg-configure -ffmpeg-configure: mkdir ${BUILD_DIR}/${FFMPEG_VERSION} ffmpeg-dep +ffmpeg-configure: mkdir pkconfig-dep ${BUILD_DIR}/${FFMPEG_VERSION} ffmpeg-dep @echo "Configuring ${FFMPEG_VERSION} => ${PREFIX}" @cd ${BUILD_DIR}/${FFMPEG_VERSION} && ./configure \ - --enable-static --disable-doc --disable-programs \ + --disable-doc --disable-programs \ --prefix="$(shell realpath ${PREFIX})" \ - --pkg-config-flags="--static" \ - --extra-libs="-lpthread" \ + --enable-static --pkg-config="${PKG_CONFIG}" --pkg-config-flags="--static" --extra-libs="-lpthread" \ --enable-gpl --enable-nonfree ${FFMPEG_CONFIG} # Build ffmpeg @@ -198,6 +198,11 @@ go-dep: docker-dep: @test -f "${DOCKER}" && test -x "${DOCKER}" || (echo "Missing docker binary" && exit 1) +.PHONY: pkconfig-dep +pkconfig-dep: + @test -f "${PKG_CONFIG}" && test -x "${PKG_CONFIG}" || (echo "Missing pkg-config binary" && exit 1) + + .PHONY: mkdir mkdir: @echo Mkdir ${BUILD_DIR} @@ -218,15 +223,19 @@ clean: go-tidy # Check for FFmpeg dependencies .PHONY: ffmpeg-dep ffmpeg-dep: - $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists libass && echo "--enable-libass")) - $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists fdk-aac && echo "--enable-libfdk-aac")) - $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists lame && echo "--enable-libmp3lame")) - $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists freetype2 && echo "--enable-libfreetype")) - $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists theora && echo "--enable-libtheora")) - $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists vorbis && echo "--enable-libvorbis")) - $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists opus && echo "--enable-libopus")) - $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists x264 && echo "--enable-libx264")) - $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists x265 && echo "--enable-libx265")) - $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists xvid && echo "--enable-libxvid")) - $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists vpx && echo "--enable-libvpx")) + $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists libass && echo "--enable-libass")) + $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists fdk-aac && echo "--enable-libfdk-aac")) + $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists lame && echo "--enable-libmp3lame")) + $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists freetype2 && echo "--enable-libfreetype")) + $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists theora && echo "--enable-libtheora")) + $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists vorbis && echo "--enable-libvorbis")) + $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists opus && echo "--enable-libopus")) + $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists x264 && echo "--enable-libx264")) + $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists x265 && echo "--enable-libx265")) + $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists xvid && echo "--enable-libxvid")) + $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists vpx && echo "--enable-libvpx")) + $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists libgcrypt && echo "--enable-gcrypt")) + $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists aom && echo "--enable-libaom")) + $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists libbluray && echo "--enable-libbluray")) + $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists dav1d && echo "--enable-libdav1d")) @echo "FFmpeg configuration: $(FFMPEG_CONFIG)" diff --git a/pkg/avfilter/graph.go b/pkg/avfilter/graph.go index 1ec103a..6abd7ef 100644 --- a/pkg/avfilter/graph.go +++ b/pkg/avfilter/graph.go @@ -2,6 +2,7 @@ package avfilter import ( "errors" + "fmt" // Packages media "github.com/mutablelogic/go-media" @@ -48,6 +49,8 @@ func ParseGraph(desc string) (*Graph, error) { graph.out = out } + fmt.Println("graph=", graph) + // Validate the graph if err := ff.AVFilterGraph_config(graph.ctx); err != nil { return nil, errors.Join(err, graph.Close()) diff --git a/sys/ffmpeg71/avfilter_graph.go b/sys/ffmpeg71/avfilter_graph.go index 803c116..f987e67 100644 --- a/sys/ffmpeg71/avfilter_graph.go +++ b/sys/ffmpeg71/avfilter_graph.go @@ -73,17 +73,17 @@ func AVFilterGraph_create_filter(graph *AVFilterGraph, filter *AVFilter, name, a func AVFilterGraph_parse(graph *AVFilterGraph, filters string) ([]*AVFilterInOut, []*AVFilterInOut, error) { var ins, outs *AVFilterInOut + // Allocate cstring for the filter spec cFilters := C.CString(filters) defer C.free(unsafe.Pointer(cFilters)) + + // First attempt to parse the filter graph: returns any inputs and outputs if err := AVError(C.avfilter_graph_parse_ptr((*C.AVFilterGraph)(graph), cFilters, (**C.AVFilterInOut)(unsafe.Pointer(&ins)), (**C.AVFilterInOut)(unsafe.Pointer(&outs)), nil)); err != 0 { AVFilterInOut_free(ins) AVFilterInOut_free(outs) return nil, nil, fmt.Errorf("avfilter_graph_parse: %w", err) } - // TODO: If ins is 0 and outs is 0, we return the linked list of inputs and outputs - // Or else we try again with the ins and outs - // Return success return AVFilterInOut_list(ins), AVFilterInOut_list(outs), nil } diff --git a/sys/ffmpeg71/avfilter_graph_test.go b/sys/ffmpeg71/avfilter_graph_test.go index 3e6779e..be7430c 100644 --- a/sys/ffmpeg71/avfilter_graph_test.go +++ b/sys/ffmpeg71/avfilter_graph_test.go @@ -46,12 +46,13 @@ func Test_avfilter_graph_002(t *testing.T) { defer ff.AVFilterInOut_list_free(in) defer ff.AVFilterInOut_list_free(out) - // Configure the graph - err = ff.AVFilterGraph_config(graph) - if !assert.NoError(err) { - t.FailNow() - } - t.Log("graph=", graph) - + t.Log("in=", in) + t.Log("out=", out) + + // One input and one output + assert.Len(in, 1) + assert.Equal("a", in[0].Name()) + assert.Len(out, 1) + assert.Equal("b", out[0].Name()) } From f31b51e9f61201fa7c5685c3f87ff1edeab9d32e Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 23 May 2025 15:57:43 +0200 Subject: [PATCH 09/11] Added metadata and codec --- Makefile | 6 +- pkg/avcodec/codec.go | 285 ++++++++++++++++++++++++++++++++++ pkg/avcodec/codec_test.go | 46 ++++++ pkg/avcodec/doc.go | 5 + pkg/avcodec/opt.go | 98 ++++++++++++ pkg/metadata/metadata.go | 131 ++++++++++++++++ pkg/metadata/metadata_test.go | 67 ++++++++ sys/ffmpeg71/avcodec.go | 6 +- 8 files changed, 637 insertions(+), 7 deletions(-) create mode 100644 pkg/avcodec/codec.go create mode 100644 pkg/avcodec/codec_test.go create mode 100644 pkg/avcodec/doc.go create mode 100644 pkg/avcodec/opt.go create mode 100644 pkg/metadata/metadata.go create mode 100644 pkg/metadata/metadata_test.go diff --git a/Makefile b/Makefile index efcc2f0..49d043a 100755 --- a/Makefile +++ b/Makefile @@ -161,14 +161,12 @@ test-ffmpeg: go-dep go-tidy ffmpeg chromaprint @${CGO_ENV} ${GO} test ./pkg/segmenter @echo ... test pkg/chromaprint @${CGO_ENV} ${GO} test ./pkg/chromaprint + @echo ... test pkg/avcodec + ${CGO_ENV} ${GO} test ./pkg/avcodec # @echo ... test pkg/ffmpeg # @${GO} test -v ./pkg/ffmpeg -# @echo ... test sys/chromaprint -# @${GO} test ./sys/chromaprint -# @echo ... test pkg/chromaprint -# @${GO} test ./pkg/chromaprint # @echo ... test pkg/file # @${GO} test ./pkg/file # @echo ... test pkg/generator diff --git a/pkg/avcodec/codec.go b/pkg/avcodec/codec.go new file mode 100644 index 0000000..c1eb762 --- /dev/null +++ b/pkg/avcodec/codec.go @@ -0,0 +1,285 @@ +package avcodec + +import ( + "encoding/json" + + // Packages + + media "github.com/mutablelogic/go-media" + metadata "github.com/mutablelogic/go-media/pkg/metadata" + ff "github.com/mutablelogic/go-media/sys/ffmpeg71" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type Codec struct { + codec *ff.AVCodec + context *ff.AVCodecContext +} + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// Return all codecs. Codecs can be either encoders or decoders. The argument +// can be ANY or any combination of INPUT, OUTPUT, VIDEO, AUDIO and SUBTITLE, +// in order to return a subset of codecs. +func Codecs(t media.Type) []media.Metadata { + result := make([]media.Metadata, 0, 100) + var opaque uintptr + for { + codec := ff.AVCodec_iterate(&opaque) + if codec == nil { + break + } + // Filter codecs by type + if t.Is(media.INPUT) && !ff.AVCodec_is_decoder(codec) { + continue + } + if t.Is(media.OUTPUT) && !ff.AVCodec_is_encoder(codec) { + continue + } + if t.Is(media.VIDEO) && codec.Type() != ff.AVMEDIA_TYPE_VIDEO { + continue + } + if t.Is(media.AUDIO) && codec.Type() != ff.AVMEDIA_TYPE_AUDIO { + continue + } + if t.Is(media.SUBTITLE) && codec.Type() != ff.AVMEDIA_TYPE_SUBTITLE { + continue + } + if codec.Capabilities().Is(ff.AV_CODEC_CAP_EXPERIMENTAL) { + // Skip experimental codecs + continue + } + result = append(result, metadata.New(codec.Name(), &Codec{codec, nil})) + } + return result +} + +// Return an encoder by name, with additional options. Call Close() to +// release the codec context. Codec options are listed at +// +func NewEncoder(name string, opts ...Opt) (*Codec, error) { + ctx := new(Codec) + + // Options + o, err := applyOptions(opts) + if err != nil { + return nil, err + } + + // Codec context + if codec := ff.AVCodec_find_encoder_by_name(name); codec == nil { + return nil, media.ErrBadParameter.Withf("unknown codec %q", name) + } else if context := ff.AVCodec_alloc_context(codec); context == nil { + return nil, media.ErrInternalError.Withf("failed to allocate codec context for %q", name) + } else if err := set_par(context, codec, o); err != nil { + ff.AVCodec_free_context(context) + return nil, err + } else if ff.AVCodec_open(context, codec, nil); err != nil { + ff.AVCodec_free_context(context) + return nil, err + } else { + ctx.context = context + } + + // Return success + return ctx, nil +} + +// Return a decoder by name, with additional options. Call Close() to +// release the codec context. Codec options are listed at +// +func NewDecoder(name string, opts ...Opt) (*Codec, error) { + ctx := new(Codec) + + // Options + o, err := applyOptions(opts) + if err != nil { + return nil, err + } + + // Codec context + if codec := ff.AVCodec_find_decoder_by_name(name); codec == nil { + return nil, media.ErrBadParameter.Withf("unknown codec %q", name) + } else if context := ff.AVCodec_alloc_context(codec); context == nil { + return nil, media.ErrInternalError.Withf("failed to allocate codec context for %q", name) + } else if err := set_par(context, codec, o); err != nil { + ff.AVCodec_free_context(context) + return nil, err + } else if ff.AVCodec_open(context, codec, nil); err != nil { + ff.AVCodec_free_context(context) + return nil, err + } else { + ctx.context = context + } + + // Return success + return ctx, nil +} + +// Release the codec resources +func (ctx *Codec) Close() error { + ctx.codec = nil + if ctx != nil && ctx.context != nil { + ff.AVCodec_free_context(ctx.context) + ctx.context = nil + } + return nil +} + +//////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (ctx *Codec) MarshalJSON() ([]byte, error) { + if ctx != nil && ctx.codec != nil { + return ctx.codec.MarshalJSON() + } + if ctx != nil && ctx.context != nil { + return ctx.context.MarshalJSON() + } + return []byte("null"), nil +} + +func (ctx *Codec) String() string { + data, err := json.MarshalIndent(ctx, "", " ") + if err != nil { + return err.Error() + } + return string(data) +} + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +func (ctx *Codec) Type() media.Type { + var t media.Type + switch { + case ctx != nil && ctx.codec != nil: + if ff.AVCodec_is_decoder(ctx.codec) { + t |= media.INPUT + } + if ff.AVCodec_is_encoder(ctx.codec) { + t |= media.OUTPUT + } + t |= type2type(ctx.codec.Type()) + case ctx != nil && ctx.context != nil: + if ff.AVCodec_is_decoder(ctx.context.Codec()) { + t |= media.INPUT + } + if ff.AVCodec_is_encoder(ctx.context.Codec()) { + t |= media.OUTPUT + } + t |= type2type(ctx.context.Codec().Type()) + } + return t +} + +func (ctx *Codec) Name() string { + switch { + case ctx != nil && ctx.codec != nil: + return ctx.codec.Name() + case ctx != nil && ctx.context != nil: + return ctx.context.Codec().Name() + } + return "" +} + +//////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +func type2type(t ff.AVMediaType) media.Type { + switch t { + case ff.AVMEDIA_TYPE_AUDIO: + return media.AUDIO + case ff.AVMEDIA_TYPE_VIDEO: + return media.VIDEO + case ff.AVMEDIA_TYPE_SUBTITLE: + return media.SUBTITLE + case ff.AVMEDIA_TYPE_ATTACHMENT: + return media.DATA + case ff.AVMEDIA_TYPE_DATA: + return media.DATA + } + return media.NONE +} + +func set_par(ctx *ff.AVCodecContext, codec *ff.AVCodec, opt *opt) error { + switch codec.Type() { + case ff.AVMEDIA_TYPE_AUDIO: + return set_audio_par(ctx, codec, opt) + case ff.AVMEDIA_TYPE_VIDEO: + return set_video_par(ctx, codec, opt) + case ff.AVMEDIA_TYPE_SUBTITLE: + return set_subtitle_par(ctx, codec, opt) + } + return nil +} + +func set_audio_par(ctx *ff.AVCodecContext, codec *ff.AVCodec, opt *opt) error { + // Channel layout + if ff.AVUtil_channel_layout_check(&opt.channel_layout) { + ctx.SetChannelLayout(opt.channel_layout) + } else if supported_layouts := codec.ChannelLayouts(); len(supported_layouts) > 0 { + ctx.SetChannelLayout(supported_layouts[0]) + } else { + ctx.SetChannelLayout(ff.AV_CHANNEL_LAYOUT_MONO) + } + + // Sample format + if opt.sample_format != ff.AV_SAMPLE_FMT_NONE { + ctx.SetSampleFormat(opt.sample_format) + } else if supported_formats := codec.SampleFormats(); len(supported_formats) > 0 { + ctx.SetSampleFormat(supported_formats[0]) + } + + // Sample rate + if opt.sample_rate > 0 { + ctx.SetSampleRate(opt.sample_rate) + } else if supported_rates := codec.SupportedSamplerates(); len(supported_rates) > 0 { + ctx.SetSampleRate(supported_rates[0]) + } + + // TODO: Time base + ctx.SetTimeBase(ff.AVUtil_rational(1, ctx.SampleRate())) + + return nil +} + +func set_video_par(ctx *ff.AVCodecContext, codec *ff.AVCodec, opt *opt) error { + // Pixel Format + if opt.pixel_format != ff.AV_PIX_FMT_NONE { + ctx.SetPixFmt(opt.pixel_format) + } else if supported_formats := codec.PixelFormats(); len(supported_formats) > 0 { + ctx.SetPixFmt(supported_formats[0]) + } else { + ctx.SetPixFmt(ff.AV_PIX_FMT_YUV420P) + } + + // Frame size + if opt.width > 0 { + ctx.SetWidth(opt.width) + } + if opt.height > 0 { + ctx.SetHeight(opt.height) + } + + // Frame rate + if !opt.frame_rate.IsZero() { + ctx.SetFramerate(opt.frame_rate) + } else if supported_rates := codec.SupportedFramerates(); len(supported_rates) > 0 { + ctx.SetFramerate(supported_rates[0]) + } + + // Time base + if frame_rate := ctx.Framerate(); !frame_rate.IsZero() { + ctx.SetTimeBase(ff.AVUtil_rational_invert(frame_rate)) + } + + return nil +} + +func set_subtitle_par(ctx *ff.AVCodecContext, codec *ff.AVCodec, opt *opt) error { + return nil +} diff --git a/pkg/avcodec/codec_test.go b/pkg/avcodec/codec_test.go new file mode 100644 index 0000000..84c47fa --- /dev/null +++ b/pkg/avcodec/codec_test.go @@ -0,0 +1,46 @@ +package avcodec_test + +import ( + "testing" + + // Packages + media "github.com/mutablelogic/go-media" + avcodec "github.com/mutablelogic/go-media/pkg/avcodec" + assert "github.com/stretchr/testify/assert" +) + +func Test_codec_001(t *testing.T) { + assert := assert.New(t) + + codecs := avcodec.Codecs(media.OUTPUT | media.AUDIO) + assert.NotNil(codecs) + for _, meta := range codecs { + assert.NotNil(meta) + codec, err := avcodec.NewEncoder(meta.Key()) + defer codec.Close() + + if assert.NoError(err) { + assert.NotNil(codec) + assert.Equal(media.AUDIO, codec.Type()) + t.Log(codec) + } + } +} + +func Test_codec_002(t *testing.T) { + assert := assert.New(t) + + codecs := avcodec.Codecs(media.OUTPUT | media.VIDEO) + assert.NotNil(codecs) + for _, meta := range codecs { + assert.NotNil(meta) + codec, err := avcodec.NewEncoder(meta.Key()) + defer codec.Close() + + if assert.NoError(err) { + assert.NotNil(codec) + assert.Equal(media.VIDEO, codec.Type()) + t.Log(codec) + } + } +} diff --git a/pkg/avcodec/doc.go b/pkg/avcodec/doc.go new file mode 100644 index 0000000..70e65ac --- /dev/null +++ b/pkg/avcodec/doc.go @@ -0,0 +1,5 @@ +/* +avcodec is roughly a wrapper around FFmpeg's libavcodec library, which provides encoding, +decoding and transcoding of audio and video streams. +*/ +package avcodec diff --git a/pkg/avcodec/opt.go b/pkg/avcodec/opt.go new file mode 100644 index 0000000..af281dc --- /dev/null +++ b/pkg/avcodec/opt.go @@ -0,0 +1,98 @@ +package avcodec + +import ( + // Packages + media "github.com/mutablelogic/go-media" + ff "github.com/mutablelogic/go-media/sys/ffmpeg71" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type opt struct { + // Audio parameters + channel_layout ff.AVChannelLayout + sample_format ff.AVSampleFormat + sample_rate int + + // Video parameters + width, height int + pixel_format ff.AVPixelFormat + frame_rate ff.AVRational + pixel_ratio ff.AVRational +} + +type Opt func(*opt) error + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +func applyOptions(opts []Opt) (*opt, error) { + o := new(opt) + o.sample_format = ff.AV_SAMPLE_FMT_NONE + o.pixel_format = ff.AV_PIX_FMT_NONE + for _, opt := range opts { + if err := opt(o); err != nil { + return nil, err + } + } + return o, nil +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +func WithChannelLayout(v string) Opt { + return func(o *opt) error { + if err := ff.AVUtil_channel_layout_from_string(&o.channel_layout, v); err != nil { + return media.ErrBadParameter.Withf("unknown channel layout %q", v) + } + return nil + } +} + +func WithSampleFormat(v string) Opt { + return func(o *opt) error { + if samplefmt := ff.AVUtil_get_sample_fmt(v); samplefmt == ff.AV_SAMPLE_FMT_NONE { + return media.ErrBadParameter.Withf("unknown sample format %q", v) + } else { + o.sample_format = samplefmt + } + return nil + } + +} + +func WithSampleRate(v int) Opt { + return func(o *opt) error { + if v <= 0 { + return media.ErrBadParameter.Withf("negative or zero samplerate %v", v) + } else { + o.sample_rate = v + } + return nil + } +} + +func WithPixelFormat(v string) Opt { + return func(o *opt) error { + if pixfmt := ff.AVUtil_get_pix_fmt(v); pixfmt == ff.AV_PIX_FMT_NONE { + return media.ErrBadParameter.Withf("unknown pixel format %q", v) + } else { + o.pixel_format = pixfmt + } + return nil + } +} + +func WithFrameSize(v string) Opt { + return func(o *opt) error { + if width, height, err := ff.AVUtil_parse_video_size(v); err != nil { + return media.ErrBadParameter.Withf("size %q", v) + } else { + o.width = width + o.height = height + } + return nil + } +} diff --git a/pkg/metadata/metadata.go b/pkg/metadata/metadata.go new file mode 100644 index 0000000..08fd9dd --- /dev/null +++ b/pkg/metadata/metadata.go @@ -0,0 +1,131 @@ +package metadata + +import ( + "bytes" + "encoding/json" + "fmt" + "image" + + // Packages + media "github.com/mutablelogic/go-media" + file "github.com/mutablelogic/go-media/pkg/file" +) + +//////////////////////////////////////////////////////////////////////////////// +// TYPES + +type meta struct { + Key string `json:"key"` + Value any `json:"value,omitempty"` +} + +type Metadata struct { + meta +} + +var _ media.Metadata = (*Metadata)(nil) + +//////////////////////////////////////////////////////////////////////////////// +// GLOBALS + +const ( + MetaArtwork = "artwork" // Metadata key for artwork, set the value as []byte +) + +//////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// Create a new metadata object +func New(key string, value any) *Metadata { + return &Metadata{ + meta: meta{ + Key: key, + Value: value, + }, + } +} + +//////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (m *Metadata) MarshalJSON() ([]byte, error) { + type j struct { + Key string `json:"key"` + Value any `json:"value,omitempty"` + } + if m == nil { + return json.Marshal(nil) + } + return json.Marshal(j{ + Key: m.Key(), + Value: m.Any(), + }) +} + +func (m *Metadata) String() string { + data, err := json.MarshalIndent(m, "", " ") + if err != nil { + return err.Error() + } + return string(data) +} + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +func (m *Metadata) Key() string { + return m.meta.Key +} + +// Value returns the value as a string. If the value is a byte slice, it will +// return the mimetype of the byte slice. +func (m *Metadata) Value() string { + if m.meta.Value == nil { + return "" + } + switch v := m.meta.Value.(type) { + case string: + return v + case []byte: + if mimetype, _, err := file.MimeType(v); err == nil { + return mimetype + } else { + return "" + } + default: + return fmt.Sprint(v) + } +} + +// Returns the value as a byte slice +func (m *Metadata) Bytes() []byte { + if m.meta.Value == nil { + return nil + } + switch v := m.meta.Value.(type) { + case []byte: + return v + case string: + return []byte(v) + } + return nil +} + +// Returns the value as an image +func (m *Metadata) Image() image.Image { + if m.meta.Value == nil { + return nil + } + switch v := m.meta.Value.(type) { + case []byte: + if img, _, err := image.Decode(bytes.NewReader(v)); err == nil { + return img + } + } + return nil +} + +// Returns the value as an interface +func (m *Metadata) Any() any { + return m.meta.Value +} diff --git a/pkg/metadata/metadata_test.go b/pkg/metadata/metadata_test.go new file mode 100644 index 0000000..be1b4c9 --- /dev/null +++ b/pkg/metadata/metadata_test.go @@ -0,0 +1,67 @@ +package metadata_test + +import ( + "os" + "testing" + + // Packages + metadata "github.com/mutablelogic/go-media/pkg/metadata" + assert "github.com/stretchr/testify/assert" +) + +func Test_metadata_001(t *testing.T) { + assert := assert.New(t) + + // Create a metadata + metadata := metadata.New("test", "test") + if !assert.NotNil(metadata) { + t.FailNow() + } + assert.Equal("test", metadata.Key()) + assert.Equal("test", metadata.Value()) +} +func Test_metadata_002(t *testing.T) { + assert := assert.New(t) + + data, err := os.ReadFile("../../etc/test/sample.png") + if !assert.NoError(err) { + t.FailNow() + } + + // Create a metadata + meta := metadata.New(metadata.MetaArtwork, data) + if !assert.NotNil(meta) { + t.FailNow() + } + assert.Equal(metadata.MetaArtwork, meta.Key()) + assert.Equal("image/png", meta.Value()) + assert.Equal(data, meta.Bytes()) + + image := meta.Image() + if !assert.NotNil(image) { + t.FailNow() + } +} + +func Test_metadata_003(t *testing.T) { + assert := assert.New(t) + + data, err := os.ReadFile("../../etc/test/sample.jpg") + if !assert.NoError(err) { + t.FailNow() + } + + // Create a metadata + meta := metadata.New(metadata.MetaArtwork, data) + if !assert.NotNil(meta) { + t.FailNow() + } + assert.Equal(metadata.MetaArtwork, meta.Key()) + assert.Equal("image/jpeg", meta.Value()) + assert.Equal(data, meta.Bytes()) + + image := meta.Image() + if !assert.NotNil(image) { + t.FailNow() + } +} diff --git a/sys/ffmpeg71/avcodec.go b/sys/ffmpeg71/avcodec.go index 78f3dcd..93890a6 100644 --- a/sys/ffmpeg71/avcodec.go +++ b/sys/ffmpeg71/avcodec.go @@ -56,11 +56,11 @@ type jsonAVCodecContext struct { PixelFormat AVPixelFormat `json:"pix_fmt,omitempty"` Width int `json:"width,omitempty"` Height int `json:"height,omitempty"` - SampleAspectRatio AVRational `json:"sample_aspect_ratio,omitempty"` - Framerate AVRational `json:"framerate,omitempty"` + SampleAspectRatio AVRational `json:"sample_aspect_ratio,omitzero"` + Framerate AVRational `json:"framerate,omitzero"` SampleFormat AVSampleFormat `json:"sample_fmt,omitempty"` SampleRate int `json:"sample_rate,omitempty"` - ChannelLayout AVChannelLayout `json:"channel_layout,omitempty"` + ChannelLayout AVChannelLayout `json:"channel_layout,omitzero"` FrameSize int `json:"frame_size,omitempty"` TimeBase AVRational `json:"time_base,omitempty"` } From 05e7580ba633fa0be5a84a03f09edf63310ff5c4 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 23 May 2025 17:40:56 +0200 Subject: [PATCH 10/11] Updated codec --- pkg/avcodec/codec.go | 72 ++++++++++++++++++--------------------- pkg/avcodec/codec_test.go | 46 +++++++++++++++++++++---- pkg/avcodec/encode.go | 24 +++++++++++++ pkg/avcodec/opt.go | 24 +++++++++++++ 4 files changed, 121 insertions(+), 45 deletions(-) create mode 100644 pkg/avcodec/encode.go diff --git a/pkg/avcodec/codec.go b/pkg/avcodec/codec.go index c1eb762..22e2329 100644 --- a/pkg/avcodec/codec.go +++ b/pkg/avcodec/codec.go @@ -2,9 +2,9 @@ package avcodec import ( "encoding/json" + "fmt" // Packages - media "github.com/mutablelogic/go-media" metadata "github.com/mutablelogic/go-media/pkg/metadata" ff "github.com/mutablelogic/go-media/sys/ffmpeg71" @@ -57,64 +57,60 @@ func Codecs(t media.Type) []media.Metadata { return result } -// Return an encoder by name, with additional options. Call Close() to -// release the codec context. Codec options are listed at +// NewEncodingCodec returns an encoder by name. Options for encoding can be passed. +// Call Close() to release the codec. Codec options are listed at // -func NewEncoder(name string, opts ...Opt) (*Codec, error) { - ctx := new(Codec) - - // Options - o, err := applyOptions(opts) - if err != nil { - return nil, err - } - - // Codec context - if codec := ff.AVCodec_find_encoder_by_name(name); codec == nil { - return nil, media.ErrBadParameter.Withf("unknown codec %q", name) - } else if context := ff.AVCodec_alloc_context(codec); context == nil { - return nil, media.ErrInternalError.Withf("failed to allocate codec context for %q", name) - } else if err := set_par(context, codec, o); err != nil { - ff.AVCodec_free_context(context) - return nil, err - } else if ff.AVCodec_open(context, codec, nil); err != nil { - ff.AVCodec_free_context(context) - return nil, err - } else { - ctx.context = context - } - - // Return success - return ctx, nil +func NewEncodingCodec(name string, opts ...Opt) (*Codec, error) { + return newCodec(ff.AVCodec_find_encoder_by_name(name), opts...) } -// Return a decoder by name, with additional options. Call Close() to -// release the codec context. Codec options are listed at +// NewDecodingCodec returns a decoder by name. Options for decoding can be passed. +// Call Close() to release the codec context. Codec options are listed at // -func NewDecoder(name string, opts ...Opt) (*Codec, error) { +func NewDecodingCodec(name string, opts ...Opt) (*Codec, error) { + return newCodec(ff.AVCodec_find_decoder_by_name(name), opts...) +} + +func newCodec(codec *ff.AVCodec, opts ...Opt) (*Codec, error) { ctx := new(Codec) - // Options + // Check parameters + if codec == nil { + return nil, media.ErrBadParameter.Withf("unknown codec %q", codec.Name()) + } + + // Apply options o, err := applyOptions(opts) if err != nil { return nil, err } + // Create a dictionary of codec options + dict := ff.AVUtil_dict_alloc() + defer ff.AVUtil_dict_free(dict) + for _, opt := range o.meta { + if err := ff.AVUtil_dict_set(dict, opt.Key(), opt.Value(), ff.AV_DICT_APPEND); err != nil { + ff.AVUtil_dict_free(dict) + return nil, err + } + } + // Codec context - if codec := ff.AVCodec_find_decoder_by_name(name); codec == nil { - return nil, media.ErrBadParameter.Withf("unknown codec %q", name) - } else if context := ff.AVCodec_alloc_context(codec); context == nil { - return nil, media.ErrInternalError.Withf("failed to allocate codec context for %q", name) + if context := ff.AVCodec_alloc_context(codec); context == nil { + return nil, media.ErrInternalError.Withf("failed to allocate codec context for %q", codec.Name()) } else if err := set_par(context, codec, o); err != nil { ff.AVCodec_free_context(context) return nil, err - } else if ff.AVCodec_open(context, codec, nil); err != nil { + } else if err := ff.AVCodec_open(context, codec, dict); err != nil { ff.AVCodec_free_context(context) return nil, err } else { ctx.context = context } + // TODO: Get the options which were not consumed + fmt.Println("TODO: Codec options not consumed:", dict) + // Return success return ctx, nil } diff --git a/pkg/avcodec/codec_test.go b/pkg/avcodec/codec_test.go index 84c47fa..ca58d42 100644 --- a/pkg/avcodec/codec_test.go +++ b/pkg/avcodec/codec_test.go @@ -16,13 +16,12 @@ func Test_codec_001(t *testing.T) { assert.NotNil(codecs) for _, meta := range codecs { assert.NotNil(meta) - codec, err := avcodec.NewEncoder(meta.Key()) - defer codec.Close() - + codec, err := avcodec.NewEncoder(meta.Key(), avcodec.WithSampleRate(22050)) if assert.NoError(err) { assert.NotNil(codec) - assert.Equal(media.AUDIO, codec.Type()) + assert.Equal(media.OUTPUT|media.AUDIO, codec.Type()) t.Log(codec) + assert.NoError(codec.Close()) } } } @@ -34,13 +33,46 @@ func Test_codec_002(t *testing.T) { assert.NotNil(codecs) for _, meta := range codecs { assert.NotNil(meta) - codec, err := avcodec.NewEncoder(meta.Key()) - defer codec.Close() + codec, err := avcodec.NewEncoder(meta.Key(), avcodec.WithFrameRate(1, 25), avcodec.WithFrameSize("hd720")) + if assert.NoError(err) { + assert.NotNil(codec) + assert.Equal(media.OUTPUT|media.VIDEO, codec.Type()) + t.Log(codec) + assert.NoError(codec.Close()) + } + } +} + +func Test_codec_003(t *testing.T) { + assert := assert.New(t) + + codecs := avcodec.Codecs(media.INPUT | media.AUDIO) + assert.NotNil(codecs) + for _, meta := range codecs { + assert.NotNil(meta) + codec, err := avcodec.NewDecoder(meta.Key()) + if assert.NoError(err) { + assert.NotNil(codec) + assert.Equal(media.INPUT|media.AUDIO, codec.Type()) + t.Log(codec) + assert.NoError(codec.Close()) + } + } +} + +func Test_codec_004(t *testing.T) { + assert := assert.New(t) + codecs := avcodec.Codecs(media.INPUT | media.VIDEO) + assert.NotNil(codecs) + for _, meta := range codecs { + assert.NotNil(meta) + codec, err := avcodec.NewDecoder(meta.Key(), avcodec.WithFrameSize("hd720")) if assert.NoError(err) { assert.NotNil(codec) - assert.Equal(media.VIDEO, codec.Type()) + assert.Equal(media.INPUT|media.VIDEO, codec.Type()) t.Log(codec) + assert.NoError(codec.Close()) } } } diff --git a/pkg/avcodec/encode.go b/pkg/avcodec/encode.go new file mode 100644 index 0000000..e84d6bf --- /dev/null +++ b/pkg/avcodec/encode.go @@ -0,0 +1,24 @@ +package avcodec + +import ( + + // Packages + ff "github.com/mutablelogic/go-media/sys/ffmpeg71" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type EncodingContext struct { + codec *Codec + stream *ff.AVStream + packet *ff.AVPacket +} + +//////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// Create an encoding context (a stream and a packet) with the given codec +func NewEncodingContext(codec *Codec) (*Encoder, error) { + +} diff --git a/pkg/avcodec/opt.go b/pkg/avcodec/opt.go index af281dc..530c628 100644 --- a/pkg/avcodec/opt.go +++ b/pkg/avcodec/opt.go @@ -3,6 +3,7 @@ package avcodec import ( // Packages media "github.com/mutablelogic/go-media" + metadata "github.com/mutablelogic/go-media/pkg/metadata" ff "github.com/mutablelogic/go-media/sys/ffmpeg71" ) @@ -20,6 +21,9 @@ type opt struct { pixel_format ff.AVPixelFormat frame_rate ff.AVRational pixel_ratio ff.AVRational + + // Codec options + meta []*metadata.Metadata } type Opt func(*opt) error @@ -42,6 +46,13 @@ func applyOptions(opts []Opt) (*opt, error) { /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS +func WithOpt(key string, value any) Opt { + return func(o *opt) error { + o.meta = append(o.meta, metadata.New(key, value)) + return nil + } +} + func WithChannelLayout(v string) Opt { return func(o *opt) error { if err := ff.AVUtil_channel_layout_from_string(&o.channel_layout, v); err != nil { @@ -96,3 +107,16 @@ func WithFrameSize(v string) Opt { return nil } } + +func WithFrameRate(num, den int) Opt { + return func(o *opt) error { + o.frame_rate = ff.AVUtil_rational(num, den) + if o.frame_rate.IsZero() { + return media.ErrBadParameter.Withf("zero frame rate %v/%v", num, den) + } + if o.frame_rate.Num() <= 0 || o.frame_rate.Den() <= 0 { + return media.ErrBadParameter.Withf("negative frame rate %v/%v", num, den) + } + return nil + } +} From 48fd9068291723ae82c906cfadad5b89c5b1b6e8 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Mon, 2 Jun 2025 09:32:50 +0200 Subject: [PATCH 11/11] Updated Makefile --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index 49d043a..40ff60c 100755 --- a/Makefile +++ b/Makefile @@ -225,7 +225,6 @@ ffmpeg-dep: $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists fdk-aac && echo "--enable-libfdk-aac")) $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists lame && echo "--enable-libmp3lame")) $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists freetype2 && echo "--enable-libfreetype")) - $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists theora && echo "--enable-libtheora")) $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists vorbis && echo "--enable-libvorbis")) $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists opus && echo "--enable-libopus")) $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists x264 && echo "--enable-libx264"))