From 910fee581564804f5a071160046133e5412eb44d Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Tue, 18 Jun 2024 09:59:57 +0200 Subject: [PATCH 01/16] Updated cli to fix mimetype issue --- cmd/cli/demuxers.go | 26 ------ cmd/cli/muxers.go | 26 ------ cmd/cli/muxers_demuxers.go | 50 +++++++++++ manager.go | 146 +++++++++++++++++++++++---------- media.go | 22 +++-- mediatype.go | 4 +- sys/ffmpeg61/avformat_input.go | 9 +- 7 files changed, 175 insertions(+), 108 deletions(-) delete mode 100644 cmd/cli/demuxers.go delete mode 100644 cmd/cli/muxers.go create mode 100644 cmd/cli/muxers_demuxers.go diff --git a/cmd/cli/demuxers.go b/cmd/cli/demuxers.go deleted file mode 100644 index 13043d8..0000000 --- a/cmd/cli/demuxers.go +++ /dev/null @@ -1,26 +0,0 @@ -package main - -import ( - "fmt" - "os" - - // Packages - "github.com/djthorpe/go-tablewriter" - "github.com/mutablelogic/go-media" -) - -type DemuxersCmd struct { - Filter string `arg:"" optional:"" help:"Filter by mimetype, name or .ext" type:"string"` -} - -func (cmd *DemuxersCmd) Run(globals *Globals) error { - manager := media.NewManager() - formats := manager.InputFormats(cmd.Filter) - writer := tablewriter.New(os.Stdout, tablewriter.OptHeader(), tablewriter.OptOutputText()) - if len(formats) == 0 { - fmt.Printf("No demuxers found for %q\n", cmd.Filter) - return nil - } else { - return writer.Write(formats) - } -} diff --git a/cmd/cli/muxers.go b/cmd/cli/muxers.go deleted file mode 100644 index 32f0d01..0000000 --- a/cmd/cli/muxers.go +++ /dev/null @@ -1,26 +0,0 @@ -package main - -import ( - "fmt" - "os" - - // Packages - "github.com/djthorpe/go-tablewriter" - "github.com/mutablelogic/go-media" -) - -type MuxersCmd struct { - Filter string `arg:"" optional:"" help:"Filter by mimetype, name or .ext" type:"string"` -} - -func (cmd *MuxersCmd) Run(globals *Globals) error { - manager := media.NewManager() - formats := manager.OutputFormats(cmd.Filter) - writer := tablewriter.New(os.Stdout, tablewriter.OptHeader(), tablewriter.OptOutputText()) - if len(formats) == 0 { - fmt.Printf("No muxers found for %q\n", cmd.Filter) - return nil - } else { - return writer.Write(formats) - } -} diff --git a/cmd/cli/muxers_demuxers.go b/cmd/cli/muxers_demuxers.go new file mode 100644 index 0000000..0d04921 --- /dev/null +++ b/cmd/cli/muxers_demuxers.go @@ -0,0 +1,50 @@ +package main + +import ( + "fmt" + "os" + + // Packages + "github.com/djthorpe/go-tablewriter" + "github.com/mutablelogic/go-media" +) + +type DemuxersCmd struct { + Filter string `arg:"" optional:"" help:"Filter by mimetype, name or .ext" type:"string"` +} + +type MuxersCmd struct { + Filter string `arg:"" optional:"" help:"Filter by mimetype, name or .ext" type:"string"` +} + +func (cmd *MuxersCmd) Run(globals *Globals) error { + manager := media.NewManager() + var formats []media.Format + if cmd.Filter == "" { + formats = manager.InputFormats() + } else { + formats = manager.InputFormats(cmd.Filter) + } + return Run(cmd.Filter, formats) +} + +func (cmd *DemuxersCmd) Run(globals *Globals) error { + manager := media.NewManager() + var formats []media.Format + if cmd.Filter == "" { + formats = manager.InputFormats() + } else { + formats = manager.InputFormats(cmd.Filter) + } + return Run(cmd.Filter, formats) +} + +func Run(filter string, formats []media.Format) error { + writer := tablewriter.New(os.Stdout, tablewriter.OptHeader(), tablewriter.OptOutputText()) + if len(formats) == 0 { + fmt.Printf("No (de)muxers found for %q\n", filter) + return nil + } else { + return writer.Write(formats) + } +} diff --git a/manager.go b/manager.go index 9405ccd..9959a7f 100644 --- a/manager.go +++ b/manager.go @@ -6,6 +6,7 @@ import ( "strings" // Package imports + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" ) @@ -19,7 +20,7 @@ type formatmeta struct { Name string `json:"name" writer:",width:25"` Description string `json:"description" writer:",wrap,width:40"` Extensions string `json:"extensions,omitempty"` - MimeTypes string `json:"mimetypes,omitempty"` + MimeTypes string `json:"mimetypes,omitempty" writer:",wrap,width:40"` } type inputformat struct { @@ -40,46 +41,40 @@ func NewManager() *manager { } func newInputFormat(ctx *ff.AVInputFormat) *inputformat { - return &inputformat{ - ctx: ctx, - formatmeta: formatmeta{ - Name: ctx.Name(), - Description: ctx.LongName(), - Extensions: ctx.Extensions(), - MimeTypes: ctx.MimeTypes(), - }, - } + v := &inputformat{ctx: ctx} + v.formatmeta.Name = strings.Join(v.Name(), " ") + v.formatmeta.Description = v.Description() + v.formatmeta.Extensions = strings.Join(v.Extensions(), " ") + v.formatmeta.MimeTypes = strings.Join(v.MimeTypes(), " ") + return v } func newOutputFormat(ctx *ff.AVOutputFormat) *outputformat { - return &outputformat{ - ctx: ctx, - formatmeta: formatmeta{ - Name: ctx.Name(), - Description: ctx.LongName(), - Extensions: ctx.Extensions(), - MimeTypes: ctx.MimeTypes(), - }, - } + v := &outputformat{ctx: ctx} + v.formatmeta.Name = strings.Join(v.Name(), " ") + v.formatmeta.Description = v.Description() + v.formatmeta.Extensions = strings.Join(v.Extensions(), " ") + v.formatmeta.MimeTypes = strings.Join(v.MimeTypes(), " ") + return v } //////////////////////////////////////////////////////////////////////////// // STRINGIFY -func (v inputformat) MarshalJSON() ([]byte, error) { +func (v *inputformat) MarshalJSON() ([]byte, error) { return json.Marshal(v.ctx) } -func (v outputformat) MarshalJSON() ([]byte, error) { +func (v *outputformat) MarshalJSON() ([]byte, error) { return json.Marshal(v.ctx) } -func (v inputformat) String() string { +func (v *inputformat) String() string { data, _ := json.MarshalIndent(v, "", " ") return string(data) } -func (v outputformat) String() string { +func (v *outputformat) String() string { data, _ := json.MarshalIndent(v, "", " ") return string(data) } @@ -90,8 +85,8 @@ func (v outputformat) String() string { // Return the list of matching input formats, optionally filtering by name, // extension or mimetype File extensions should be prefixed with a dot, // e.g. ".mp4" -func (manager *manager) InputFormats(filter ...string) []InputFormat { - var result []InputFormat +func (manager *manager) InputFormats(filter ...string) []Format { + var result []Format // Iterate over all input formats var opaque uintptr @@ -100,9 +95,7 @@ func (manager *manager) InputFormats(filter ...string) []InputFormat { if demuxer == nil { break } - if len(filter) == 0 { - result = append(result, newInputFormat(demuxer)) - } else if manager.matchesInput(demuxer, filter...) { + if matchesInput(demuxer, filter...) { result = append(result, newInputFormat(demuxer)) } } @@ -114,8 +107,8 @@ func (manager *manager) InputFormats(filter ...string) []InputFormat { // Return the list of matching output formats, optionally filtering by name, // extension or mimetype File extensions should be prefixed with a dot, // e.g. ".mp4" -func (manager *manager) OutputFormats(filter ...string) []OutputFormat { - var result []OutputFormat +func (manager *manager) OutputFormats(filter ...string) []Format { + var result []Format // Iterate over all output formats var opaque uintptr @@ -124,9 +117,7 @@ func (manager *manager) OutputFormats(filter ...string) []OutputFormat { if muxer == nil { break } - if len(filter) == 0 { - result = append(result, newOutputFormat(muxer)) - } else if manager.matchesOutput(muxer, filter...) { + if matchesOutput(muxer, filter...) { result = append(result, newOutputFormat(muxer)) } } @@ -135,10 +126,76 @@ func (manager *manager) OutputFormats(filter ...string) []OutputFormat { return result } +func (v *inputformat) Name() []string { + return strings.Split(v.ctx.Name(), ",") +} + +func (v *inputformat) Description() string { + return v.ctx.LongName() +} + +func (v *inputformat) Extensions() []string { + result := []string{} + for _, ext := range strings.Split(v.ctx.Extensions(), ",") { + ext = strings.TrimSpace(ext) + if ext != "" { + result = append(result, "."+ext) + } + } + return result +} + +func (v *inputformat) MimeTypes() []string { + result := []string{} + for _, mimetype := range strings.Split(v.ctx.MimeTypes(), ",") { + if mimetype != "" { + result = append(result, mimetype) + } + } + return result +} + +func (v *inputformat) Type() MediaType { + return INPUT +} + +func (v *outputformat) Name() []string { + return strings.Split(v.ctx.Name(), ",") +} + +func (v *outputformat) Description() string { + return v.ctx.LongName() +} + +func (v *outputformat) Extensions() []string { + result := []string{} + for _, ext := range strings.Split(v.ctx.Extensions(), ",") { + ext = strings.TrimSpace(ext) + if ext != "" { + result = append(result, "."+ext) + } + } + return result +} + +func (v *outputformat) MimeTypes() []string { + result := []string{} + for _, mimetype := range strings.Split(v.ctx.MimeTypes(), ",") { + if mimetype != "" { + result = append(result, mimetype) + } + } + return result +} + +func (v *outputformat) Type() MediaType { + return OUTPUT +} + //////////////////////////////////////////////////////////////////////////// // PRIVATE METHODS -func (this *manager) matchesInput(demuxer *ff.AVInputFormat, mimetype ...string) bool { +func matchesInput(demuxer *ff.AVInputFormat, mimetype ...string) bool { // Match any if len(mimetype) == 0 { return true @@ -163,24 +220,27 @@ func (this *manager) matchesInput(demuxer *ff.AVInputFormat, mimetype ...string) return false } -func (this *manager) matchesOutput(muxer *ff.AVOutputFormat, mimetype ...string) bool { +func matchesOutput(muxer *ff.AVOutputFormat, filter ...string) bool { // Match any - if len(mimetype) == 0 { + if len(filter) == 0 { return true } // Match mimetype - for _, mimetype := range mimetype { - mimetype = strings.ToLower(strings.TrimSpace(mimetype)) - if slices.Contains(strings.Split(muxer.Name(), ","), mimetype) { + for _, filter := range filter { + if filter == "" { + continue + } + filter = strings.ToLower(strings.TrimSpace(filter)) + if slices.Contains(strings.Split(muxer.Name(), ","), filter) { return true } - if strings.HasPrefix(mimetype, ".") { - ext := strings.TrimPrefix(mimetype, ".") - if slices.Contains(strings.Split(muxer.Extensions(), ","), ext) { + if strings.HasPrefix(filter, ".") { + if slices.Contains(strings.Split(muxer.Extensions(), ","), filter[1:]) { return true } } - if slices.Contains(strings.Split(muxer.MimeTypes(), ","), mimetype) { + mt := strings.Split(muxer.MimeTypes(), ",") + if slices.Contains(mt, filter) { return true } } diff --git a/media.go b/media.go index 62d60bd..45246c5 100644 --- a/media.go +++ b/media.go @@ -3,13 +3,23 @@ package media import "io" -// InputFormat represents a container format for input -// of media streams. -type InputFormat interface{} +// Format represents a container format for input or output of media streams. +type Format interface { + // Name(s) of the format + Name() []string -// OuputFormat represents a container format for output -// of media streams. -type OutputFormat interface{} + // Description of the format + Description() string + + // Extensions associated with the format + Extensions() []string + + // MimeTypes associated with the format + MimeTypes() []string + + // INPUT for a demuxer, OUTPUT for a muxer + Type() MediaType +} // Media represents a media stream, which can // be input or output. A new media object is created diff --git a/mediatype.go b/mediatype.go index 2ad080f..88c2d43 100644 --- a/mediatype.go +++ b/mediatype.go @@ -15,5 +15,7 @@ const ( AUDIO // Audio stream DATA // Opaque data information usually continuous SUBTITLE // Subtitle stream - ATTACHMENT + INPUT // Demuxer + OUTPUT // Muxer + DEVICE // Device rather than byte stream or file ) diff --git a/sys/ffmpeg61/avformat_input.go b/sys/ffmpeg61/avformat_input.go index b27a4c2..f66ead4 100644 --- a/sys/ffmpeg61/avformat_input.go +++ b/sys/ffmpeg61/avformat_input.go @@ -20,7 +20,7 @@ import "C" type jsonAVInputFormat struct { Name string `json:"name,omitempty"` LongName string `json:"long_name,omitempty"` - MimeTypes string `json:"mime_types,omitempty"` + MimeTypes string `json:"mime_type,omitempty"` Extensions string `json:"extensions,omitempty"` Flags AVFormat `json:"flags,omitempty"` } @@ -36,11 +36,8 @@ func (ctx *AVInputFormat) MarshalJSON() ([]byte, error) { } func (ctx *AVInputFormat) String() string { - if str, err := json.MarshalIndent(ctx, "", " "); err != nil { - return err.Error() - } else { - return string(str) - } + str, _ := json.MarshalIndent(ctx, "", " ") + return string(str) } //////////////////////////////////////////////////////////////////////////////// From 57789ccb06f0f42da7ebbae81c8b644c5ae62a34 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Tue, 18 Jun 2024 10:28:28 +0200 Subject: [PATCH 02/16] Updated --- cmd/cli/decode.go | 20 ++++++++++++++++++-- cmd/cli/metadata.go | 2 +- manager.go | 27 +++++++++++++++++++++++++-- media.go | 36 +++++++++++++++++++++++++++++++++++- reader.go | 24 ++++++++++++++++++------ 5 files changed, 97 insertions(+), 12 deletions(-) diff --git a/cmd/cli/decode.go b/cmd/cli/decode.go index 142327e..13fdaf0 100644 --- a/cmd/cli/decode.go +++ b/cmd/cli/decode.go @@ -9,11 +9,27 @@ import ( ) type DecodeCmd struct { - Path string `arg:"" required:"" help:"Media file" type:"path"` + Path string `arg:"" required:"" help:"Media file" type:"path"` + Format string `name:"format" short:"f" help:"Format of input file (name, .extension or mimetype)" type:"string"` + Audio *bool `name:"audio" short:"a" help:"Output raw audio stream" type:"bool"` + Video *bool `name:"video" short:"v" help:"Output raw video stream" type:"bool"` } func (cmd *DecodeCmd) Run(globals *Globals) error { - reader, err := media.Open(cmd.Path, "") + var format media.Format + + manager := media.NewManager() + if cmd.Format != "" { + if formats := manager.InputFormats(cmd.Format); len(formats) == 0 { + return fmt.Errorf("unknown format %q", cmd.Format) + } else if len(formats) > 1 { + return fmt.Errorf("ambiguous format %q", cmd.Format) + } else { + format = formats[0] + } + } + + reader, err := manager.Open(cmd.Path, format) if err != nil { return err } diff --git a/cmd/cli/metadata.go b/cmd/cli/metadata.go index a200ae2..a8c319a 100644 --- a/cmd/cli/metadata.go +++ b/cmd/cli/metadata.go @@ -14,7 +14,7 @@ type MetadataCmd struct { } func (cmd *MetadataCmd) Run(globals *Globals) error { - reader, err := media.Open(cmd.Path, "") + reader, err := media.Open(cmd.Path, nil) if err != nil { return err } diff --git a/manager.go b/manager.go index 9959a7f..98d25ed 100644 --- a/manager.go +++ b/manager.go @@ -2,12 +2,15 @@ package media import ( "encoding/json" + "io" "slices" "strings" // Package imports - ff "github.com/mutablelogic/go-media/sys/ffmpeg61" + + // Namespace imports + . "github.com/djthorpe/go-errors" ) //////////////////////////////////////////////////////////////////////////// @@ -36,7 +39,7 @@ type outputformat struct { //////////////////////////////////////////////////////////////////////////// // LIFECYCLE -func NewManager() *manager { +func NewManager() Manager { return new(manager) } @@ -126,6 +129,26 @@ func (manager *manager) OutputFormats(filter ...string) []Format { return result } +// Open a media file for reading +func (manager *manager) Open(url string, format Format) (Media, error) { + return Open(url, format) +} + +// Open a media stream for reading. +func (manager *manager) Read(r io.Reader, format Format) (Media, error) { + return NewReader(r, format) +} + +// Create a media file for writing, from a path. +func (manager *manager) Create(string, Format) (Media, error) { + return nil, ErrNotImplemented +} + +// Create a media stream for writing. +func (manager *manager) Write(io.Writer, Format) (Media, error) { + return nil, ErrNotImplemented +} + func (v *inputformat) Name() []string { return strings.Split(v.ctx.Name(), ",") } diff --git a/media.go b/media.go index 45246c5..9ec68fc 100644 --- a/media.go +++ b/media.go @@ -3,6 +3,40 @@ package media import "io" +// Manager represents a manager for media formats. Create a new manager +// object using the NewManager function. +type Manager interface { + // Return supported input formats which match any filter, which can be + // a name, extension (with preceeding period) or mimetype. + InputFormats(...string) []Format + + // Return supported output formats which match any filter, which can be + // a name, extension (with preceeding period) or mimetype. + OutputFormats(...string) []Format + + // Open a media file for reading, from a path or url. If a format is + // specified, then the format will be used to open the file. Close the + // media object when done. + Open(string, Format) (Media, error) + + // Open a media stream for reading. If a format is + // specified, then the format will be used to open the file. Close the + // media object when done. It is the responsibility of the caller to + // also close the reader when done. + Read(io.Reader, Format) (Media, error) + + // Create a media file for writing, from a path. If a format is + // specified, then the format will be used to create the file. Close + // the media object when done. + Create(string, Format) (Media, error) + + // Create a media stream for writing. If a format is + // specified, then the format will be used to create the file. + // Close the media object when done. It is the responsibility of the caller to + // also close the writer when done. + Write(io.Writer, Format) (Media, error) +} + // Format represents a container format for input or output of media streams. type Format interface { // Name(s) of the format @@ -11,7 +45,7 @@ type Format interface { // Description of the format Description() string - // Extensions associated with the format + // Extensions associated with the format, if a stream Extensions() []string // MimeTypes associated with the format diff --git a/reader.go b/reader.go index 403105b..540abf7 100644 --- a/reader.go +++ b/reader.go @@ -38,14 +38,20 @@ const ( // Open a reader from a url or file path, and either use the mimetype or guess // the format otherwise. Returns a media object. -func Open(url string, mimetype string) (*reader, error) { +func Open(url string, format Format) (*reader, error) { reader := new(reader) reader.decoders = make(map[int]*decoder) - // TODO: mimetype input is currently ignored, format is always guessed + // Set the input format + var fmt *ff.AVInputFormat + if format != nil { + if inputfmt, ok := format.(*inputformat); ok { + fmt = inputfmt.ctx + } + } // Open the stream - if ctx, err := ff.AVFormat_open_url(url, nil, nil); err != nil { + if ctx, err := ff.AVFormat_open_url(url, fmt, nil); err != nil { return nil, err } else { reader.input = ctx @@ -56,11 +62,17 @@ func Open(url string, mimetype string) (*reader, error) { } // Create a new reader from an io.Reader -func NewReader(r io.Reader, mimetype string) (*reader, error) { +func NewReader(r io.Reader, format Format) (*reader, error) { reader := new(reader) reader.decoders = make(map[int]*decoder) - // TODO: mimetype input is currently ignored, format is always guessed + // Set the input format + var fmt *ff.AVInputFormat + if format != nil { + if inputfmt, ok := format.(*inputformat); ok { + fmt = inputfmt.ctx + } + } // Allocate the AVIO context reader.avio = ff.AVFormat_avio_alloc_context(bufSize, false, &reader_callback{r}) @@ -69,7 +81,7 @@ func NewReader(r io.Reader, mimetype string) (*reader, error) { } // Open the stream - if ctx, err := ff.AVFormat_open_reader(reader.avio, nil, nil); err != nil { + if ctx, err := ff.AVFormat_open_reader(reader.avio, fmt, nil); err != nil { ff.AVFormat_avio_context_free(reader.avio) return nil, err } else { From 22d21c75d5991af97d2b345f12634a7c9d20eb23 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Tue, 18 Jun 2024 20:21:11 +0200 Subject: [PATCH 03/16] Added devices --- README.md | 3 +- sys/ffmpeg61/avdevice.go | 80 +++++++++++++++++++++++++++ sys/ffmpeg61/avdevice_core.go | 32 +++++++++++ sys/ffmpeg61/avdevice_input.go | 54 ++++++++++++++++++ sys/ffmpeg61/avdevice_input_test.go | 44 +++++++++++++++ sys/ffmpeg61/avdevice_output.go | 55 ++++++++++++++++++ sys/ffmpeg61/avdevice_output_test.go | 43 ++++++++++++++ sys/ffmpeg61/avdevice_version.go | 28 ++++++++++ sys/ffmpeg61/avdevice_version_test.go | 20 +++++++ sys/ffmpeg61/util.go | 14 +++++ 10 files changed, 371 insertions(+), 2 deletions(-) create mode 100644 sys/ffmpeg61/avdevice.go create mode 100644 sys/ffmpeg61/avdevice_core.go create mode 100644 sys/ffmpeg61/avdevice_input.go create mode 100644 sys/ffmpeg61/avdevice_input_test.go create mode 100644 sys/ffmpeg61/avdevice_output.go create mode 100644 sys/ffmpeg61/avdevice_output_test.go create mode 100644 sys/ffmpeg61/avdevice_version.go create mode 100644 sys/ffmpeg61/avdevice_version_test.go diff --git a/README.md b/README.md index b1cf141..748efb6 100644 --- a/README.md +++ b/README.md @@ -73,5 +73,4 @@ repository for more information: ## References - * https://ffmpeg.org/doxygen/6.1/index.html - +* https://ffmpeg.org/doxygen/6.1/index.html diff --git a/sys/ffmpeg61/avdevice.go b/sys/ffmpeg61/avdevice.go new file mode 100644 index 0000000..beea4a9 --- /dev/null +++ b/sys/ffmpeg61/avdevice.go @@ -0,0 +1,80 @@ +package ffmpeg + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavdevice +#include +*/ +import "C" +import ( + "encoding/json" + "unsafe" +) + +//////////////////////////////////////////////////////////////////////////////// +// TYPES + +type ( + AVAppToDevMessageType C.enum_AVAppToDevMessageType + AVDevToAppMessageType C.enum_AVDevToAppMessageType + AVDeviceInfoList C.struct_AVDeviceInfoList + AVDeviceInfo C.struct_AVDeviceInfo +) + +type jsonAVDeviceInfoList struct { + Devices []*AVDeviceInfo `json:"devices"` + DefaultDevice int `json:"default_device"` +} + +type jsonAVDeviceInfo struct { + Name string `json:"device_name"` + Description string `json:"device_description"` + MediaTypes []AVMediaType `json:"media_types"` +} + +//////////////////////////////////////////////////////////////////////////////// +// JSON OUTPUT + +func (ctx *AVDeviceInfoList) MarshalJSON() ([]byte, error) { + return json.Marshal(jsonAVDeviceInfoList{ + Devices: ctx.Devices(), + DefaultDevice: int(ctx.default_device), + }) +} + +func (ctx *AVDeviceInfo) MarshalJSON() ([]byte, error) { + return json.Marshal(jsonAVDeviceInfo{ + Name: C.GoString(ctx.device_name), + Description: C.GoString(ctx.device_description), + MediaTypes: ctx.MediaTypes(), + }) +} + +//////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (ctx *AVDeviceInfoList) String() string { + data, _ := json.MarshalIndent(ctx, "", " ") + return string(data) +} + +func (ctx *AVDeviceInfo) String() string { + data, _ := json.MarshalIndent(ctx, "", " ") + return string(data) +} + +//////////////////////////////////////////////////////////////////////////////// +// PROPERTIES + +func (ctx *AVDeviceInfoList) Devices() []*AVDeviceInfo { + if ctx.nb_devices == 0 || ctx.devices == nil { + return nil + } + return cAVDeviceInfoSlice(unsafe.Pointer(ctx.devices), ctx.nb_devices) +} + +func (ctx *AVDeviceInfo) MediaTypes() []AVMediaType { + return cAVMediaTypeSlice(unsafe.Pointer(ctx.media_types), ctx.nb_media_types) +} diff --git a/sys/ffmpeg61/avdevice_core.go b/sys/ffmpeg61/avdevice_core.go new file mode 100644 index 0000000..1e47dc8 --- /dev/null +++ b/sys/ffmpeg61/avdevice_core.go @@ -0,0 +1,32 @@ +package ffmpeg + +import ( + "unsafe" +) + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavdevice +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// BINDINGS + +func AVDevice_list_devices(ctx *AVFormatContext) (*AVDeviceInfoList, error) { + var list *C.struct_AVDeviceInfoList + if ret := int(C.avdevice_list_devices((*C.struct_AVFormatContext)(unsafe.Pointer(ctx)), &list)); ret < 0 { + return nil, AVError(ret) + } else if ret == 0 { + return nil, nil + } else { + return (*AVDeviceInfoList)(list), nil + } +} + +func AVDevice_free_list_devices(device_list *AVDeviceInfoList) { + C.avdevice_free_list_devices((**C.struct_AVDeviceInfoList)(unsafe.Pointer(&device_list))) +} diff --git a/sys/ffmpeg61/avdevice_input.go b/sys/ffmpeg61/avdevice_input.go new file mode 100644 index 0000000..25370f7 --- /dev/null +++ b/sys/ffmpeg61/avdevice_input.go @@ -0,0 +1,54 @@ +package ffmpeg + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavdevice +#include +*/ +import "C" +import "unsafe" + +//////////////////////////////////////////////////////////////////////////////// +// BINDINGS + +// Return the first registered audio input format, or NULL if there are none. +func AVDevice_input_audio_device_first() *AVInputFormat { + return (*AVInputFormat)(C.av_input_audio_device_next((*C.struct_AVInputFormat)(nil))) +} + +// Return the next registered audio input device. +func AVDevice_input_audio_device_next(d *AVInputFormat) *AVInputFormat { + return (*AVInputFormat)(C.av_input_audio_device_next((*C.struct_AVInputFormat)(d))) +} + +// Return the first registered video input format, or NULL if there are none. +func AVDevice_input_video_device_first() *AVInputFormat { + return (*AVInputFormat)(C.av_input_video_device_next((*C.struct_AVInputFormat)(nil))) +} + +// Return the next registered video input device. +func AVDevice_input_video_device_next(d *AVInputFormat) *AVInputFormat { + return (*AVInputFormat)(C.av_input_video_device_next((*C.struct_AVInputFormat)(d))) +} + +// List devices. Returns available device names and their parameters. +// device format may be nil if device name is set. +func AVDevice_list_input_sources(device *AVInputFormat, device_name string, device_options *AVDictionary) (*AVDeviceInfoList, error) { + cName := C.CString(device_name) + defer C.free(unsafe.Pointer(cName)) + + var dict *C.struct_AVDictionary + if device_options != nil { + dict = device_options.ctx + } + var list *C.struct_AVDeviceInfoList + if ret := int(C.avdevice_list_input_sources((*C.struct_AVInputFormat)(device), cName, dict, &list)); ret < 0 { + return nil, AVError(ret) + } else if ret == 0 { + return nil, nil + } else { + return (*AVDeviceInfoList)(list), nil + } +} diff --git a/sys/ffmpeg61/avdevice_input_test.go b/sys/ffmpeg61/avdevice_input_test.go new file mode 100644 index 0000000..ab01c8c --- /dev/null +++ b/sys/ffmpeg61/avdevice_input_test.go @@ -0,0 +1,44 @@ +package ffmpeg_test + +import ( + "testing" + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func Test_avdevice_input_000(t *testing.T) { + //assert := assert.New(t) + input := AVDevice_input_audio_device_first() + for { + if input == nil { + break + } + t.Log("audio input=", input) + devices, err := AVDevice_list_input_sources(input, "", nil) + if err == nil { + t.Log(" devices=", devices) + } + AVDevice_free_list_devices(devices) + + input = AVDevice_input_audio_device_next(input) + + } +} + +func Test_avdevice_input_001(t *testing.T) { + input := AVDevice_input_video_device_first() + for { + if input == nil { + break + } + t.Log("video input=", input) + devices, err := AVDevice_list_input_sources(input, "", nil) + if err == nil { + t.Log(" devices=", devices) + } + AVDevice_free_list_devices(devices) + + input = AVDevice_input_video_device_next(input) + } +} diff --git a/sys/ffmpeg61/avdevice_output.go b/sys/ffmpeg61/avdevice_output.go new file mode 100644 index 0000000..02d2a55 --- /dev/null +++ b/sys/ffmpeg61/avdevice_output.go @@ -0,0 +1,55 @@ +package ffmpeg + +import "unsafe" + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavdevice +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// BINDINGS + +// Return the first registered audio output format, or NULL if there are none. +func AVDevice_output_audio_device_first() *AVOutputFormat { + return (*AVOutputFormat)(C.av_output_audio_device_next((*C.struct_AVOutputFormat)(nil))) +} + +// Return the next registered audio output device. +func AVDevice_output_audio_device_next(d *AVOutputFormat) *AVOutputFormat { + return (*AVOutputFormat)(C.av_output_audio_device_next((*C.struct_AVOutputFormat)(d))) +} + +// Return the first registered video output format, or NULL if there are none. +func AVDevice_output_video_device_first() *AVOutputFormat { + return (*AVOutputFormat)(C.av_output_video_device_next((*C.struct_AVOutputFormat)(nil))) +} + +// Return the next registered video output device. +func AVDevice_output_video_device_next(d *AVOutputFormat) *AVOutputFormat { + return (*AVOutputFormat)(C.av_output_video_device_next((*C.struct_AVOutputFormat)(d))) +} + +// List devices. Returns available device names and their parameters. +// device format may be nil if device name is set. +func AVDevice_list_output_sinks(device *AVOutputFormat, device_name string, device_options *AVDictionary) (*AVDeviceInfoList, error) { + cName := C.CString(device_name) + defer C.free(unsafe.Pointer(cName)) + + var dict *C.struct_AVDictionary + if device_options != nil { + dict = device_options.ctx + } + var list *C.struct_AVDeviceInfoList + if ret := int(C.avdevice_list_output_sinks((*C.struct_AVOutputFormat)(device), cName, dict, &list)); ret < 0 { + return nil, AVError(ret) + } else if ret == 0 { + return nil, nil + } else { + return (*AVDeviceInfoList)(list), nil + } +} diff --git a/sys/ffmpeg61/avdevice_output_test.go b/sys/ffmpeg61/avdevice_output_test.go new file mode 100644 index 0000000..1212722 --- /dev/null +++ b/sys/ffmpeg61/avdevice_output_test.go @@ -0,0 +1,43 @@ +package ffmpeg_test + +import ( + "testing" + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func Test_avdevice_output_000(t *testing.T) { + output := AVDevice_output_audio_device_first() + for { + if output == nil { + break + } + t.Log("audio output=", output) + devices, err := AVDevice_list_output_sinks(output, "", nil) + if err == nil { + t.Log(" devices=", devices) + } + AVDevice_free_list_devices(devices) + + output = AVDevice_output_audio_device_next(output) + + } +} + +func Test_avdevice_output_001(t *testing.T) { + output := AVDevice_output_video_device_first() + for { + if output == nil { + break + } + t.Log("video output=", output) + devices, err := AVDevice_list_output_sinks(output, "", nil) + if err == nil { + t.Log(" devices=", devices) + } + AVDevice_free_list_devices(devices) + + output = AVDevice_output_video_device_next(output) + } +} diff --git a/sys/ffmpeg61/avdevice_version.go b/sys/ffmpeg61/avdevice_version.go new file mode 100644 index 0000000..6b24df0 --- /dev/null +++ b/sys/ffmpeg61/avdevice_version.go @@ -0,0 +1,28 @@ +package ffmpeg + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavdevice +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Return the LIBAVDEVICE_VERSION_INT constant. +func AVDevice_version() uint { + return uint(C.avdevice_version()) +} + +// Return the libavdevice build-time configuration. +func AVDevice_configuration() string { + return C.GoString(C.avdevice_configuration()) +} + +// Return the libavdevice license. +func AVDevice_license() string { + return C.GoString(C.avdevice_license()) +} diff --git a/sys/ffmpeg61/avdevice_version_test.go b/sys/ffmpeg61/avdevice_version_test.go new file mode 100644 index 0000000..16d37c1 --- /dev/null +++ b/sys/ffmpeg61/avdevice_version_test.go @@ -0,0 +1,20 @@ +package ffmpeg_test + +import ( + "testing" + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func Test_avdevice_version_000(t *testing.T) { + t.Log("avdevice_version=", AVDevice_version()) +} + +func Test_avdevice_version_001(t *testing.T) { + t.Log("avdevice_configuration=", AVDevice_configuration()) +} + +func Test_avdevice_version_002(t *testing.T) { + t.Log("avdevice_license=", AVDevice_license()) +} diff --git a/sys/ffmpeg61/util.go b/sys/ffmpeg61/util.go index 73623f9..b488183 100644 --- a/sys/ffmpeg61/util.go +++ b/sys/ffmpeg61/util.go @@ -67,3 +67,17 @@ func cAVStreamSlice(p unsafe.Pointer, sz C.int) []*AVStream { } return (*[1 << 30]*AVStream)(p)[:int(sz)] } + +func cAVDeviceInfoSlice(p unsafe.Pointer, sz C.int) []*AVDeviceInfo { + if p == nil { + return nil + } + return (*[1 << 30]*AVDeviceInfo)(p)[:int(sz)] +} + +func cAVMediaTypeSlice(p unsafe.Pointer, sz C.int) []AVMediaType { + if p == nil { + return nil + } + return (*[1 << 30]AVMediaType)(p)[:int(sz)] +} From 423a806a9106acbfa9482225a528add3df603aff Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Wed, 19 Jun 2024 10:27:52 +0200 Subject: [PATCH 04/16] Updated devices --- cmd/cli/decode.go | 2 +- cmd/cli/main.go | 1 + cmd/cli/muxers_demuxers.go | 8 +- cmd/cli/probe.go | 58 +++++++++ manager.go | 180 +++++++++++++++++++++++----- media.go | 33 ++++- mediatype.go | 64 +++++++++- reader.go | 5 +- sys/ffmpeg61/avcodec.go | 19 +++ sys/ffmpeg61/avdevice.go | 12 ++ sys/ffmpeg61/avdevice_input.go | 28 +++-- sys/ffmpeg61/avdevice_input_test.go | 21 ++-- sys/ffmpeg61/avdevice_output.go | 17 ++- sys/ffmpeg61/avformat_demux.go | 19 ++- 14 files changed, 403 insertions(+), 64 deletions(-) create mode 100644 cmd/cli/probe.go diff --git a/cmd/cli/decode.go b/cmd/cli/decode.go index 13fdaf0..d5c5192 100644 --- a/cmd/cli/decode.go +++ b/cmd/cli/decode.go @@ -20,7 +20,7 @@ func (cmd *DecodeCmd) Run(globals *Globals) error { manager := media.NewManager() if cmd.Format != "" { - if formats := manager.InputFormats(cmd.Format); len(formats) == 0 { + if formats := manager.InputFormats(media.NONE, cmd.Format); len(formats) == 0 { return fmt.Errorf("unknown format %q", cmd.Format) } else if len(formats) > 1 { return fmt.Errorf("ambiguous format %q", cmd.Format) diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 92c502e..85b6b13 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -18,6 +18,7 @@ type CLI struct { Demuxers DemuxersCmd `cmd:"demuxers" help:"List media demultiplex (input) formats"` Muxers MuxersCmd `cmd:"muxers" help:"List media multiplex (output) formats"` Metadata MetadataCmd `cmd:"metadata" help:"Display media metadata information"` + Probe ProbeCmd `cmd:"probe" help:"Probe media file or device"` Decode DecodeCmd `cmd:"decode" help:"Decode media"` } diff --git a/cmd/cli/muxers_demuxers.go b/cmd/cli/muxers_demuxers.go index 0d04921..46ec0e1 100644 --- a/cmd/cli/muxers_demuxers.go +++ b/cmd/cli/muxers_demuxers.go @@ -21,9 +21,9 @@ func (cmd *MuxersCmd) Run(globals *Globals) error { manager := media.NewManager() var formats []media.Format if cmd.Filter == "" { - formats = manager.InputFormats() + formats = manager.OutputFormats(media.ANY) } else { - formats = manager.InputFormats(cmd.Filter) + formats = manager.OutputFormats(media.ANY, cmd.Filter) } return Run(cmd.Filter, formats) } @@ -32,9 +32,9 @@ func (cmd *DemuxersCmd) Run(globals *Globals) error { manager := media.NewManager() var formats []media.Format if cmd.Filter == "" { - formats = manager.InputFormats() + formats = manager.InputFormats(media.ANY) } else { - formats = manager.InputFormats(cmd.Filter) + formats = manager.InputFormats(media.ANY, cmd.Filter) } return Run(cmd.Filter, formats) } diff --git a/cmd/cli/probe.go b/cmd/cli/probe.go new file mode 100644 index 0000000..3edce24 --- /dev/null +++ b/cmd/cli/probe.go @@ -0,0 +1,58 @@ +package main + +import ( + "encoding/json" + "fmt" + "regexp" + + // Packages + "github.com/mutablelogic/go-media" +) + +type ProbeCmd struct { + Path string `arg:"" required:"" help:"Media file or device name" type:"string"` + Audio bool `name:"audio" short:"a" help:"Probe audio stream" type:"bool"` + Video bool `name:"video" short:"v" help:"Probe video stream" type:"bool"` +} + +var ( + reDevice = regexp.MustCompile(`^([a-zA-Z0-9]+):(.*)$`) +) + +func (cmd *ProbeCmd) Run(globals *Globals) error { + var format media.Format + + manager := media.NewManager() + filter := media.NONE + if cmd.Audio { + filter |= media.AUDIO + } + if cmd.Video { + filter |= media.VIDEO + } + + // Try device first + if m := reDevice.FindStringSubmatch(cmd.Path); m != nil { + cmd.Path = m[2] + fmts := manager.InputFormats(filter|media.DEVICE, m[1]) + if len(fmts) == 1 { + format = fmts[0] + } else if len(fmts) > 1 { + return fmt.Errorf("ambigious device name %q, use -audio or -video", m[1]) + } + } + + // Open the media file or device + reader, err := manager.Open(cmd.Path, format) + if err != nil { + return err + } + defer reader.Close() + + // Print out probe data + data, _ := json.MarshalIndent(reader, "", " ") + fmt.Println(string(data)) + + // Return success + return nil +} diff --git a/manager.go b/manager.go index 98d25ed..eacb6c1 100644 --- a/manager.go +++ b/manager.go @@ -2,6 +2,7 @@ package media import ( "encoding/json" + "fmt" "io" "slices" "strings" @@ -20,10 +21,11 @@ type manager struct { } type formatmeta struct { - Name string `json:"name" writer:",width:25"` - Description string `json:"description" writer:",wrap,width:40"` - Extensions string `json:"extensions,omitempty"` - MimeTypes string `json:"mimetypes,omitempty" writer:",wrap,width:40"` + Name string `json:"name" writer:",width:25"` + Description string `json:"description" writer:",wrap,width:40"` + Extensions string `json:"extensions,omitempty"` + MimeTypes string `json:"mimetypes,omitempty" writer:",wrap,width:40"` + MediaType MediaType `json:"type,omitempty" writer:",wrap,width:21"` } type inputformat struct { @@ -36,6 +38,14 @@ type outputformat struct { ctx *ff.AVOutputFormat } +type device struct { + Format string `json:"format"` + Name string `json:"name"` + Description string `json:"description"` + Default bool `json:"default,omitempty"` + MediaType MediaType `json:"type,omitempty" writer:",wrap,width:21"` +} + //////////////////////////////////////////////////////////////////////////// // LIFECYCLE @@ -43,21 +53,43 @@ func NewManager() Manager { return new(manager) } -func newInputFormat(ctx *ff.AVInputFormat) *inputformat { +func newInputFormat(ctx *ff.AVInputFormat, t MediaType) *inputformat { v := &inputformat{ctx: ctx} v.formatmeta.Name = strings.Join(v.Name(), " ") v.formatmeta.Description = v.Description() v.formatmeta.Extensions = strings.Join(v.Extensions(), " ") v.formatmeta.MimeTypes = strings.Join(v.MimeTypes(), " ") + v.formatmeta.MediaType = INPUT | t return v } -func newOutputFormat(ctx *ff.AVOutputFormat) *outputformat { +func newOutputFormat(ctx *ff.AVOutputFormat, t MediaType) *outputformat { v := &outputformat{ctx: ctx} v.formatmeta.Name = strings.Join(v.Name(), " ") v.formatmeta.Description = v.Description() v.formatmeta.Extensions = strings.Join(v.Extensions(), " ") v.formatmeta.MimeTypes = strings.Join(v.MimeTypes(), " ") + v.formatmeta.MediaType = OUTPUT | t + return v +} + +func newInputDevice(ctx *ff.AVInputFormat, d *ff.AVDeviceInfo, t MediaType, def bool) *device { + v := &device{} + v.Format = ctx.Name() + v.Name = d.Name() + v.Description = d.Description() + v.Default = def + v.MediaType = INPUT | t + return v +} + +func newOutputDevice(ctx *ff.AVOutputFormat, d *ff.AVDeviceInfo, t MediaType, def bool) *device { + v := &device{} + v.Format = ctx.Name() + v.Name = d.Name() + v.Description = d.Description() + v.Default = def + v.MediaType = OUTPUT | t return v } @@ -87,19 +119,47 @@ func (v *outputformat) String() string { // Return the list of matching input formats, optionally filtering by name, // extension or mimetype File extensions should be prefixed with a dot, -// e.g. ".mp4" -func (manager *manager) InputFormats(filter ...string) []Format { +// e.g. ".mp4". The media type can be NONE (for any) or combinations of +// STREAM, DEVICE. +func (manager *manager) InputFormats(t MediaType, filter ...string) []Format { var result []Format // Iterate over all input formats - var opaque uintptr - for { - demuxer := ff.AVFormat_demuxer_iterate(&opaque) - if demuxer == nil { - break + if t == NONE || t.Is(FILE) { + var opaque uintptr + for { + demuxer := ff.AVFormat_demuxer_iterate(&opaque) + if demuxer == nil { + break + } + if matchesInput(demuxer, t, filter...) { + result = append(result, newInputFormat(demuxer, FILE)) + } } - if matchesInput(demuxer, filter...) { - result = append(result, newInputFormat(demuxer)) + } + + if t == NONE || t.Is(DEVICE) { + // Iterate over all device inputs + audio := ff.AVDevice_input_audio_device_first() + for { + if audio == nil { + break + } + if matchesInput(audio, t, filter...) { + result = append(result, newInputFormat(audio, AUDIO|DEVICE)) + } + audio = ff.AVDevice_input_audio_device_next(audio) + } + + video := ff.AVDevice_input_video_device_first() + for { + if video == nil { + break + } + if matchesInput(video, t, filter...) { + result = append(result, newInputFormat(video, VIDEO|DEVICE)) + } + video = ff.AVDevice_input_video_device_next(video) } } @@ -109,19 +169,47 @@ func (manager *manager) InputFormats(filter ...string) []Format { // Return the list of matching output formats, optionally filtering by name, // extension or mimetype File extensions should be prefixed with a dot, -// e.g. ".mp4" -func (manager *manager) OutputFormats(filter ...string) []Format { +// e.g. ".mp4". The media type can be NONE (for any) or combinations of +// STREAM, DEVICE. +func (manager *manager) OutputFormats(t MediaType, filter ...string) []Format { var result []Format // Iterate over all output formats - var opaque uintptr - for { - muxer := ff.AVFormat_muxer_iterate(&opaque) - if muxer == nil { - break + if t == NONE || t.Is(FILE) { + var opaque uintptr + for { + muxer := ff.AVFormat_muxer_iterate(&opaque) + if muxer == nil { + break + } + if matchesOutput(muxer, t, filter...) { + result = append(result, newOutputFormat(muxer, FILE)) + } + } + } + + // Iterate over all device outputs + if t == NONE || t.Is(DEVICE) { + audio := ff.AVDevice_output_audio_device_first() + for { + if audio == nil { + break + } + if matchesOutput(audio, t, filter...) { + result = append(result, newOutputFormat(audio, AUDIO|DEVICE)) + } + audio = ff.AVDevice_output_audio_device_next(audio) } - if matchesOutput(muxer, filter...) { - result = append(result, newOutputFormat(muxer)) + + video := ff.AVDevice_output_video_device_first() + for { + if video == nil { + break + } + if matchesOutput(video, t, filter...) { + result = append(result, newOutputFormat(video, VIDEO|DEVICE)) + } + video = ff.AVDevice_output_video_device_next(video) } } @@ -129,7 +217,37 @@ func (manager *manager) OutputFormats(filter ...string) []Format { return result } -// Open a media file for reading +// Return supported input devices for a given input format +func (manager *manager) InputDevices(format string) []Device { + input := ff.AVFormat_find_input_format(format) + if input == nil { + return nil + } + + device_list, err := ff.AVDevice_list_input_sources(input, format, nil) + if err != nil { + panic(err) + } + if device_list == nil { + return nil + } + defer ff.AVDevice_free_list_devices(device_list) + + // Iterate over devices + result := make([]Device, 0, device_list.NumDevices()) + for i, device := range device_list.Devices() { + fmt.Println(i, device) + } + + return result +} + +// Return supported output devices for a given name +func (manager *manager) OutputDevices(format string) []Device { + panic("TODO") +} + +// Open a media file or device for reading, from a path or url. func (manager *manager) Open(url string, format Format) (Media, error) { return Open(url, format) } @@ -218,9 +336,11 @@ func (v *outputformat) Type() MediaType { //////////////////////////////////////////////////////////////////////////// // PRIVATE METHODS -func matchesInput(demuxer *ff.AVInputFormat, mimetype ...string) bool { +func matchesInput(demuxer *ff.AVInputFormat, media_type MediaType, mimetype ...string) bool { + // TODO: media_type + // Match any - if len(mimetype) == 0 { + if len(mimetype) == 0 && media_type == ANY { return true } // Match mimetype @@ -243,9 +363,11 @@ func matchesInput(demuxer *ff.AVInputFormat, mimetype ...string) bool { return false } -func matchesOutput(muxer *ff.AVOutputFormat, filter ...string) bool { +func matchesOutput(muxer *ff.AVOutputFormat, media_type MediaType, filter ...string) bool { + // TODO: media_type + // Match any - if len(filter) == 0 { + if len(filter) == 0 && media_type == ANY { return true } // Match mimetype diff --git a/media.go b/media.go index 9ec68fc..625b1ef 100644 --- a/media.go +++ b/media.go @@ -7,12 +7,20 @@ import "io" // object using the NewManager function. type Manager interface { // Return supported input formats which match any filter, which can be - // a name, extension (with preceeding period) or mimetype. - InputFormats(...string) []Format + // a name, extension (with preceeding period) or mimetype. The MediaType + // can be NONE (for any) or combinations of DEVICE and STREAM. + InputFormats(MediaType, ...string) []Format // Return supported output formats which match any filter, which can be - // a name, extension (with preceeding period) or mimetype. - OutputFormats(...string) []Format + // a name, extension (with preceeding period) or mimetype. The MediaType + // can be NONE (for any) or combinations of DEVICE and STREAM. + OutputFormats(MediaType, ...string) []Format + + // Return supported input devices for a given name + InputDevices(string) []Device + + // Return supported output devices for a given name + OutputDevices(string) []Device // Open a media file for reading, from a path or url. If a format is // specified, then the format will be used to open the file. Close the @@ -37,6 +45,21 @@ type Manager interface { Write(io.Writer, Format) (Media, error) } +// Device represents a device for input or output of media streams. +type Device interface { + // Device name, format depends on the device + Name() string + + // Description of the device + Description() string + + // Flags indicating the type + Type() MediaType + + // Whether this is the default device + Default() bool +} + // Format represents a container format for input or output of media streams. type Format interface { // Name(s) of the format @@ -51,7 +74,7 @@ type Format interface { // MimeTypes associated with the format MimeTypes() []string - // INPUT for a demuxer, OUTPUT for a muxer + // Flags indicating the type. INPUT for a demuxer or source, OUTPUT for a muxer or sink, DEVICE for a device, FILE for a file. Type() MediaType } diff --git a/mediatype.go b/mediatype.go index 88c2d43..335cef7 100644 --- a/mediatype.go +++ b/mediatype.go @@ -1,5 +1,7 @@ package media +import "encoding/json" + //////////////////////////////////////////////////////////////////////////// // TYPES @@ -10,6 +12,7 @@ type MediaType uint32 // GLOBALS const ( + NONE MediaType = 0 UNKNOWN MediaType = (1 << iota) // Usually treated as DATA VIDEO // Video stream AUDIO // Audio stream @@ -17,5 +20,64 @@ const ( SUBTITLE // Subtitle stream INPUT // Demuxer OUTPUT // Muxer - DEVICE // Device rather than byte stream or file + FILE // File or byte stream + DEVICE // Device rather than stream + + // Set minimum and maximum values + MIN = UNKNOWN + MAX = DEVICE + + // Convenience values + ANY = NONE ) + +//////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (v MediaType) MarshalJSON() ([]byte, error) { + return json.Marshal(v.String()) +} + +func (v MediaType) String() string { + if v == NONE { + return v.String() + } + str := "" + for f := MIN; f <= MAX; f <<= 1 { + if v&f == f { + str += "|" + f.FlagString() + } + } + return str[1:] +} + +func (v MediaType) FlagString() string { + switch v { + case NONE: + return "NONE" + case VIDEO: + return "VIDEO" + case AUDIO: + return "AUDIO" + case DATA: + return "DATA" + case SUBTITLE: + return "SUBTITLE" + case INPUT: + return "INPUT" + case OUTPUT: + return "OUTPUT" + case FILE: + return "FILE" + case DEVICE: + return "DEVICE" + } + return "" +} + +//////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +func (v MediaType) Is(f MediaType) bool { + return v&f == f +} diff --git a/reader.go b/reader.go index 540abf7..1ff9040 100644 --- a/reader.go +++ b/reader.go @@ -36,8 +36,7 @@ const ( //////////////////////////////////////////////////////////////////////////////// // LIFECYCLE -// Open a reader from a url or file path, and either use the mimetype or guess -// the format otherwise. Returns a media object. +// Open a reader from a url, file path or device func Open(url string, format Format) (*reader, error) { reader := new(reader) reader.decoders = make(map[int]*decoder) @@ -50,7 +49,7 @@ func Open(url string, format Format) (*reader, error) { } } - // Open the stream + // Open the device or stream if ctx, err := ff.AVFormat_open_url(url, fmt, nil); err != nil { return nil, err } else { diff --git a/sys/ffmpeg61/avcodec.go b/sys/ffmpeg61/avcodec.go index d96e755..c6032ef 100644 --- a/sys/ffmpeg61/avcodec.go +++ b/sys/ffmpeg61/avcodec.go @@ -240,6 +240,10 @@ func (v AVCodecCap) MarshalJSON() ([]byte, error) { return json.Marshal(v.String()) } +func (v AVCodecID) MarshalJSON() ([]byte, error) { + return json.Marshal(v.String()) +} + //////////////////////////////////////////////////////////////////////////////// // STRINGIFY @@ -696,3 +700,18 @@ func (v AVCodecCap) FlagString() string { return fmt.Sprintf("AVCodecCap(0x%08X)", uint32(v)) } } + +//////////////////////////////////////////////////////////////////////////////// +// AVCodecID + +func (v AVCodecID) String() string { + return v.Name() +} + +func (v AVCodecID) Name() string { + return C.GoString(C.avcodec_get_name(C.enum_AVCodecID(v))) +} + +func (v AVCodecID) Type() AVMediaType { + return AVMediaType(C.avcodec_get_type(C.enum_AVCodecID(v))) +} diff --git a/sys/ffmpeg61/avdevice.go b/sys/ffmpeg61/avdevice.go index beea4a9..a00eead 100644 --- a/sys/ffmpeg61/avdevice.go +++ b/sys/ffmpeg61/avdevice.go @@ -75,6 +75,18 @@ func (ctx *AVDeviceInfoList) Devices() []*AVDeviceInfo { return cAVDeviceInfoSlice(unsafe.Pointer(ctx.devices), ctx.nb_devices) } +func (ctx *AVDeviceInfoList) NumDevices() int { + return int(ctx.nb_devices) +} + +func (ctx *AVDeviceInfo) Name() string { + return C.GoString(ctx.device_name) +} + +func (ctx *AVDeviceInfo) Description() string { + return C.GoString(ctx.device_description) +} + func (ctx *AVDeviceInfo) MediaTypes() []AVMediaType { return cAVMediaTypeSlice(unsafe.Pointer(ctx.media_types), ctx.nb_media_types) } diff --git a/sys/ffmpeg61/avdevice_input.go b/sys/ffmpeg61/avdevice_input.go index 25370f7..c29545c 100644 --- a/sys/ffmpeg61/avdevice_input.go +++ b/sys/ffmpeg61/avdevice_input.go @@ -8,7 +8,10 @@ package ffmpeg #include */ import "C" -import "unsafe" +import ( + "fmt" + "unsafe" +) //////////////////////////////////////////////////////////////////////////////// // BINDINGS @@ -33,22 +36,33 @@ func AVDevice_input_video_device_next(d *AVInputFormat) *AVInputFormat { return (*AVInputFormat)(C.av_input_video_device_next((*C.struct_AVInputFormat)(d))) } -// List devices. Returns available device names and their parameters. -// device format may be nil if device name is set. +// List devices. Returns available device names and their parameters, or nil if the +// enumeration of devices is not supported. +// Device format may be nil if device name is set. Call AVDevice_free_list_devices +// to free resources afterwards. func AVDevice_list_input_sources(device *AVInputFormat, device_name string, device_options *AVDictionary) (*AVDeviceInfoList, error) { + // Return nil if the devices does not implement the get_device_list function + if device != nil && device.get_device_list == nil { + return nil, nil + } + + // Prepare name cName := C.CString(device_name) defer C.free(unsafe.Pointer(cName)) + // Prepare dictionary var dict *C.struct_AVDictionary if device_options != nil { dict = device_options.ctx } + + // Get list var list *C.struct_AVDeviceInfoList if ret := int(C.avdevice_list_input_sources((*C.struct_AVInputFormat)(device), cName, dict, &list)); ret < 0 { + fmt.Println("C list", device, cName, dict, list, ret) return nil, AVError(ret) - } else if ret == 0 { - return nil, nil - } else { - return (*AVDeviceInfoList)(list), nil } + + // Return success + return (*AVDeviceInfoList)(list), nil } diff --git a/sys/ffmpeg61/avdevice_input_test.go b/sys/ffmpeg61/avdevice_input_test.go index ab01c8c..9c977ce 100644 --- a/sys/ffmpeg61/avdevice_input_test.go +++ b/sys/ffmpeg61/avdevice_input_test.go @@ -3,12 +3,14 @@ package ffmpeg_test import ( "testing" + "github.com/stretchr/testify/assert" + // Namespace imports . "github.com/mutablelogic/go-media/sys/ffmpeg61" ) func Test_avdevice_input_000(t *testing.T) { - //assert := assert.New(t) + assert := assert.New(t) input := AVDevice_input_audio_device_first() for { if input == nil { @@ -16,10 +18,12 @@ func Test_avdevice_input_000(t *testing.T) { } t.Log("audio input=", input) devices, err := AVDevice_list_input_sources(input, "", nil) - if err == nil { - t.Log(" devices=", devices) + if assert.NoError(err) { + if devices != nil { + t.Log(" devices=", devices) + AVDevice_free_list_devices(devices) + } } - AVDevice_free_list_devices(devices) input = AVDevice_input_audio_device_next(input) @@ -27,6 +31,7 @@ func Test_avdevice_input_000(t *testing.T) { } func Test_avdevice_input_001(t *testing.T) { + assert := assert.New(t) input := AVDevice_input_video_device_first() for { if input == nil { @@ -34,10 +39,12 @@ func Test_avdevice_input_001(t *testing.T) { } t.Log("video input=", input) devices, err := AVDevice_list_input_sources(input, "", nil) - if err == nil { - t.Log(" devices=", devices) + if assert.NoError(err) { + if devices != nil { + t.Log(" devices=", devices) + AVDevice_free_list_devices(devices) + } } - AVDevice_free_list_devices(devices) input = AVDevice_input_video_device_next(input) } diff --git a/sys/ffmpeg61/avdevice_output.go b/sys/ffmpeg61/avdevice_output.go index 02d2a55..1cf3c9b 100644 --- a/sys/ffmpeg61/avdevice_output.go +++ b/sys/ffmpeg61/avdevice_output.go @@ -34,22 +34,27 @@ func AVDevice_output_video_device_next(d *AVOutputFormat) *AVOutputFormat { return (*AVOutputFormat)(C.av_output_video_device_next((*C.struct_AVOutputFormat)(d))) } -// List devices. Returns available device names and their parameters. -// device format may be nil if device name is set. +// List devices. Returns available device names and their parameters, or nil if the +// enumeration of devices is not supported. +// Device format may be nil if device name is set. Call AVDevice_free_list_devices +// to free resources afterwards. func AVDevice_list_output_sinks(device *AVOutputFormat, device_name string, device_options *AVDictionary) (*AVDeviceInfoList, error) { + // Prepare name cName := C.CString(device_name) defer C.free(unsafe.Pointer(cName)) + // Prepare dictionary var dict *C.struct_AVDictionary if device_options != nil { dict = device_options.ctx } + + // Get list var list *C.struct_AVDeviceInfoList if ret := int(C.avdevice_list_output_sinks((*C.struct_AVOutputFormat)(device), cName, dict, &list)); ret < 0 { return nil, AVError(ret) - } else if ret == 0 { - return nil, nil - } else { - return (*AVDeviceInfoList)(list), nil } + + // Return success + return (*AVDeviceInfoList)(list), nil } diff --git a/sys/ffmpeg61/avformat_demux.go b/sys/ffmpeg61/avformat_demux.go index 623afd6..132acbd 100644 --- a/sys/ffmpeg61/avformat_demux.go +++ b/sys/ffmpeg61/avformat_demux.go @@ -69,7 +69,24 @@ func AVFormat_open_url(url string, format *AVInputFormat, options *AVDictionary) // Open an input stream from a device. func AVFormat_open_device(format *AVInputFormat, options *AVDictionary) (*AVFormatContext, error) { - return AVFormat_open_url("", format, options) + var opts **C.struct_AVDictionary + if options != nil { + opts = &options.ctx + } + + // Allocate a context + ctx := AVFormat_alloc_context() + if ctx == nil { + return nil, AVError(syscall.ENOMEM) + } + + // Open the device + if err := AVError(C.avformat_open_input((**C.struct_AVFormatContext)(unsafe.Pointer(&ctx)), nil, (*C.struct_AVInputFormat)(format), opts)); err != 0 { + return nil, err + } + + // Return success + return ctx, nil } // Close an opened input AVFormatContext, free it and all its contents. From 02aa056d01d2d14531edb0a3bbdffdabc259f9ae Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Wed, 19 Jun 2024 11:02:10 +0200 Subject: [PATCH 05/16] Updated device support --- cmd/cli/probe.go | 17 ++++------------- manager.go | 8 ++++---- media.go | 4 ++-- reader.go | 31 +++++++++++++++++++++++++------ sys/ffmpeg61/avutil_dict.go | 17 +++++++++++++++++ 5 files changed, 52 insertions(+), 25 deletions(-) diff --git a/cmd/cli/probe.go b/cmd/cli/probe.go index 3edce24..08e5d80 100644 --- a/cmd/cli/probe.go +++ b/cmd/cli/probe.go @@ -10,9 +10,8 @@ import ( ) type ProbeCmd struct { - Path string `arg:"" required:"" help:"Media file or device name" type:"string"` - Audio bool `name:"audio" short:"a" help:"Probe audio stream" type:"bool"` - Video bool `name:"video" short:"v" help:"Probe video stream" type:"bool"` + Path string `arg:"" required:"" help:"Media file or device name" type:"string"` + Opts string `name:"opts" short:"o" help:"Options for opening the media file or device, (ie, \"framerate=30 video_size=176x144\")"` } var ( @@ -24,26 +23,18 @@ func (cmd *ProbeCmd) Run(globals *Globals) error { manager := media.NewManager() filter := media.NONE - if cmd.Audio { - filter |= media.AUDIO - } - if cmd.Video { - filter |= media.VIDEO - } // Try device first if m := reDevice.FindStringSubmatch(cmd.Path); m != nil { cmd.Path = m[2] fmts := manager.InputFormats(filter|media.DEVICE, m[1]) - if len(fmts) == 1 { + if len(fmts) > 0 { format = fmts[0] - } else if len(fmts) > 1 { - return fmt.Errorf("ambigious device name %q, use -audio or -video", m[1]) } } // Open the media file or device - reader, err := manager.Open(cmd.Path, format) + reader, err := manager.Open(cmd.Path, format, cmd.Opts) if err != nil { return err } diff --git a/manager.go b/manager.go index eacb6c1..f14b922 100644 --- a/manager.go +++ b/manager.go @@ -248,13 +248,13 @@ func (manager *manager) OutputDevices(format string) []Device { } // Open a media file or device for reading, from a path or url. -func (manager *manager) Open(url string, format Format) (Media, error) { - return Open(url, format) +func (manager *manager) Open(url string, format Format, opts ...string) (Media, error) { + return Open(url, format, opts...) } // Open a media stream for reading. -func (manager *manager) Read(r io.Reader, format Format) (Media, error) { - return NewReader(r, format) +func (manager *manager) Read(r io.Reader, format Format, opts ...string) (Media, error) { + return NewReader(r, format, opts...) } // Create a media file for writing, from a path. diff --git a/media.go b/media.go index 625b1ef..81d56f6 100644 --- a/media.go +++ b/media.go @@ -25,13 +25,13 @@ type Manager interface { // Open a media file for reading, from a path or url. If a format is // specified, then the format will be used to open the file. Close the // media object when done. - Open(string, Format) (Media, error) + Open(string, Format, ...string) (Media, error) // Open a media stream for reading. If a format is // specified, then the format will be used to open the file. Close the // media object when done. It is the responsibility of the caller to // also close the reader when done. - Read(io.Reader, Format) (Media, error) + Read(io.Reader, Format, ...string) (Media, error) // Create a media file for writing, from a path. If a format is // specified, then the format will be used to create the file. Close diff --git a/reader.go b/reader.go index 1ff9040..516704a 100644 --- a/reader.go +++ b/reader.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "io" + "strings" "syscall" // Packages @@ -37,20 +38,29 @@ const ( // LIFECYCLE // Open a reader from a url, file path or device -func Open(url string, format Format) (*reader, error) { +func Open(url string, format Format, opts ...string) (*reader, error) { reader := new(reader) reader.decoders = make(map[int]*decoder) // Set the input format - var fmt *ff.AVInputFormat + var f *ff.AVInputFormat if format != nil { if inputfmt, ok := format.(*inputformat); ok { - fmt = inputfmt.ctx + f = inputfmt.ctx + } + } + + // Get the options + dict := ff.AVUtil_dict_alloc() + defer ff.AVUtil_dict_free(dict) + if len(opts) > 0 { + if err := ff.AVUtil_dict_parse_string(dict, strings.Join(opts, " "), "=", " ", 0); err != nil { + return nil, err } } // Open the device or stream - if ctx, err := ff.AVFormat_open_url(url, fmt, nil); err != nil { + if ctx, err := ff.AVFormat_open_url(url, f, dict); err != nil { return nil, err } else { reader.input = ctx @@ -61,7 +71,7 @@ func Open(url string, format Format) (*reader, error) { } // Create a new reader from an io.Reader -func NewReader(r io.Reader, format Format) (*reader, error) { +func NewReader(r io.Reader, format Format, opts ...string) (*reader, error) { reader := new(reader) reader.decoders = make(map[int]*decoder) @@ -73,6 +83,15 @@ func NewReader(r io.Reader, format Format) (*reader, error) { } } + // Get the options + dict := ff.AVUtil_dict_alloc() + defer ff.AVUtil_dict_free(dict) + if len(opts) > 0 { + if err := ff.AVUtil_dict_parse_string(dict, strings.Join(opts, " "), "=", " ", 0); err != nil { + return nil, err + } + } + // Allocate the AVIO context reader.avio = ff.AVFormat_avio_alloc_context(bufSize, false, &reader_callback{r}) if reader.avio == nil { @@ -80,7 +99,7 @@ func NewReader(r io.Reader, format Format) (*reader, error) { } // Open the stream - if ctx, err := ff.AVFormat_open_reader(reader.avio, fmt, nil); err != nil { + if ctx, err := ff.AVFormat_open_reader(reader.avio, fmt, dict); err != nil { ff.AVFormat_avio_context_free(reader.avio) return nil, err } else { diff --git a/sys/ffmpeg61/avutil_dict.go b/sys/ffmpeg61/avutil_dict.go index 7524081..2c5af95 100644 --- a/sys/ffmpeg61/avutil_dict.go +++ b/sys/ffmpeg61/avutil_dict.go @@ -110,6 +110,23 @@ func AVUtil_dict_entries(dict *AVDictionary) []*AVDictionaryEntry { return result } +// Parse the key/value pairs list and add the parsed entries to a dictionary. +func AVUtil_dict_parse_string(dict *AVDictionary, opts, key_value_sep, pairs_sep string, flags AVDictionaryFlag) error { + if dict == nil { + return nil + } + cOpts, cTupleSep, cKeyValueSep := C.CString(opts), C.CString(pairs_sep), C.CString(key_value_sep) + defer C.free(unsafe.Pointer(cOpts)) + defer C.free(unsafe.Pointer(cTupleSep)) + defer C.free(unsafe.Pointer(cKeyValueSep)) + if err := AVError(C.av_dict_parse_string(&dict.ctx, cOpts, cKeyValueSep, cTupleSep, C.int(flags))); err != 0 { + return err + } + + // Success + return nil +} + //////////////////////////////////////////////////////////////////////////////// // DICTIONARY ENTRY From 08983e59c93cbc6bbebf1920eaf32f1c2fe86c70 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Wed, 19 Jun 2024 11:39:41 +0200 Subject: [PATCH 06/16] Updated interfaces --- media.go | 85 ++++++++++++++++++++++++++++++++++++++----------------- reader.go | 3 ++ 2 files changed, 62 insertions(+), 26 deletions(-) diff --git a/media.go b/media.go index 81d56f6..e4c4979 100644 --- a/media.go +++ b/media.go @@ -1,10 +1,17 @@ -/* media is a package for reading and writing media files. */ +/* +This is a package for reading, writing and inspecting media files. In +order to operate on media, call NewManager() and then use the manager +functions to determine capabilities and manage media files and devices. +*/ package media -import "io" +import ( + "context" + "io" +) -// Manager represents a manager for media formats. Create a new manager -// object using the NewManager function. +// Manager represents a manager for media formats and devices. +// Create a new manager object using the NewManager function. type Manager interface { // Return supported input formats which match any filter, which can be // a name, extension (with preceeding period) or mimetype. The MediaType @@ -22,9 +29,9 @@ type Manager interface { // Return supported output devices for a given name OutputDevices(string) []Device - // Open a media file for reading, from a path or url. If a format is - // specified, then the format will be used to open the file. Close the - // media object when done. + // Open a media file or device for reading, from a path or url. + // If a format is specified, then the format will be used to open + // the file. Close the media object when done. Open(string, Format, ...string) (Media, error) // Open a media stream for reading. If a format is @@ -33,7 +40,7 @@ type Manager interface { // also close the reader when done. Read(io.Reader, Format, ...string) (Media, error) - // Create a media file for writing, from a path. If a format is + // Create a media file or device for writing, from a path. If a format is // specified, then the format will be used to create the file. Close // the media object when done. Create(string, Format) (Media, error) @@ -43,9 +50,14 @@ type Manager interface { // Close the media object when done. It is the responsibility of the caller to // also close the writer when done. Write(io.Writer, Format) (Media, error) + + // Return version information for the media manager as a set of + // metadata + Version() []Metadata } // Device represents a device for input or output of media streams. +// TODO type Device interface { // Device name, format depends on the device Name() string @@ -53,7 +65,7 @@ type Device interface { // Description of the device Description() string - // Flags indicating the type + // Flags indicating the type INPUT or OUTPUT, AUDIO or VIDEO Type() MediaType // Whether this is the default device @@ -68,40 +80,57 @@ type Format interface { // Description of the format Description() string - // Extensions associated with the format, if a stream + // Extensions associated with the format, if a stream. Each extension + // should have a preceeding period (ie, ".mp4") Extensions() []string // MimeTypes associated with the format MimeTypes() []string - // Flags indicating the type. INPUT for a demuxer or source, OUTPUT for a muxer or sink, DEVICE for a device, FILE for a file. + // Flags indicating the type. INPUT for a demuxer or source, OUTPUT for a muxer or + // sink, DEVICE for a device, FILE for a file. Plus AUDIO, VIDEO, DATA, SUBTITLE. Type() MediaType } -// Media represents a media stream, which can -// be input or output. A new media object is created -// using NewReader, Open, NewWriter or Create. +// Media represents a media stream, which can be input or output. A new media +// object is created using Open, Read, Write or Create. type Media interface { io.Closer - // Return the metadata for the media stream. - Metadata() []Metadata + // Return a decoding context for the media stream, and + // map the streams to decoders. If no function is provided + // (ie, the argument is nil) then all streams are demultiplexed. + Decoder(DecoderMapFunc) (Decoder, error) - // Demultiplex media (when NewReader or Open has - // been used). Pass a packet to a decoder function. - Demux(DecoderFunc) error + // Return INPUT for a demuxer or source, OUTPUT for a muxer or + // sink, DEVICE for a device, FILE for a file or stream. + Type() MediaType - // Return a decode function, which can rescale or - // resample a frame and then call a frame processing - // function for encoding and multiplexing. - Decode(FrameFunc) DecoderFunc + // Return the metadata for the media. + Metadata() []Metadata } +// Return true if a the stream should be decoded +type DecoderMapFunc func(Stream) bool + // Decoder represents a decoder for a media stream. -type Decoder interface{} +type Decoder interface { + // Demultiplex media into packets. Pass a packet to a decoder function. + // TODO + // Stop when the context is cancelled or the end of the media stream is + // reached. + Demux(context.Context, DecoderFunc) error + + /* + // Return a decode function, which can rescale or + // resample a frame and then call a frame processing + // function for encoding and multiplexing. + Decode(FrameFunc) DecoderFunc + */ +} // DecoderFunc is a function that decodes a packet -type DecoderFunc func(Decoder, Packet) error +type DecoderFunc func(Packet) error // FrameFunc is a function that processes a frame of audio // or video data. @@ -110,7 +139,11 @@ type FrameFunc func(Frame) error // Packet represents a packet of demultiplexed data. type Packet interface{} -// Frame represents a frame of audio or video data. +// Stream represents a audio, video, subtitle or data stream +// within a media file +type Stream interface{} + +// Frame represents a frame of audio or picture data. type Frame interface{} // Metadata represents a metadata entry for a media stream. diff --git a/reader.go b/reader.go index 516704a..06bb3bf 100644 --- a/reader.go +++ b/reader.go @@ -78,6 +78,9 @@ func NewReader(r io.Reader, format Format, opts ...string) (*reader, error) { // Set the input format var fmt *ff.AVInputFormat if format != nil { + if format.Type().Is(DEVICE) { + return nil, errors.New("cannot create a reader from a device") + } if inputfmt, ok := format.(*inputformat); ok { fmt = inputfmt.ctx } From 80ce54f61298f48d346d97b7314207b7dbe98725 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Thu, 20 Jun 2024 10:48:01 +0200 Subject: [PATCH 07/16] Updated interface --- decoder.go | 473 +++++++++++++++++++++++++------------- decoder_old.go_old | 249 ++++++++++++++++++++ decoder_test.go | 35 +++ device.go | 79 +++++++ format.go | 207 +++++++++++++++++ media.go => interfaces.go | 80 ++++++- manager.go | 240 +++---------------- manager_test.go | 18 +- metadata.go | 27 +++ pkg/version/version.go | 2 +- reader.go | 166 ++++--------- 11 files changed, 1074 insertions(+), 502 deletions(-) create mode 100644 decoder_old.go_old create mode 100644 decoder_test.go create mode 100644 device.go create mode 100644 format.go rename media.go => interfaces.go (69%) create mode 100644 metadata.go diff --git a/decoder.go b/decoder.go index 2199bfe..d021810 100644 --- a/decoder.go +++ b/decoder.go @@ -1,77 +1,98 @@ package media import ( + // Packages + + "context" "errors" "fmt" + "io" - // Packages ff "github.com/mutablelogic/go-media/sys/ffmpeg61" ) //////////////////////////////////////////////////////////////////////////// // TYPES -// decoder for a single stream includes an audio resampler and output -// frame +// demuxer context - deconstructs media into packets +type demuxer struct { + input *ff.AVFormatContext + decoders map[int]*decoder + frame *ff.AVFrame // Source frame +} + +// decoder context - decodes packets into frames type decoder struct { - codec *ff.AVCodecContext - resampler *ff.SWRContext - rescaler *ff.SWSContext - frame *ff.AVFrame + stream int + codec *ff.AVCodecContext + frame *ff.AVFrame // Destination frame } +var _ Decoder = (*demuxer)(nil) + //////////////////////////////////////////////////////////////////////////// // LIFECYCLE -// Create a decoder for a stream -func (r *reader) NewDecoder(media_type MediaType, stream_num int) (*decoder, error) { - decoder := new(decoder) - - // Find the best stream - stream_num, codec, err := ff.AVFormat_find_best_stream(r.input, ff.AVMediaType(media_type), stream_num, -1) - if err != nil { - return nil, err +func newDemuxer(input *ff.AVFormatContext, mapfn DecoderMapFunc) (*demuxer, error) { + demuxer := new(demuxer) + demuxer.input = input + demuxer.decoders = make(map[int]*decoder) + + // Get all the streams + streams := input.Streams() + + // Create a decoder for each stream + // The decoder map function should be returning the parameters for the + // destination frame. If it's nil then it's mostly a copy. + for _, stream := range streams { + if mapfn == nil || mapfn(stream) { + if decoder, err := demuxer.newDecoder(stream); err != nil { + return nil, errors.Join(err, demuxer.close()) + } else { + streamNum := stream.Index() + demuxer.decoders[streamNum] = decoder + } + } } - // Find the decoder for the stream - dec := ff.AVCodec_find_decoder(codec.ID()) - if dec == nil { - return nil, fmt.Errorf("failed to find decoder for codec %q", codec.Name()) + // Create a frame for encoding - after resampling and resizing + if frame := ff.AVUtil_frame_alloc(); frame == nil { + return nil, errors.Join(demuxer.close(), errors.New("failed to allocate frame")) + } else { + demuxer.frame = frame } - // Allocate a codec context for the decoder - dec_ctx := ff.AVCodec_alloc_context(dec) - if dec_ctx == nil { + // Return success + return demuxer, nil +} + +func (d *demuxer) newDecoder(stream *ff.AVStream) (*decoder, error) { + decoder := new(decoder) + decoder.stream = stream.Id() + + // Create a codec context for the decoder + codec := ff.AVCodec_find_decoder(stream.CodecPar().CodecID()) + if codec == nil { + return nil, fmt.Errorf("failed to find decoder for codec %q", stream.CodecPar().CodecID()) + } else if ctx := ff.AVCodec_alloc_context(codec); ctx == nil { return nil, fmt.Errorf("failed to allocate codec context for codec %q", codec.Name()) + } else { + decoder.codec = ctx } // Copy codec parameters from input stream to output codec context - stream := r.input.Stream(stream_num) - if err := ff.AVCodec_parameters_to_context(dec_ctx, stream.CodecPar()); err != nil { - ff.AVCodec_free_context(dec_ctx) - return nil, fmt.Errorf("failed to copy codec parameters to decoder context for codec %q", codec.Name()) + if err := ff.AVCodec_parameters_to_context(decoder.codec, stream.CodecPar()); err != nil { + return nil, errors.Join(decoder.close(), fmt.Errorf("failed to copy codec parameters to decoder context for codec %q", codec.Name())) } // Init the decoder - if err := ff.AVCodec_open(dec_ctx, dec, nil); err != nil { - ff.AVCodec_free_context(dec_ctx) - return nil, err - } else { - decoder.codec = dec_ctx + if err := ff.AVCodec_open(decoder.codec, codec, nil); err != nil { + return nil, errors.Join(decoder.close(), err) } - // Map the decoder - if _, exists := r.decoders[stream_num]; exists { - ff.AVCodec_free_context(dec_ctx) - return nil, fmt.Errorf("decoder for stream %d already exists", stream_num) - } else { - r.decoders[stream_num] = decoder - } - - // Create a frame for decoder output + // Create a frame for decoder output - after resize/resample if frame := ff.AVUtil_frame_alloc(); frame == nil { - ff.AVCodec_free_context(dec_ctx) - return nil, errors.New("failed to allocate frame") + return nil, errors.Join(decoder.close(), errors.New("failed to allocate frame")) } else { decoder.frame = frame } @@ -80,170 +101,296 @@ func (r *reader) NewDecoder(media_type MediaType, stream_num int) (*decoder, err return decoder, nil } -// Close the decoder -func (d *decoder) Close() { - if d.resampler != nil { - ff.SWResample_free(d.resampler) +func (d *demuxer) close() error { + var result error + + // Free decoded frame + if d.frame != nil { + ff.AVUtil_frame_free(d.frame) } - if d.rescaler != nil { - ff.SWScale_free_context(d.rescaler) + + // Free resources + for _, decoder := range d.decoders { + result = errors.Join(result, decoder.close()) } - ff.AVUtil_frame_free(d.frame) - ff.AVCodec_free_context(d.codec) + d.decoders = nil + + // Return any errors + return result } -//////////////////////////////////////////////////////////////////////////// -// STRINGIFY +func (d *decoder) close() error { + var result error + + // Free the codec context + if d.codec != nil { + ff.AVCodec_free_context(d.codec) + } -func (d *decoder) String() string { - return d.codec.String() + // Free destination frame + if d.frame != nil { + ff.AVUtil_frame_free(d.frame) + } + + // Return any errors + return result } //////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS -// Resample the audio as int16 non-planar samples -// TODO: This should be NewAudioDecoder(..., sample_rate, sample_format, channel_layout) -func (decoder *decoder) ResampleS16Mono(sample_rate int) error { - // Check decoder type - if decoder.codec.Codec().Type() != ff.AVMEDIA_TYPE_AUDIO { - return fmt.Errorf("decoder is not an audio decoder") +func (d *demuxer) Demux(ctx context.Context, fn DecoderFunc) error { + // If the decoder is nil then set it to default - which is to send the + // packet to the appropriate decoder + if fn == nil { + fn = func(packet Packet) error { + fmt.Println("TODO", packet) + return nil + } } - // TODO: Currently hard-coded to 16-bit mono at 44.1kHz - decoder.frame.SetSampleRate(sample_rate) - decoder.frame.SetSampleFormat(ff.AV_SAMPLE_FMT_S16) - if err := decoder.frame.SetChannelLayout(ff.AV_CHANNEL_LAYOUT_STEREO); err != nil { - return err + // Allocate a packet + packet := ff.AVCodec_packet_alloc() + if packet == nil { + return errors.New("failed to allocate packet") } - - // Create a new resampler - ctx := ff.SWResample_alloc() - if ctx == nil { - return errors.New("failed to allocate resampler") - } else { - decoder.resampler = ctx + defer ff.AVCodec_packet_free(packet) + + // Read packets +FOR_LOOP: + for { + select { + case <-ctx.Done(): + break FOR_LOOP + default: + if err := ff.AVFormat_read_frame(d.input, packet); errors.Is(err, io.EOF) { + break FOR_LOOP + } else if err != nil { + return err + } + stream := packet.StreamIndex() + if decoder := d.decoders[stream]; decoder != nil { + if err := decoder.decode(fn, packet); errors.Is(err, io.EOF) { + break FOR_LOOP + } else if err != nil { + return err + } + } + // Unreference the packet + ff.AVCodec_packet_unref(packet) + } } - // Set options to covert from the codec frame to the decoder frame - if err := ff.SWResample_set_opts(ctx, - decoder.frame.ChannelLayout(), decoder.frame.SampleFormat(), decoder.frame.SampleRate(), // destination - decoder.codec.ChannelLayout(), decoder.codec.SampleFormat(), decoder.codec.SampleRate(), // source - ); err != nil { - return fmt.Errorf("SWResample_set_opts: %w", err) + // Flush the decoders + for _, decoder := range d.decoders { + if err := decoder.decode(fn, nil); err != nil { + return err + } } - // Initialize the resampling context - if err := ff.SWResample_init(ctx); err != nil { - return fmt.Errorf("SWResample_init: %w", err) - } + // Return the context error - will be cancelled, perhaps, or nil if the + // demuxer finished successfully without cancellation + return ctx.Err() +} - // Return success - return nil +func (d *decoder) decode(fn DecoderFunc, packet *ff.AVPacket) error { + // Send the packet to the user defined packet function or + // to the default version + return fn(packet) } -// Rescale the video -// TODO: This should be NewVideoDecoder(..., pixel_format, width, height) -func (decoder *decoder) Rescale(width, height int) error { - // Check decoder type - if decoder.codec.Codec().Type() != ff.AVMEDIA_TYPE_VIDEO { - return fmt.Errorf("decoder is not an video decoder") - } - - // TODO: Currently hard-coded - decoder.frame.SetPixFmt(ff.AV_PIX_FMT_GRAY8) - decoder.frame.SetWidth(width) - decoder.frame.SetHeight(height) - - // Create scaling context - ctx := ff.SWScale_get_context( - decoder.codec.Width(), decoder.codec.Height(), decoder.codec.PixFmt(), // source - decoder.frame.Width(), decoder.frame.Height(), decoder.frame.PixFmt(), // destination - ff.SWS_BILINEAR, nil, nil, nil) - if ctx == nil { - return errors.New("failed to allocate swscale context") +/* + // Get the codec + stream. + + // Find the decoder for the stream + dec := ff.AVCodec_find_decoder(codec.ID()) + if dec == nil { + return nil, fmt.Errorf("failed to find decoder for codec %q", codec.Name()) + } + + // Allocate a codec context for the decoder + dec_ctx := ff.AVCodec_alloc_context(dec) + if dec_ctx == nil { + return nil, fmt.Errorf("failed to allocate codec context for codec %q", codec.Name()) + } + + // Create a frame for encoding - after resampling and resizing + if frame := ff.AVUtil_frame_alloc(); frame == nil { + return nil, errors.New("failed to allocate frame") } else { - decoder.rescaler = ctx + decoder.frame = frame } // Return success - return nil + return decoder, nil } -// Ref: -// https://github.com/romatthe/alephone/blob/b1f7af38b14f74585f0442f1dd757d1238bfcef4/Source_Files/FFmpeg/SDL_ffmpeg.c#L2048 -func (decoder *decoder) re(src *ff.AVFrame) (*ff.AVFrame, error) { - switch decoder.codec.Codec().Type() { - case ff.AVMEDIA_TYPE_AUDIO: - // Resample the audio - can flush if src is nil - if decoder.resampler != nil { - if err := decoder.resample(decoder.frame, src); err != nil { - return nil, err - } - return decoder.frame, nil - } - case ff.AVMEDIA_TYPE_VIDEO: - // Rescale the video - if decoder.rescaler != nil && src != nil { - if err := decoder.rescale(decoder.frame, src); err != nil { - return nil, err - } - return decoder.frame, nil - } - } +// Close the demuxer +func (d *demuxer) close() error { - // NO-OP - just return the source frame - return src, nil } -func (decoder *decoder) resample(dest, src *ff.AVFrame) error { - num_samples := 0 - if src != nil { - num_samples = src.NumSamples() - } - dest_samples, err := ff.SWResample_get_out_samples(decoder.resampler, num_samples) - if err != nil { - return fmt.Errorf("SWResample_get_out_samples: %w", err) - } +// Close the decoder +func (d *decoder) close() error { - dest.SetNumSamples(dest_samples) - if src != nil { - dest.SetPts(decoder.get_next_pts(src)) +} + +// Demultiplex streams from the reader +func (d *demuxer) Demux(ctx context.Context, fn DecoderFunc) error { + // If the decoder is nil then set it to default + if fn == nil { + fn = func(packet Packet) error { + if packet == nil { + return d.decodePacket(nil) + } + return d.decodePacket(packet.(*ff.AVPacket)) + } } - // Perform resampling - if err := ff.SWResample_convert_frame(decoder.resampler, src, dest); err != nil { - return fmt.Errorf("SWResample_convert_frame: %w", err) + // Allocate a packet + packet := ff.AVCodec_packet_alloc() + if packet == nil { + return errors.New("failed to allocate packet") + } + defer ff.AVCodec_packet_free(packet) + + // Read packets +FOR_LOOP: + for { + select { + case <-ctx.Done(): + break FOR_LOOP + default: + if err := ff.AVFormat_read_frame(d.input, packet); errors.Is(err, io.EOF) { + break + } else if err != nil { + return err + } + stream := packet.StreamIndex() + if decoder := d.decoders[stream]; decoder != nil { + if err := decoder.decode(fn, packet); errors.Is(err, io.EOF) { + break FOR_LOOP + } else if err != nil { + return err + } + } + // Unreference the packet + ff.AVCodec_packet_unref(packet) + } } - //fmt.Println("in_samples", src.NumSamples(), "out_samples", dest.NumSamples()) - //fmt.Println("in_pts", src.Pts(), "out_pts", dest.Pts()) - //fmt.Println("in_timebase", src.TimeBase(), "out_timebase", dest.TimeBase()) + // Flush the decoders + for _, decoder := range d.decoders { + if err := decoder.decode(fn, nil); err != nil { + return err + } + } + // Return success return nil } -func (decoder *decoder) rescale(dest, src *ff.AVFrame) error { - // Copy properties from source - //if err := ff.AVUtil_frame_copy_props(dest, src); err != nil { - // return fmt.Errorf("failed to copy props: %w", err) - //} - // Perform resizing - if err := ff.SWScale_scale_frame(decoder.rescaler, dest, src, false); err != nil { - return fmt.Errorf("SWScale_scale_frame: %w", err) +func (d *decoder) decode(fn DecoderFunc, packet *ff.AVPacket) error { + // Send the packet to the user defined packet function or + // to the default version + return fn(packet) +} + +func (d *demuxer) decodePacket(packet *ff.AVPacket) error { + // Submit the packet to the decoder. If nil then flush + if err := ff.AVCodec_send_packet(d.codec, packet); err != nil { + return err } - // Return success + // get all the available frames from the decoder + for { + if err := ff.AVCodec_receive_frame(d.codec, d.frame); errors.Is(err, syscall.EAGAIN) || errors.Is(err, io.EOF) { + // Finished decoding packet or EOF + break + } else if err != nil { + return err + } + + fmt.Println("TODO", d.frame) + } return nil } -func (decoder *decoder) get_next_pts(src *ff.AVFrame) int64 { - ts := src.BestEffortTs() - if ts == ff.AV_NOPTS_VALUE { - ts = src.Pts() +// Resample or resize the frame, then pass back +/* + if frame, err := d.re(d.frame); err != nil { + return err + } else if err := fn(frame); errors.Is(err, io.EOF) { + // End early + break + } else if err != nil { + return err + }*/ + +// Flush +/* + if frame, err := d.re(nil); err != nil { + return err + } else if frame == nil { + // NOOP + } else if err := fn(frame); errors.Is(err, io.EOF) { + // NOOP + } else if err != nil { + return err } - if ts == ff.AV_NOPTS_VALUE { - return ff.AV_NOPTS_VALUE +*/ + +/* + +// Return a function to decode packets from the streams into frames +func (r *reader) Decode(fn FrameFunc) DecoderFunc { + return func(codec Decoder, packet Packet) error { + if packet != nil { + // Submit the packet to the decoder + if err := ff.AVCodec_send_packet(codec.(*decoder).codec, packet.(*ff.AVPacket)); err != nil { + return err + } + } else { + // Flush remaining frames + if err := ff.AVCodec_send_packet(codec.(*decoder).codec, nil); err != nil { + return err + } + } + + // get all the available frames from the decoder + for { + if err := ff.AVCodec_receive_frame(codec.(*decoder).codec, r.frame); errors.Is(err, syscall.EAGAIN) || errors.Is(err, io.EOF) { + // Finished decoding packet or EOF + break + } else if err != nil { + return err + } + + // Resample or resize the frame, then pass back + if frame, err := codec.(*decoder).re(r.frame); err != nil { + return err + } else if err := fn(frame); errors.Is(err, io.EOF) { + // End early + break + } else if err != nil { + return err + } + } + + // Flush + if frame, err := codec.(*decoder).re(nil); err != nil { + return err + } else if frame == nil { + // NOOP + } else if err := fn(frame); errors.Is(err, io.EOF) { + // NOOP + } else if err != nil { + return err + } + + // Success + return nil } - return ff.SWResample_next_pts(decoder.resampler, ts) } +*/ diff --git a/decoder_old.go_old b/decoder_old.go_old new file mode 100644 index 0000000..2199bfe --- /dev/null +++ b/decoder_old.go_old @@ -0,0 +1,249 @@ +package media + +import ( + "errors" + "fmt" + + // Packages + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +//////////////////////////////////////////////////////////////////////////// +// TYPES + +// decoder for a single stream includes an audio resampler and output +// frame +type decoder struct { + codec *ff.AVCodecContext + resampler *ff.SWRContext + rescaler *ff.SWSContext + frame *ff.AVFrame +} + +//////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// Create a decoder for a stream +func (r *reader) NewDecoder(media_type MediaType, stream_num int) (*decoder, error) { + decoder := new(decoder) + + // Find the best stream + stream_num, codec, err := ff.AVFormat_find_best_stream(r.input, ff.AVMediaType(media_type), stream_num, -1) + if err != nil { + return nil, err + } + + // Find the decoder for the stream + dec := ff.AVCodec_find_decoder(codec.ID()) + if dec == nil { + return nil, fmt.Errorf("failed to find decoder for codec %q", codec.Name()) + } + + // Allocate a codec context for the decoder + dec_ctx := ff.AVCodec_alloc_context(dec) + if dec_ctx == nil { + return nil, fmt.Errorf("failed to allocate codec context for codec %q", codec.Name()) + } + + // Copy codec parameters from input stream to output codec context + stream := r.input.Stream(stream_num) + if err := ff.AVCodec_parameters_to_context(dec_ctx, stream.CodecPar()); err != nil { + ff.AVCodec_free_context(dec_ctx) + return nil, fmt.Errorf("failed to copy codec parameters to decoder context for codec %q", codec.Name()) + } + + // Init the decoder + if err := ff.AVCodec_open(dec_ctx, dec, nil); err != nil { + ff.AVCodec_free_context(dec_ctx) + return nil, err + } else { + decoder.codec = dec_ctx + } + + // Map the decoder + if _, exists := r.decoders[stream_num]; exists { + ff.AVCodec_free_context(dec_ctx) + return nil, fmt.Errorf("decoder for stream %d already exists", stream_num) + } else { + r.decoders[stream_num] = decoder + } + + // Create a frame for decoder output + if frame := ff.AVUtil_frame_alloc(); frame == nil { + ff.AVCodec_free_context(dec_ctx) + return nil, errors.New("failed to allocate frame") + } else { + decoder.frame = frame + } + + // Return success + return decoder, nil +} + +// Close the decoder +func (d *decoder) Close() { + if d.resampler != nil { + ff.SWResample_free(d.resampler) + } + if d.rescaler != nil { + ff.SWScale_free_context(d.rescaler) + } + ff.AVUtil_frame_free(d.frame) + ff.AVCodec_free_context(d.codec) +} + +//////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (d *decoder) String() string { + return d.codec.String() +} + +//////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Resample the audio as int16 non-planar samples +// TODO: This should be NewAudioDecoder(..., sample_rate, sample_format, channel_layout) +func (decoder *decoder) ResampleS16Mono(sample_rate int) error { + // Check decoder type + if decoder.codec.Codec().Type() != ff.AVMEDIA_TYPE_AUDIO { + return fmt.Errorf("decoder is not an audio decoder") + } + + // TODO: Currently hard-coded to 16-bit mono at 44.1kHz + decoder.frame.SetSampleRate(sample_rate) + decoder.frame.SetSampleFormat(ff.AV_SAMPLE_FMT_S16) + if err := decoder.frame.SetChannelLayout(ff.AV_CHANNEL_LAYOUT_STEREO); err != nil { + return err + } + + // Create a new resampler + ctx := ff.SWResample_alloc() + if ctx == nil { + return errors.New("failed to allocate resampler") + } else { + decoder.resampler = ctx + } + + // Set options to covert from the codec frame to the decoder frame + if err := ff.SWResample_set_opts(ctx, + decoder.frame.ChannelLayout(), decoder.frame.SampleFormat(), decoder.frame.SampleRate(), // destination + decoder.codec.ChannelLayout(), decoder.codec.SampleFormat(), decoder.codec.SampleRate(), // source + ); err != nil { + return fmt.Errorf("SWResample_set_opts: %w", err) + } + + // Initialize the resampling context + if err := ff.SWResample_init(ctx); err != nil { + return fmt.Errorf("SWResample_init: %w", err) + } + + // Return success + return nil +} + +// Rescale the video +// TODO: This should be NewVideoDecoder(..., pixel_format, width, height) +func (decoder *decoder) Rescale(width, height int) error { + // Check decoder type + if decoder.codec.Codec().Type() != ff.AVMEDIA_TYPE_VIDEO { + return fmt.Errorf("decoder is not an video decoder") + } + + // TODO: Currently hard-coded + decoder.frame.SetPixFmt(ff.AV_PIX_FMT_GRAY8) + decoder.frame.SetWidth(width) + decoder.frame.SetHeight(height) + + // Create scaling context + ctx := ff.SWScale_get_context( + decoder.codec.Width(), decoder.codec.Height(), decoder.codec.PixFmt(), // source + decoder.frame.Width(), decoder.frame.Height(), decoder.frame.PixFmt(), // destination + ff.SWS_BILINEAR, nil, nil, nil) + if ctx == nil { + return errors.New("failed to allocate swscale context") + } else { + decoder.rescaler = ctx + } + + // Return success + return nil +} + +// Ref: +// https://github.com/romatthe/alephone/blob/b1f7af38b14f74585f0442f1dd757d1238bfcef4/Source_Files/FFmpeg/SDL_ffmpeg.c#L2048 +func (decoder *decoder) re(src *ff.AVFrame) (*ff.AVFrame, error) { + switch decoder.codec.Codec().Type() { + case ff.AVMEDIA_TYPE_AUDIO: + // Resample the audio - can flush if src is nil + if decoder.resampler != nil { + if err := decoder.resample(decoder.frame, src); err != nil { + return nil, err + } + return decoder.frame, nil + } + case ff.AVMEDIA_TYPE_VIDEO: + // Rescale the video + if decoder.rescaler != nil && src != nil { + if err := decoder.rescale(decoder.frame, src); err != nil { + return nil, err + } + return decoder.frame, nil + } + } + + // NO-OP - just return the source frame + return src, nil +} + +func (decoder *decoder) resample(dest, src *ff.AVFrame) error { + num_samples := 0 + if src != nil { + num_samples = src.NumSamples() + } + dest_samples, err := ff.SWResample_get_out_samples(decoder.resampler, num_samples) + if err != nil { + return fmt.Errorf("SWResample_get_out_samples: %w", err) + } + + dest.SetNumSamples(dest_samples) + if src != nil { + dest.SetPts(decoder.get_next_pts(src)) + } + + // Perform resampling + if err := ff.SWResample_convert_frame(decoder.resampler, src, dest); err != nil { + return fmt.Errorf("SWResample_convert_frame: %w", err) + } + + //fmt.Println("in_samples", src.NumSamples(), "out_samples", dest.NumSamples()) + //fmt.Println("in_pts", src.Pts(), "out_pts", dest.Pts()) + //fmt.Println("in_timebase", src.TimeBase(), "out_timebase", dest.TimeBase()) + + return nil +} + +func (decoder *decoder) rescale(dest, src *ff.AVFrame) error { + // Copy properties from source + //if err := ff.AVUtil_frame_copy_props(dest, src); err != nil { + // return fmt.Errorf("failed to copy props: %w", err) + //} + // Perform resizing + if err := ff.SWScale_scale_frame(decoder.rescaler, dest, src, false); err != nil { + return fmt.Errorf("SWScale_scale_frame: %w", err) + } + + // Return success + return nil +} + +func (decoder *decoder) get_next_pts(src *ff.AVFrame) int64 { + ts := src.BestEffortTs() + if ts == ff.AV_NOPTS_VALUE { + ts = src.Pts() + } + if ts == ff.AV_NOPTS_VALUE { + return ff.AV_NOPTS_VALUE + } + return ff.SWResample_next_pts(decoder.resampler, ts) +} diff --git a/decoder_test.go b/decoder_test.go new file mode 100644 index 0000000..fb03a99 --- /dev/null +++ b/decoder_test.go @@ -0,0 +1,35 @@ +package media_test + +import ( + // Import namespaces + "context" + "testing" + + // Package imports + "github.com/stretchr/testify/assert" + + // Namespace imports + . "github.com/mutablelogic/go-media" +) + +func Test_decoder_001(t *testing.T) { + assert := assert.New(t) + + manager := NewManager() + media, err := manager.Open("./etc/test/sample.mp4", nil) + if !assert.NoError(err) { + t.SkipNow() + } + defer media.Close() + + decoder, err := media.Decoder(func (stream Stream) Parameters { + // Copy parameters from the stream + return stream.Parameters() + } + if !assert.NoError(err) { + t.SkipNow() + } + + // Demuliplex the stream + assert.NoError(decoder.Demux(context.Background(), nil)) +} diff --git a/device.go b/device.go new file mode 100644 index 0000000..e6c1cde --- /dev/null +++ b/device.go @@ -0,0 +1,79 @@ +package media + +import ( + "encoding/json" + + // Package imports + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +//////////////////////////////////////////////////////////////////////////// +// TYPES + +type device struct { + devicemeta +} + +type devicemeta struct { + Format string `json:"format"` + Name string `json:"name"` + Description string `json:"description"` + Default bool `json:"default,omitempty"` + MediaType MediaType `json:"type,omitempty" writer:",wrap,width:21"` +} + +//////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +func newInputDevice(ctx *ff.AVInputFormat, d *ff.AVDeviceInfo, t MediaType, def bool) *device { + meta := &devicemeta{ + Format: ctx.Name(), + Name: d.Name(), + Description: d.Description(), + Default: def, + MediaType: INPUT | t, + } + return &device{devicemeta: *meta} +} + +func newOutputDevice(ctx *ff.AVOutputFormat, d *ff.AVDeviceInfo, t MediaType, def bool) *device { + meta := &devicemeta{ + Format: ctx.Name(), + Name: d.Name(), + Description: d.Description(), + Default: def, + MediaType: OUTPUT | t, + } + return &device{devicemeta: *meta} +} + +//////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (v *device) String() string { + data, _ := json.MarshalIndent(v, "", " ") + return string(data) +} + +//////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Device name, format depends on the device +func (v *device) Name() string { + return v.devicemeta.Name +} + +// Description of the device +func (v *device) Description() string { + return v.devicemeta.Description +} + +// Flags indicating the type INPUT or OUTPUT, AUDIO or VIDEO +func (v *device) Type() MediaType { + return v.devicemeta.MediaType +} + +// Whether this is the default device +func (v *device) Default() bool { + return v.devicemeta.Default +} diff --git a/format.go b/format.go new file mode 100644 index 0000000..c41e4a2 --- /dev/null +++ b/format.go @@ -0,0 +1,207 @@ +package media + +import ( + "encoding/json" + "slices" + "strings" + + // Package imports + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +//////////////////////////////////////////////////////////////////////////// +// TYPES + +type formatmeta struct { + Name string `json:"name" writer:",width:25"` + Description string `json:"description" writer:",wrap,width:40"` + Extensions string `json:"extensions,omitempty"` + MimeTypes string `json:"mimetypes,omitempty" writer:",wrap,width:40"` + MediaType MediaType `json:"type,omitempty" writer:",wrap,width:21"` +} + +type inputformat struct { + formatmeta + ctx *ff.AVInputFormat +} + +type outputformat struct { + formatmeta + ctx *ff.AVOutputFormat +} + +var _ Format = (*inputformat)(nil) +var _ Format = (*outputformat)(nil) + +//////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +func newInputFormat(ctx *ff.AVInputFormat, t MediaType) *inputformat { + v := &inputformat{ctx: ctx} + v.formatmeta.Name = strings.Join(v.Name(), " ") + v.formatmeta.Description = v.Description() + v.formatmeta.Extensions = strings.Join(v.Extensions(), " ") + v.formatmeta.MimeTypes = strings.Join(v.MimeTypes(), " ") + v.formatmeta.MediaType = INPUT | t + return v +} + +func newOutputFormat(ctx *ff.AVOutputFormat, t MediaType) *outputformat { + v := &outputformat{ctx: ctx} + v.formatmeta.Name = strings.Join(v.Name(), " ") + v.formatmeta.Description = v.Description() + v.formatmeta.Extensions = strings.Join(v.Extensions(), " ") + v.formatmeta.MimeTypes = strings.Join(v.MimeTypes(), " ") + v.formatmeta.MediaType = OUTPUT | t + return v +} + +//////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (v *inputformat) MarshalJSON() ([]byte, error) { + return json.Marshal(v.ctx) +} + +func (v *outputformat) MarshalJSON() ([]byte, error) { + return json.Marshal(v.ctx) +} + +func (v *inputformat) String() string { + data, _ := json.MarshalIndent(v, "", " ") + return string(data) +} + +func (v *outputformat) String() string { + data, _ := json.MarshalIndent(v, "", " ") + return string(data) +} + +//////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +func (v *inputformat) Name() []string { + return strings.Split(v.ctx.Name(), ",") +} + +func (v *inputformat) Description() string { + return v.ctx.LongName() +} + +func (v *inputformat) Extensions() []string { + result := []string{} + for _, ext := range strings.Split(v.ctx.Extensions(), ",") { + ext = strings.TrimSpace(ext) + if ext != "" { + result = append(result, "."+ext) + } + } + return result +} + +func (v *inputformat) MimeTypes() []string { + result := []string{} + for _, mimetype := range strings.Split(v.ctx.MimeTypes(), ",") { + if mimetype != "" { + result = append(result, mimetype) + } + } + return result +} + +func (v *inputformat) Type() MediaType { + return INPUT +} + +func (v *outputformat) Name() []string { + return strings.Split(v.ctx.Name(), ",") +} + +func (v *outputformat) Description() string { + return v.ctx.LongName() +} + +func (v *outputformat) Extensions() []string { + result := []string{} + for _, ext := range strings.Split(v.ctx.Extensions(), ",") { + ext = strings.TrimSpace(ext) + if ext != "" { + result = append(result, "."+ext) + } + } + return result +} + +func (v *outputformat) MimeTypes() []string { + result := []string{} + for _, mimetype := range strings.Split(v.ctx.MimeTypes(), ",") { + if mimetype != "" { + result = append(result, mimetype) + } + } + return result +} + +func (v *outputformat) Type() MediaType { + return OUTPUT +} + +//////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +func matchesInput(demuxer *ff.AVInputFormat, media_type MediaType, mimetype ...string) bool { + // TODO: media_type + + // Match any + if len(mimetype) == 0 && media_type == ANY { + return true + } + // Match mimetype + for _, mimetype := range mimetype { + mimetype = strings.ToLower(strings.TrimSpace(mimetype)) + if slices.Contains(strings.Split(demuxer.Name(), ","), mimetype) { + return true + } + if strings.HasPrefix(mimetype, ".") { + ext := strings.TrimPrefix(mimetype, ".") + if slices.Contains(strings.Split(demuxer.Extensions(), ","), ext) { + return true + } + } + if slices.Contains(strings.Split(demuxer.MimeTypes(), ","), mimetype) { + return true + } + } + // No match + return false +} + +func matchesOutput(muxer *ff.AVOutputFormat, media_type MediaType, filter ...string) bool { + // TODO: media_type + + // Match any + if len(filter) == 0 && media_type == ANY { + return true + } + // Match mimetype + for _, filter := range filter { + if filter == "" { + continue + } + filter = strings.ToLower(strings.TrimSpace(filter)) + if slices.Contains(strings.Split(muxer.Name(), ","), filter) { + return true + } + if strings.HasPrefix(filter, ".") { + if slices.Contains(strings.Split(muxer.Extensions(), ","), filter[1:]) { + return true + } + } + mt := strings.Split(muxer.MimeTypes(), ",") + if slices.Contains(mt, filter) { + return true + } + } + // No match + return false +} diff --git a/media.go b/interfaces.go similarity index 69% rename from media.go rename to interfaces.go index e4c4979..ec1c919 100644 --- a/media.go +++ b/interfaces.go @@ -23,10 +23,14 @@ type Manager interface { // can be NONE (for any) or combinations of DEVICE and STREAM. OutputFormats(MediaType, ...string) []Format - // Return supported input devices for a given name + // Return supported input devices for a given format name + // Not all devices may be supported on all platforms or listed + // if the device does not support enumeration. InputDevices(string) []Device - // Return supported output devices for a given name + // Return supported output devices for a given format name + // Not all devices may be supported on all platforms or listed + // if the device does not support enumeration. OutputDevices(string) []Device // Open a media file or device for reading, from a path or url. @@ -43,17 +47,36 @@ type Manager interface { // Create a media file or device for writing, from a path. If a format is // specified, then the format will be used to create the file. Close // the media object when done. + // TODO Create(string, Format) (Media, error) // Create a media stream for writing. If a format is // specified, then the format will be used to create the file. // Close the media object when done. It is the responsibility of the caller to // also close the writer when done. + // TODO Write(io.Writer, Format) (Media, error) // Return version information for the media manager as a set of // metadata Version() []Metadata + + // Return all supported channel layouts + ChannelLayouts() []Metadata + + // Return all supported sample formats + SampleFormats() []Metadata + + // Return all supported pixel formats + PixelFormats() []Metadata + + // Return audio parameters for encoding + // ChannelLayout, SampleFormat, SampleRate + AudioParameters(string, string, int) (AudioParameters, error) + + // Return video parameters for encoding + // Width, Height, PixelFormat, FrameRate + VideoParameters(int, int, string, int) (VideoParameters, error) } // Device represents a device for input or output of media streams. @@ -73,6 +96,7 @@ type Device interface { } // Format represents a container format for input or output of media streams. +// Use the manager object to get a list of supported formats. type Format interface { // Name(s) of the format Name() []string @@ -93,7 +117,7 @@ type Format interface { } // Media represents a media stream, which can be input or output. A new media -// object is created using Open, Read, Write or Create. +// object is created using the Manager object type Media interface { io.Closer @@ -110,8 +134,9 @@ type Media interface { Metadata() []Metadata } -// Return true if a the stream should be decoded -type DecoderMapFunc func(Stream) bool +// Return encoding parameters if a the stream should be decoded +// and either resampled or resized +type DecoderMapFunc func(Stream) Parameters // Decoder represents a decoder for a media stream. type Decoder interface { @@ -129,6 +154,42 @@ type Decoder interface { */ } +// Parameters represents a set of parameters for encoding +type Parameters interface { + AudioParameters + VideoParameters + + // Return the media type (AUDIO, VIDEO, SUBTITLE, DATA) + Type() MediaType +} + +// Audio parameters for encoding or decoding audio data. +type AudioParameters interface { + // Return the channel layout + ChannelLayout() string + + // Return the sample format + SampleFormat() string + + // Return the sample rate (Hz) + SampleRate() int +} + +// Video parameters for encoding or decoding video data. +type VideoParameters interface { + // Return the width of the video frame + Width() int + + // Return the height of the video frame + Height() int + + // Return the pixel format + PixelFormat() string + + // Return the frame rate (fps) + FrameRate() int +} + // DecoderFunc is a function that decodes a packet type DecoderFunc func(Packet) error @@ -137,11 +198,18 @@ type DecoderFunc func(Packet) error type FrameFunc func(Frame) error // Packet represents a packet of demultiplexed data. +// Currently this is quite opaque! type Packet interface{} // Stream represents a audio, video, subtitle or data stream // within a media file -type Stream interface{} +type Stream interface { + // Return AUDIO, VIDEO, SUBTITLE or DATA + Type() MediaType + + // Return the stream parameters + Parameters() Parameters +} // Frame represents a frame of audio or picture data. type Frame interface{} diff --git a/manager.go b/manager.go index f14b922..6cac2b1 100644 --- a/manager.go +++ b/manager.go @@ -1,13 +1,12 @@ package media import ( - "encoding/json" "fmt" "io" - "slices" - "strings" + "runtime" // Package imports + version "github.com/mutablelogic/go-media/pkg/version" ff "github.com/mutablelogic/go-media/sys/ffmpeg61" // Namespace imports @@ -20,31 +19,7 @@ import ( type manager struct { } -type formatmeta struct { - Name string `json:"name" writer:",width:25"` - Description string `json:"description" writer:",wrap,width:40"` - Extensions string `json:"extensions,omitempty"` - MimeTypes string `json:"mimetypes,omitempty" writer:",wrap,width:40"` - MediaType MediaType `json:"type,omitempty" writer:",wrap,width:21"` -} - -type inputformat struct { - formatmeta - ctx *ff.AVInputFormat -} - -type outputformat struct { - formatmeta - ctx *ff.AVOutputFormat -} - -type device struct { - Format string `json:"format"` - Name string `json:"name"` - Description string `json:"description"` - Default bool `json:"default,omitempty"` - MediaType MediaType `json:"type,omitempty" writer:",wrap,width:21"` -} +var _ Manager = (*manager)(nil) //////////////////////////////////////////////////////////////////////////// // LIFECYCLE @@ -53,67 +28,6 @@ func NewManager() Manager { return new(manager) } -func newInputFormat(ctx *ff.AVInputFormat, t MediaType) *inputformat { - v := &inputformat{ctx: ctx} - v.formatmeta.Name = strings.Join(v.Name(), " ") - v.formatmeta.Description = v.Description() - v.formatmeta.Extensions = strings.Join(v.Extensions(), " ") - v.formatmeta.MimeTypes = strings.Join(v.MimeTypes(), " ") - v.formatmeta.MediaType = INPUT | t - return v -} - -func newOutputFormat(ctx *ff.AVOutputFormat, t MediaType) *outputformat { - v := &outputformat{ctx: ctx} - v.formatmeta.Name = strings.Join(v.Name(), " ") - v.formatmeta.Description = v.Description() - v.formatmeta.Extensions = strings.Join(v.Extensions(), " ") - v.formatmeta.MimeTypes = strings.Join(v.MimeTypes(), " ") - v.formatmeta.MediaType = OUTPUT | t - return v -} - -func newInputDevice(ctx *ff.AVInputFormat, d *ff.AVDeviceInfo, t MediaType, def bool) *device { - v := &device{} - v.Format = ctx.Name() - v.Name = d.Name() - v.Description = d.Description() - v.Default = def - v.MediaType = INPUT | t - return v -} - -func newOutputDevice(ctx *ff.AVOutputFormat, d *ff.AVDeviceInfo, t MediaType, def bool) *device { - v := &device{} - v.Format = ctx.Name() - v.Name = d.Name() - v.Description = d.Description() - v.Default = def - v.MediaType = OUTPUT | t - return v -} - -//////////////////////////////////////////////////////////////////////////// -// STRINGIFY - -func (v *inputformat) MarshalJSON() ([]byte, error) { - return json.Marshal(v.ctx) -} - -func (v *outputformat) MarshalJSON() ([]byte, error) { - return json.Marshal(v.ctx) -} - -func (v *inputformat) String() string { - data, _ := json.MarshalIndent(v, "", " ") - return string(data) -} - -func (v *outputformat) String() string { - data, _ := json.MarshalIndent(v, "", " ") - return string(data) -} - //////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS @@ -217,7 +131,9 @@ func (manager *manager) OutputFormats(t MediaType, filter ...string) []Format { return result } -// Return supported input devices for a given input format +// Return supported input devices for a given input format. Sometimes +// (ie, AVFoundation) there is a option which provides the input +// devices and this function returns an empty string instead. Go figure! func (manager *manager) InputDevices(format string) []Device { input := ff.AVFormat_find_input_format(format) if input == nil { @@ -249,12 +165,12 @@ func (manager *manager) OutputDevices(format string) []Device { // Open a media file or device for reading, from a path or url. func (manager *manager) Open(url string, format Format, opts ...string) (Media, error) { - return Open(url, format, opts...) + return newMedia(url, format, opts...) } // Open a media stream for reading. func (manager *manager) Read(r io.Reader, format Format, opts ...string) (Media, error) { - return NewReader(r, format, opts...) + return newReader(r, format, opts...) } // Create a media file for writing, from a path. @@ -267,128 +183,32 @@ func (manager *manager) Write(io.Writer, Format) (Media, error) { return nil, ErrNotImplemented } -func (v *inputformat) Name() []string { - return strings.Split(v.ctx.Name(), ",") -} - -func (v *inputformat) Description() string { - return v.ctx.LongName() -} - -func (v *inputformat) Extensions() []string { - result := []string{} - for _, ext := range strings.Split(v.ctx.Extensions(), ",") { - ext = strings.TrimSpace(ext) - if ext != "" { - result = append(result, "."+ext) - } - } - return result -} - -func (v *inputformat) MimeTypes() []string { - result := []string{} - for _, mimetype := range strings.Split(v.ctx.MimeTypes(), ",") { - if mimetype != "" { - result = append(result, mimetype) - } - } - return result -} - -func (v *inputformat) Type() MediaType { - return INPUT -} - -func (v *outputformat) Name() []string { - return strings.Split(v.ctx.Name(), ",") -} - -func (v *outputformat) Description() string { - return v.ctx.LongName() -} - -func (v *outputformat) Extensions() []string { - result := []string{} - for _, ext := range strings.Split(v.ctx.Extensions(), ",") { - ext = strings.TrimSpace(ext) - if ext != "" { - result = append(result, "."+ext) - } +// Return version information for the media manager as a set of metadata +func (manager *manager) Version() []Metadata { + metadata := []Metadata{ + newMetadata("libavcodec_version", ff.AVCodec_version()), + newMetadata("libavformat_versionn", ff.AVFormat_version()), + newMetadata("libavutil_version", ff.AVUtil_version()), + newMetadata("libavdevice_version", ff.AVDevice_version()), + // newMetadata("libavfilter_version", ff.AVFilter_version()), + newMetadata("libswscale_version", ff.SWScale_version()), + newMetadata("libswresample_version", ff.SWResample_version()), } - return result -} - -func (v *outputformat) MimeTypes() []string { - result := []string{} - for _, mimetype := range strings.Split(v.ctx.MimeTypes(), ",") { - if mimetype != "" { - result = append(result, mimetype) - } + if version.GitSource != "" { + metadata = append(metadata, newMetadata("git_source", version.GitSource)) } - return result -} - -func (v *outputformat) Type() MediaType { - return OUTPUT -} - -//////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - -func matchesInput(demuxer *ff.AVInputFormat, media_type MediaType, mimetype ...string) bool { - // TODO: media_type - - // Match any - if len(mimetype) == 0 && media_type == ANY { - return true + if version.GitBranch != "" { + metadata = append(metadata, newMetadata("git_branch", version.GitBranch)) } - // Match mimetype - for _, mimetype := range mimetype { - mimetype = strings.ToLower(strings.TrimSpace(mimetype)) - if slices.Contains(strings.Split(demuxer.Name(), ","), mimetype) { - return true - } - if strings.HasPrefix(mimetype, ".") { - ext := strings.TrimPrefix(mimetype, ".") - if slices.Contains(strings.Split(demuxer.Extensions(), ","), ext) { - return true - } - } - if slices.Contains(strings.Split(demuxer.MimeTypes(), ","), mimetype) { - return true - } + if version.GitTag != "" { + metadata = append(metadata, newMetadata("git_tag", version.GitTag)) } - // No match - return false -} - -func matchesOutput(muxer *ff.AVOutputFormat, media_type MediaType, filter ...string) bool { - // TODO: media_type - - // Match any - if len(filter) == 0 && media_type == ANY { - return true + if version.GoBuildTime != "" { + metadata = append(metadata, newMetadata("go_build_time", version.GoBuildTime)) } - // Match mimetype - for _, filter := range filter { - if filter == "" { - continue - } - filter = strings.ToLower(strings.TrimSpace(filter)) - if slices.Contains(strings.Split(muxer.Name(), ","), filter) { - return true - } - if strings.HasPrefix(filter, ".") { - if slices.Contains(strings.Split(muxer.Extensions(), ","), filter[1:]) { - return true - } - } - mt := strings.Split(muxer.MimeTypes(), ",") - if slices.Contains(mt, filter) { - return true - } + if runtime.Version() != "" { + metadata = append(metadata, newMetadata("go_version", runtime.Version())) + metadata = append(metadata, newMetadata("go_arch", runtime.GOOS+"/"+runtime.GOARCH)) } - // No match - return false + return metadata } diff --git a/manager_test.go b/manager_test.go index 990cc02..8feb4b7 100644 --- a/manager_test.go +++ b/manager_test.go @@ -2,9 +2,11 @@ package media_test import ( // Import namespaces + "os" "testing" // Package imports + "github.com/djthorpe/go-tablewriter" "github.com/stretchr/testify/assert" // Namespace imports @@ -17,7 +19,7 @@ func Test_manager_001(t *testing.T) { manager := NewManager() assert.NotNil(manager) - formats := manager.InputFormats() + formats := manager.InputFormats(ANY) assert.NotNil(formats) t.Log(formats) } @@ -28,7 +30,19 @@ func Test_manager_002(t *testing.T) { manager := NewManager() assert.NotNil(manager) - formats := manager.OutputFormats() + formats := manager.OutputFormats(ANY) assert.NotNil(formats) t.Log(formats) } + +func Test_manager_003(t *testing.T) { + assert := assert.New(t) + + manager := NewManager() + assert.NotNil(manager) + + version := manager.Version() + assert.NotNil(version) + + tablewriter.New(os.Stderr, tablewriter.OptHeader(), tablewriter.OptOutputText()).Write(version) +} diff --git a/metadata.go b/metadata.go new file mode 100644 index 0000000..9e75570 --- /dev/null +++ b/metadata.go @@ -0,0 +1,27 @@ +package media + +import "encoding/json" + +//////////////////////////////////////////////////////////////////////////////// +// TYPES + +type metadata struct { + Key string `json:"key"` + Value any `json:"value"` +} + +//////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// Return the metadata for the media stream +func newMetadata(key string, value any) Metadata { + return &metadata{key, value} +} + +//////////////////////////////////////////////////////////////////////////////// +// STRINIGY + +func (m *metadata) String() string { + data, _ := json.MarshalIndent(m, "", " ") + return string(data) +} diff --git a/pkg/version/version.go b/pkg/version/version.go index 0e74549..cc3e270 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -3,6 +3,6 @@ package version var ( GitSource string GitTag string - GetBranch string + GitBranch string GoBuildTime string ) diff --git a/reader.go b/reader.go index 06bb3bf..809654e 100644 --- a/reader.go +++ b/reader.go @@ -5,7 +5,6 @@ import ( "errors" "io" "strings" - "syscall" // Packages ff "github.com/mutablelogic/go-media/sys/ffmpeg61" @@ -15,10 +14,10 @@ import ( // TYPES type reader struct { - input *ff.AVFormatContext - avio *ff.AVIOContextEx - decoders map[int]*decoder - frame *ff.AVFrame + t MediaType + input *ff.AVFormatContext + avio *ff.AVIOContextEx + demuxer *demuxer } type reader_callback struct { @@ -37,14 +36,15 @@ const ( //////////////////////////////////////////////////////////////////////////////// // LIFECYCLE -// Open a reader from a url, file path or device -func Open(url string, format Format, opts ...string) (*reader, error) { +// Open media from a url, file path or device +func newMedia(url string, format Format, opts ...string) (*reader, error) { reader := new(reader) - reader.decoders = make(map[int]*decoder) + reader.t = INPUT // Set the input format var f *ff.AVInputFormat if format != nil { + reader.t |= format.Type() if inputfmt, ok := format.(*inputformat); ok { f = inputfmt.ctx } @@ -71,13 +71,14 @@ func Open(url string, format Format, opts ...string) (*reader, error) { } // Create a new reader from an io.Reader -func NewReader(r io.Reader, format Format, opts ...string) (*reader, error) { +func newReader(r io.Reader, format Format, opts ...string) (*reader, error) { reader := new(reader) - reader.decoders = make(map[int]*decoder) + reader.t = INPUT | FILE // Set the input format var fmt *ff.AVInputFormat if format != nil { + reader.t |= format.Type() if format.Type().Is(DEVICE) { return nil, errors.New("cannot create a reader from a device") } @@ -121,39 +122,32 @@ func (r *reader) open() (*reader, error) { return nil, err } - // Create a frame for decoding - if frame := ff.AVUtil_frame_alloc(); frame == nil { - ff.AVFormat_free_context(r.input) - ff.AVFormat_avio_context_free(r.avio) - return nil, errors.New("failed to allocate frame") - } else { - r.frame = frame - } - // Return success return r, nil } // Close the reader func (r *reader) Close() error { - // Free resources - for _, decoder := range r.decoders { - decoder.Close() + var result error + + // Free demuxer + if r.demuxer != nil { + result = errors.Join(result, r.demuxer.close()) } - ff.AVUtil_frame_free(r.frame) + + // Free resources ff.AVFormat_free_context(r.input) if r.avio != nil { ff.AVFormat_avio_context_free(r.avio) } // Release resources - r.decoders = nil - r.frame = nil + r.demuxer = nil r.input = nil r.avio = nil - // Return success - return nil + // Return any errors + return result } //////////////////////////////////////////////////////////////////////////////// @@ -164,105 +158,40 @@ func (r *reader) MarshalJSON() ([]byte, error) { return json.Marshal(r.input) } +// Display the reader as a string +func (r *reader) String() string { + data, _ := json.MarshalIndent(r, "", " ") + return string(data) +} + //////////////////////////////////////////////////////////////////////////////// // METHODS -// TODO: Frame should be a struct to access plane data and other properties -// TODO: Frame output may not include pts and time_base +func (r *reader) Type() MediaType { + return r.t +} -// Demultiplex streams from the reader -func (r *reader) Demux(fn DecoderFunc) error { - // Allocate a packet - packet := ff.AVCodec_packet_alloc() - if packet == nil { - return errors.New("failed to allocate packet") - } - defer ff.AVCodec_packet_free(packet) - - // Read packets - for { - if err := ff.AVFormat_read_frame(r.input, packet); errors.Is(err, io.EOF) { - break - } else if err != nil { - return err - } - stream := packet.StreamIndex() - if decoder := r.decoders[stream]; decoder != nil { - if err := fn(decoder, packet); errors.Is(err, io.EOF) { - break - } else if err != nil { - return err - } - } - // Unreference the packet - ff.AVCodec_packet_unref(packet) +func (r *reader) Decoder(fn DecoderMapFunc) (Decoder, error) { + // Check if this is actually an input + if !r.Type().Is(INPUT) { + return nil, errors.New("not an input stream") } - // Flush the decoders - for _, decoder := range r.decoders { - if err := fn(decoder, nil); err != nil { - return err - } + // Return existing decoder + if r.demuxer != nil { + return r.demuxer, nil } - // Return success - return nil -} - -// Return a function to decode packets from the streams into frames -func (r *reader) Decode(fn FrameFunc) DecoderFunc { - return func(codec Decoder, packet Packet) error { - if packet != nil { - // Submit the packet to the decoder - if err := ff.AVCodec_send_packet(codec.(*decoder).codec, packet.(*ff.AVPacket)); err != nil { - return err - } - } else { - // Flush remaining frames - if err := ff.AVCodec_send_packet(codec.(*decoder).codec, nil); err != nil { - return err - } - } - - // get all the available frames from the decoder - for { - if err := ff.AVCodec_receive_frame(codec.(*decoder).codec, r.frame); errors.Is(err, syscall.EAGAIN) || errors.Is(err, io.EOF) { - // Finished decoding packet or EOF - break - } else if err != nil { - return err - } - - // Resample or resize the frame, then pass back - if frame, err := codec.(*decoder).re(r.frame); err != nil { - return err - } else if err := fn(frame); errors.Is(err, io.EOF) { - // End early - break - } else if err != nil { - return err - } - } - - // Flush - if frame, err := codec.(*decoder).re(nil); err != nil { - return err - } else if frame == nil { - // NOOP - } else if err := fn(frame); errors.Is(err, io.EOF) { - // NOOP - } else if err != nil { - return err - } - - // Success - return nil + // Create a decoding context + decoder, err := newDemuxer(r.input, fn) + if err != nil { + return nil, err + } else { + r.demuxer = decoder } -} -type metadata struct { - Key string `json:"key"` - Value string `json:"value"` + // Return success + return decoder, nil } // Return the metadata for the media stream @@ -270,10 +199,7 @@ func (r *reader) Metadata() []Metadata { entries := ff.AVUtil_dict_entries(r.input.Metadata()) result := make([]Metadata, len(entries)) for i, entry := range entries { - result[i] = &metadata{ - Key: entry.Key(), - Value: entry.Value(), - } + result[i] = newMetadata(entry.Key(), entry.Value()) } return result } From 7422ce64f36aa8e035a4db87f5e898e9a7135abb Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Thu, 20 Jun 2024 11:04:32 +0200 Subject: [PATCH 08/16] Updated documentation --- README.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 748efb6..ced1f12 100644 --- a/README.md +++ b/README.md @@ -46,9 +46,73 @@ DOCKER_REGISTRY=ghcr.io/mutablelogic make docker ## Examples -## Media Transcoding +There are a variety of types of object needed as part of media processing. +All examples require a `Manager` to be created, which is used to enumerate all supported formats +and open media files and byte streams. + +* `Manager` is the main entry point for the package. It is used to open media files and byte streams, + and enumerate supported formats, codecs, pixel formats, etc. +* `Media` is a hardware device, file or byte stream. It contains metadata, artwork, and streams. +* `Decoder` is used to demultiplex media streams. Create a decoder and enumerate the streams which + you'd like to demultiplex. Provide the audio and video parameters if you want to resample or + reformat the streams. +* `Encoder` is used to multiplex media streams. Create an encoder and send the output of the + decoder to reencode the streams. + +### Demultiplexing + +```go +import ( + media "github.com/mutablelogic/go-media" +) + +func main() { + manager := media NewManager() + file, err := manager.Open(os.Args[1], nil) + if err != nil { + log.Fatal(err) + } + defer file.Close() + + // Choose which streams to demultiplex - pass the stream parameters + // to the decoder. If you don't want to resample or reformat the streams, + // then you can pass nil as the function and all streams will be demultiplexed. + decoder, err := file.Decoder(func (stream media.Stream) (Parameters, error) { + return stream.Parameters(), nil + } + if err != nil { + log.Fatal(err) + } + + // Demuliplex the stream and receive the packets. If you don't want to + // process the packets yourself, then you can pass nil as the function + if err := decoder.Demux(context.Background(), func(Packet) error { + // TODO: Each packet is specific to a stream. It can be processed here + // to receive audio or video frames, etc. + return nil + }); err != nil { + log.Fatal(err) + }) +} +``` + +### Decoding + +TODO + +### Encoding + +TODO + +### Multiplexing + +TODO + +### Retrieving Metadata and Artwork from a media file + +TODO -## Audio Fingerprinting +### Audio Fingerprinting You can programmatically fingerprint audio files, compare fingerprints and identify music using the following packages: From 6c4dfba7ca7c8feafbacdc16fd859b481ae7b6bf Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Thu, 20 Jun 2024 11:08:26 +0200 Subject: [PATCH 09/16] Updated --- decoder.go | 3 +-- interfaces.go | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/decoder.go b/decoder.go index d021810..7a0fce6 100644 --- a/decoder.go +++ b/decoder.go @@ -1,13 +1,12 @@ package media import ( - // Packages - "context" "errors" "fmt" "io" + // Packages ff "github.com/mutablelogic/go-media/sys/ffmpeg61" ) diff --git a/interfaces.go b/interfaces.go index ec1c919..9c17425 100644 --- a/interfaces.go +++ b/interfaces.go @@ -134,14 +134,14 @@ type Media interface { Metadata() []Metadata } -// Return encoding parameters if a the stream should be decoded -// and either resampled or resized -type DecoderMapFunc func(Stream) Parameters +// Return parameters if a the stream should be decoded +// and either resampled or resized. Return nil if you +// don't want to resample or resize the stream. +type DecoderMapFunc func(Stream) (Parameters, error) // Decoder represents a decoder for a media stream. type Decoder interface { // Demultiplex media into packets. Pass a packet to a decoder function. - // TODO // Stop when the context is cancelled or the end of the media stream is // reached. Demux(context.Context, DecoderFunc) error From af583605477b7b1ae6e0a4730d9647326b800cd5 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Thu, 20 Jun 2024 11:13:46 +0200 Subject: [PATCH 10/16] Updated the documentation --- README.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ced1f12..c68fabc 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,11 @@ import ( ) func main() { - manager := media NewManager() + manager := media.NewManager() + + // Open a media file for reading. The format of the file is guessed. + // Alteratively, you can pass a format as the second argument. Further optional + // arguments can be used to set the format options. file, err := manager.Open(os.Args[1], nil) if err != nil { log.Fatal(err) @@ -77,7 +81,7 @@ func main() { // Choose which streams to demultiplex - pass the stream parameters // to the decoder. If you don't want to resample or reformat the streams, // then you can pass nil as the function and all streams will be demultiplexed. - decoder, err := file.Decoder(func (stream media.Stream) (Parameters, error) { + decoder, err := file.Decoder(func (stream media.Stream) (media.Parameters, error) { return stream.Parameters(), nil } if err != nil { @@ -86,9 +90,11 @@ func main() { // Demuliplex the stream and receive the packets. If you don't want to // process the packets yourself, then you can pass nil as the function - if err := decoder.Demux(context.Background(), func(Packet) error { - // TODO: Each packet is specific to a stream. It can be processed here - // to receive audio or video frames, etc. + if err := decoder.Demux(context.Background(), func(_ media.Packet) error { + // Each packet is specific to a stream. It can be processed here + // to receive audio or video frames, then resize or resample them, + // for example. Alternatively, you can pass the packet to an encoder + // to remultiplex the streams without processing them. return nil }); err != nil { log.Fatal(err) From 42f791d6f32a3b303c208164475fb4eee1f9e2f7 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Thu, 20 Jun 2024 11:30:32 +0200 Subject: [PATCH 11/16] Updated interfaces --- interfaces.go | 53 +++++++++++++++++++++------------- sys/chromaprint/chromaprint.go | 11 +++---- 2 files changed, 39 insertions(+), 25 deletions(-) diff --git a/interfaces.go b/interfaces.go index 9c17425..45e735c 100644 --- a/interfaces.go +++ b/interfaces.go @@ -136,22 +136,34 @@ type Media interface { // Return parameters if a the stream should be decoded // and either resampled or resized. Return nil if you -// don't want to resample or resize the stream. +// want to ignore the stream, or pass identical stream +// parameters (stream.Parameters()) if you want to copy +// the stream without any changes. type DecoderMapFunc func(Stream) (Parameters, error) -// Decoder represents a decoder for a media stream. +// Stream represents a audio, video, subtitle or data stream +// within a media file +type Stream interface { + // Return AUDIO, VIDEO, SUBTITLE or DATA + Type() MediaType + + // Return the stream parameters + Parameters() Parameters +} + +// Decoder represents a demuliplexer and decoder for a media stream. +// You can call either Demux or Decode to process the media stream, +// but not both. type Decoder interface { // Demultiplex media into packets. Pass a packet to a decoder function. // Stop when the context is cancelled or the end of the media stream is // reached. Demux(context.Context, DecoderFunc) error - /* - // Return a decode function, which can rescale or - // resample a frame and then call a frame processing - // function for encoding and multiplexing. - Decode(FrameFunc) DecoderFunc - */ + // Decode media into frames, and resample or resize the frame. + // Stop when the context is cancelled or the end of the media stream is + // reached. + Decode(context.Context, FrameFunc) error } // Parameters represents a set of parameters for encoding @@ -173,6 +185,9 @@ type AudioParameters interface { // Return the sample rate (Hz) SampleRate() int + + // TODO: + // Planar, number of planes, bits and bytes per sample } // Video parameters for encoding or decoding video data. @@ -188,31 +203,29 @@ type VideoParameters interface { // Return the frame rate (fps) FrameRate() int + + // TODO: + // Planar, number of planes, names of the planes, bits and bytes per pixel } -// DecoderFunc is a function that decodes a packet +// DecoderFunc is a function that decodes a packet. Return +// io.EOF if you want to stop processing the packets early. type DecoderFunc func(Packet) error // FrameFunc is a function that processes a frame of audio -// or video data. +// or video data. Return io.EOF if you want to stop +// processing the frames early. type FrameFunc func(Frame) error // Packet represents a packet of demultiplexed data. // Currently this is quite opaque! type Packet interface{} -// Stream represents a audio, video, subtitle or data stream -// within a media file -type Stream interface { - // Return AUDIO, VIDEO, SUBTITLE or DATA - Type() MediaType - - // Return the stream parameters - Parameters() Parameters -} - // Frame represents a frame of audio or picture data. +// Currently this is quite opaque - should allow access to +// the audio sample data, or the individual pixel data! type Frame interface{} // Metadata represents a metadata entry for a media stream. +// Currently this is quite opaque! type Metadata interface{} diff --git a/sys/chromaprint/chromaprint.go b/sys/chromaprint/chromaprint.go index 85f4906..f9ff3df 100644 --- a/sys/chromaprint/chromaprint.go +++ b/sys/chromaprint/chromaprint.go @@ -1,5 +1,11 @@ package chromaprint +import ( + "fmt" + "time" + "unsafe" +) + //////////////////////////////////////////////////////////////////////////////// // CGO @@ -8,11 +14,6 @@ package chromaprint #include */ import "C" -import ( - "fmt" - "time" - "unsafe" -) //////////////////////////////////////////////////////////////////////////////// // TYPES From 3f773c3cac98aeed1734e278b37cba88a156f4da Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 21 Jun 2024 11:03:49 +0200 Subject: [PATCH 12/16] Added enumeration of channel layouts, sample formats and pixel formats --- cmd/cli/formats.go | 33 ++++++++++++++ cmd/cli/main.go | 15 ++++--- cmd/cli/metadata.go | 3 +- decoder.go | 46 ++++++++++++++++---- decoder_test.go | 6 +-- interfaces.go | 52 +++++++++++----------- manager.go | 62 +++++++++++++++++++++++++++ manager_test.go | 36 ++++++++++++++++ stream.go | 43 +++++++++++++++++++ sys/ffmpeg61/avutil_pixfmt.go | 17 ++++++++ sys/ffmpeg61/avutil_pixfmt_test.go | 21 +++++++++ sys/ffmpeg61/avutil_samplefmt.go | 18 ++++++++ sys/ffmpeg61/avutil_samplefmt_test.go | 11 +++++ 13 files changed, 319 insertions(+), 44 deletions(-) create mode 100644 cmd/cli/formats.go create mode 100644 stream.go create mode 100644 sys/ffmpeg61/avutil_pixfmt_test.go diff --git a/cmd/cli/formats.go b/cmd/cli/formats.go new file mode 100644 index 0000000..bf1c1a1 --- /dev/null +++ b/cmd/cli/formats.go @@ -0,0 +1,33 @@ +package main + +import ( + "os" + + // Packages + "github.com/djthorpe/go-tablewriter" + "github.com/mutablelogic/go-media" +) + +type SampleFormatsCmd struct{} + +type ChannelLayoutsCmd struct{} + +type PixelFormatsCmd struct{} + +func (cmd *SampleFormatsCmd) Run(globals *Globals) error { + manager := media.NewManager() + writer := tablewriter.New(os.Stdout, tablewriter.OptHeader(), tablewriter.OptOutputText()) + return writer.Write(manager.SampleFormats()) +} + +func (cmd *ChannelLayoutsCmd) Run(globals *Globals) error { + manager := media.NewManager() + writer := tablewriter.New(os.Stdout, tablewriter.OptHeader(), tablewriter.OptOutputText()) + return writer.Write(manager.ChannelLayouts()) +} + +func (cmd *PixelFormatsCmd) Run(globals *Globals) error { + manager := media.NewManager() + writer := tablewriter.New(os.Stdout, tablewriter.OptHeader(), tablewriter.OptOutputText()) + return writer.Write(manager.PixelFormats()) +} diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 85b6b13..b9994da 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -14,12 +14,15 @@ type Globals struct { type CLI struct { Globals - Version VersionCmd `cmd:"version" help:"Print version information"` - Demuxers DemuxersCmd `cmd:"demuxers" help:"List media demultiplex (input) formats"` - Muxers MuxersCmd `cmd:"muxers" help:"List media multiplex (output) formats"` - Metadata MetadataCmd `cmd:"metadata" help:"Display media metadata information"` - Probe ProbeCmd `cmd:"probe" help:"Probe media file or device"` - Decode DecodeCmd `cmd:"decode" help:"Decode media"` + Version VersionCmd `cmd:"version" help:"Print version information"` + Demuxers DemuxersCmd `cmd:"demuxers" help:"List media demultiplex (input) formats"` + Muxers MuxersCmd `cmd:"muxers" help:"List media multiplex (output) formats"` + SampleFormats SampleFormatsCmd `cmd:"samplefmts" help:"List audio sample formats"` + ChannelLayouts ChannelLayoutsCmd `cmd:"channellayouts" help:"List audio channel layouts"` + PixelFormats PixelFormatsCmd `cmd:"pixelfmts" help:"List video pixel formats"` + Metadata MetadataCmd `cmd:"metadata" help:"Display media metadata information"` + Probe ProbeCmd `cmd:"probe" help:"Probe media file or device"` + Decode DecodeCmd `cmd:"decode" help:"Decode media"` } func main() { diff --git a/cmd/cli/metadata.go b/cmd/cli/metadata.go index a8c319a..0dedb63 100644 --- a/cmd/cli/metadata.go +++ b/cmd/cli/metadata.go @@ -14,7 +14,8 @@ type MetadataCmd struct { } func (cmd *MetadataCmd) Run(globals *Globals) error { - reader, err := media.Open(cmd.Path, nil) + manager := media.NewManager() + reader, err := manager.Open(cmd.Path, nil) if err != nil { return err } diff --git a/decoder.go b/decoder.go index 7a0fce6..a4e2aa2 100644 --- a/decoder.go +++ b/decoder.go @@ -40,20 +40,40 @@ func newDemuxer(input *ff.AVFormatContext, mapfn DecoderMapFunc) (*demuxer, erro // Get all the streams streams := input.Streams() + // Use standard map function if none provided + if mapfn == nil { + mapfn = func(stream Stream) (Parameters, error) { + return stream.Parameters(), nil + } + } + // Create a decoder for each stream // The decoder map function should be returning the parameters for the // destination frame. If it's nil then it's mostly a copy. + var result error for _, stream := range streams { - if mapfn == nil || mapfn(stream) { - if decoder, err := demuxer.newDecoder(stream); err != nil { - return nil, errors.Join(err, demuxer.close()) - } else { - streamNum := stream.Index() - demuxer.decoders[streamNum] = decoder - } + // Get decoder parameters + parameters, err := mapfn(newStream(stream)) + if err != nil { + result = errors.Join(result, err) + } else if parameters == nil { + continue + } + + // Create the decoder with the parameters + if decoder, err := demuxer.newDecoder(stream, parameters); err != nil { + result = errors.Join(result, err) + } else { + streamNum := stream.Index() + demuxer.decoders[streamNum] = decoder } } + // Return any errors + if result != nil { + return nil, errors.Join(result, demuxer.close()) + } + // Create a frame for encoding - after resampling and resizing if frame := ff.AVUtil_frame_alloc(); frame == nil { return nil, errors.Join(demuxer.close(), errors.New("failed to allocate frame")) @@ -65,10 +85,12 @@ func newDemuxer(input *ff.AVFormatContext, mapfn DecoderMapFunc) (*demuxer, erro return demuxer, nil } -func (d *demuxer) newDecoder(stream *ff.AVStream) (*decoder, error) { +func (d *demuxer) newDecoder(stream *ff.AVStream, parameters Parameters) (*decoder, error) { decoder := new(decoder) decoder.stream = stream.Id() + // TODO: Use parameters to create the decoder + // Create a codec context for the decoder codec := ff.AVCodec_find_decoder(stream.CodecPar().CodecID()) if codec == nil { @@ -192,6 +214,14 @@ FOR_LOOP: return ctx.Err() } +func (d *demuxer) Decode(context.Context, FrameFunc) error { + // TODO + return errors.New("not implemented") +} + +//////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + func (d *decoder) decode(fn DecoderFunc, packet *ff.AVPacket) error { // Send the packet to the user defined packet function or // to the default version diff --git a/decoder_test.go b/decoder_test.go index fb03a99..672454d 100644 --- a/decoder_test.go +++ b/decoder_test.go @@ -22,10 +22,10 @@ func Test_decoder_001(t *testing.T) { } defer media.Close() - decoder, err := media.Decoder(func (stream Stream) Parameters { + decoder, err := media.Decoder(func(stream Stream) (Parameters, error) { // Copy parameters from the stream - return stream.Parameters() - } + return stream.Parameters(), nil + }) if !assert.NoError(err) { t.SkipNow() } diff --git a/interfaces.go b/interfaces.go index 45e735c..9bd7e18 100644 --- a/interfaces.go +++ b/interfaces.go @@ -13,26 +13,6 @@ import ( // Manager represents a manager for media formats and devices. // Create a new manager object using the NewManager function. type Manager interface { - // Return supported input formats which match any filter, which can be - // a name, extension (with preceeding period) or mimetype. The MediaType - // can be NONE (for any) or combinations of DEVICE and STREAM. - InputFormats(MediaType, ...string) []Format - - // Return supported output formats which match any filter, which can be - // a name, extension (with preceeding period) or mimetype. The MediaType - // can be NONE (for any) or combinations of DEVICE and STREAM. - OutputFormats(MediaType, ...string) []Format - - // Return supported input devices for a given format name - // Not all devices may be supported on all platforms or listed - // if the device does not support enumeration. - InputDevices(string) []Device - - // Return supported output devices for a given format name - // Not all devices may be supported on all platforms or listed - // if the device does not support enumeration. - OutputDevices(string) []Device - // Open a media file or device for reading, from a path or url. // If a format is specified, then the format will be used to open // the file. Close the media object when done. @@ -57,9 +37,25 @@ type Manager interface { // TODO Write(io.Writer, Format) (Media, error) - // Return version information for the media manager as a set of - // metadata - Version() []Metadata + // Return supported input formats which match any filter, which can be + // a name, extension (with preceeding period) or mimetype. The MediaType + // can be NONE (for any) or combinations of DEVICE and STREAM. + InputFormats(MediaType, ...string) []Format + + // Return supported output formats which match any filter, which can be + // a name, extension (with preceeding period) or mimetype. The MediaType + // can be NONE (for any) or combinations of DEVICE and STREAM. + OutputFormats(MediaType, ...string) []Format + + // Return supported input devices for a given format name + // Not all devices may be supported on all platforms or listed + // if the device does not support enumeration. + InputDevices(string) []Device + + // Return supported output devices for a given format name + // Not all devices may be supported on all platforms or listed + // if the device does not support enumeration. + OutputDevices(string) []Device // Return all supported channel layouts ChannelLayouts() []Metadata @@ -71,12 +67,16 @@ type Manager interface { PixelFormats() []Metadata // Return audio parameters for encoding - // ChannelLayout, SampleFormat, SampleRate + // ChannelLayout, SampleFormat, Samplerate AudioParameters(string, string, int) (AudioParameters, error) // Return video parameters for encoding - // Width, Height, PixelFormat, FrameRate - VideoParameters(int, int, string, int) (VideoParameters, error) + // Width, Height, PixelFormat, Framerate + VideoParameters(int, int, string, float32) (VideoParameters, error) + + // Return version information for the media manager as a set of + // metadata + Version() []Metadata } // Device represents a device for input or output of media streams. diff --git a/manager.go b/manager.go index 6cac2b1..48e23a8 100644 --- a/manager.go +++ b/manager.go @@ -163,6 +163,68 @@ func (manager *manager) OutputDevices(format string) []Device { panic("TODO") } +// Return all supported channel layouts +func (manager *manager) ChannelLayouts() []Metadata { + var result []Metadata + var iter uintptr + for { + ch := ff.AVUtil_channel_layout_standard(&iter) + if ch == nil { + break + } + if name, err := ff.AVUtil_channel_layout_describe(ch); err != nil { + continue + } else { + result = append(result, newMetadata(name, ch)) + } + } + return result +} + +// Return all supported sample formats +func (manager *manager) SampleFormats() []Metadata { + var result []Metadata + var opaque uintptr + for { + samplefmt := ff.AVUtil_next_sample_fmt(&opaque) + if samplefmt == ff.AV_SAMPLE_FMT_NONE { + break + } + if name := ff.AVUtil_get_sample_fmt_name(samplefmt); name != "" { + result = append(result, newMetadata(name, samplefmt)) + } + } + return result +} + +// Return all supported pixel formats +func (manager *manager) PixelFormats() []Metadata { + var result []Metadata + var opaque uintptr + for { + pixfmt := ff.AVUtil_next_pixel_fmt(&opaque) + if pixfmt == ff.AV_PIX_FMT_NONE { + break + } + if name := ff.AVUtil_get_pix_fmt_name(pixfmt); name != "" { + result = append(result, newMetadata(name, pixfmt)) + } + } + return result +} + +// Return audio parameters for encoding +// ChannelLayout, SampleFormat, Samplerate +func (manager *manager) AudioParameters(string, string, int) (AudioParameters, error) { + return nil, ErrNotImplemented +} + +// Return video parameters for encoding +// Width, Height, PixelFormat, Framerate +func (manager *manager) VideoParameters(int, int, string, float32) (VideoParameters, error) { + return nil, ErrNotImplemented +} + // Open a media file or device for reading, from a path or url. func (manager *manager) Open(url string, format Format, opts ...string) (Media, error) { return newMedia(url, format, opts...) diff --git a/manager_test.go b/manager_test.go index 8feb4b7..4ade8a1 100644 --- a/manager_test.go +++ b/manager_test.go @@ -46,3 +46,39 @@ func Test_manager_003(t *testing.T) { tablewriter.New(os.Stderr, tablewriter.OptHeader(), tablewriter.OptOutputText()).Write(version) } + +func Test_manager_004(t *testing.T) { + assert := assert.New(t) + + manager := NewManager() + assert.NotNil(manager) + + channel_layouts := manager.ChannelLayouts() + assert.NotNil(channel_layouts) + + tablewriter.New(os.Stderr, tablewriter.OptHeader(), tablewriter.OptOutputText()).Write(channel_layouts) +} + +func Test_manager_005(t *testing.T) { + assert := assert.New(t) + + manager := NewManager() + assert.NotNil(manager) + + sample_formats := manager.SampleFormats() + assert.NotNil(sample_formats) + + tablewriter.New(os.Stderr, tablewriter.OptHeader(), tablewriter.OptOutputText()).Write(sample_formats) +} + +func Test_manager_006(t *testing.T) { + assert := assert.New(t) + + manager := NewManager() + assert.NotNil(manager) + + pixel_formats := manager.PixelFormats() + assert.NotNil(pixel_formats) + + tablewriter.New(os.Stderr, tablewriter.OptHeader(), tablewriter.OptOutputText()).Write(pixel_formats) +} diff --git a/stream.go b/stream.go new file mode 100644 index 0000000..caf2c48 --- /dev/null +++ b/stream.go @@ -0,0 +1,43 @@ +package media + +import ( + // Packages + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +//////////////////////////////////////////////////////////////////////////////// +// TYPES + +type stream struct { + *ff.AVStream +} + +var _ Stream = (*stream)(nil) + +//////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// Open media from a url, file path or device +func newStream(ctx *ff.AVStream) *stream { + return &stream{ctx} +} + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +func (stream *stream) Type() MediaType { + switch stream.CodecPar().CodecType() { + case ff.AVMEDIA_TYPE_AUDIO: + return AUDIO + case ff.AVMEDIA_TYPE_VIDEO: + return VIDEO + case ff.AVMEDIA_TYPE_SUBTITLE: + return SUBTITLE + } + return DATA +} + +func (stream *stream) Parameters() Parameters { + // TODO + return nil +} diff --git a/sys/ffmpeg61/avutil_pixfmt.go b/sys/ffmpeg61/avutil_pixfmt.go index 30853a9..f55d670 100644 --- a/sys/ffmpeg61/avutil_pixfmt.go +++ b/sys/ffmpeg61/avutil_pixfmt.go @@ -263,6 +263,23 @@ func (v AVPixelFormat) String() string { //////////////////////////////////////////////////////////////////////////////// // PUBLIC FUNCTIONS +// Enumerate pixel formats +func AVUtil_next_pixel_fmt(iterator *uintptr) AVPixelFormat { + if iterator == nil { + return AV_PIX_FMT_NONE + } + + // Increment the iterator + *iterator += 1 + + desc := AVUtil_get_pix_fmt_desc(AVPixelFormat(*iterator)) + if desc == nil { + return AV_PIX_FMT_NONE + } else { + return AVPixelFormat(*iterator) + } +} + func AVUtil_get_pix_fmt_name(pixfmt AVPixelFormat) string { return C.GoString(C.av_get_pix_fmt_name((C.enum_AVPixelFormat)(pixfmt))) } diff --git a/sys/ffmpeg61/avutil_pixfmt_test.go b/sys/ffmpeg61/avutil_pixfmt_test.go new file mode 100644 index 0000000..fa9caf3 --- /dev/null +++ b/sys/ffmpeg61/avutil_pixfmt_test.go @@ -0,0 +1,21 @@ +package ffmpeg_test + +import ( + "testing" + + // Packages + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func Test_avutil_pixfmt_002(t *testing.T) { + var opaque uintptr + for { + fmt := AVUtil_next_pixel_fmt(&opaque) + if fmt == AV_PIX_FMT_NONE { + break + } + t.Logf("pixel_fmt[%d]=%v", fmt, AVUtil_get_pix_fmt_name(fmt)) + } +} diff --git a/sys/ffmpeg61/avutil_samplefmt.go b/sys/ffmpeg61/avutil_samplefmt.go index 86e44f7..015245c 100644 --- a/sys/ffmpeg61/avutil_samplefmt.go +++ b/sys/ffmpeg61/avutil_samplefmt.go @@ -49,6 +49,24 @@ func (v AVSampleFormat) MarshalJSON() ([]byte, error) { //////////////////////////////////////////////////////////////////////////////// // PUBLIC FUNCTIONS +// Enumerate sample formats +func AVUtil_next_sample_fmt(iterator *uintptr) AVSampleFormat { + if iterator == nil { + return AV_SAMPLE_FMT_NONE + } + + // Increment the iterator + *iterator += 1 + + // Check for end of enumeration + if AVSampleFormat(*iterator) == AV_SAMPLE_FMT_NB { + return AV_SAMPLE_FMT_NONE + } + + // Return the sample format + return AVSampleFormat(*iterator) +} + // Return the name of sample_fmt, or empty string if sample_fmt is not recognized func AVUtil_get_sample_fmt_name(sample_fmt AVSampleFormat) string { return C.GoString(C.av_get_sample_fmt_name(C.enum_AVSampleFormat(sample_fmt))) diff --git a/sys/ffmpeg61/avutil_samplefmt_test.go b/sys/ffmpeg61/avutil_samplefmt_test.go index fdb328a..7cfeef8 100644 --- a/sys/ffmpeg61/avutil_samplefmt_test.go +++ b/sys/ffmpeg61/avutil_samplefmt_test.go @@ -32,3 +32,14 @@ func Test_avutil_samplefmt_001(t *testing.T) { t.Logf(" bytes_per_sample=%v", AVUtil_get_bytes_per_sample(fmt)) } } + +func Test_avutil_samplefmt_002(t *testing.T) { + var opaque uintptr + for { + fmt := AVUtil_next_sample_fmt(&opaque) + if fmt == AV_SAMPLE_FMT_NONE { + break + } + t.Logf("sample_fmt[%d]=%v", fmt, AVUtil_get_sample_fmt_name(fmt)) + } +} From 5ca580ff41e992179a209279f8972fa2af7d8daa Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 21 Jun 2024 11:50:18 +0200 Subject: [PATCH 13/16] Added parameters --- interfaces.go | 6 +- manager.go | 8 +- parameters.go | 186 ++++++++++++++++++++++++++++++++ parameters_test.go | 35 ++++++ sys/ffmpeg61/avutil_pixfmt.go | 9 ++ sys/ffmpeg61/avutil_rational.go | 10 ++ 6 files changed, 247 insertions(+), 7 deletions(-) create mode 100644 parameters.go create mode 100644 parameters_test.go diff --git a/interfaces.go b/interfaces.go index 9bd7e18..3151f08 100644 --- a/interfaces.go +++ b/interfaces.go @@ -72,7 +72,7 @@ type Manager interface { // Return video parameters for encoding // Width, Height, PixelFormat, Framerate - VideoParameters(int, int, string, float32) (VideoParameters, error) + VideoParameters(int, int, string, float64) (VideoParameters, error) // Return version information for the media manager as a set of // metadata @@ -184,7 +184,7 @@ type AudioParameters interface { SampleFormat() string // Return the sample rate (Hz) - SampleRate() int + Samplerate() int // TODO: // Planar, number of planes, bits and bytes per sample @@ -202,7 +202,7 @@ type VideoParameters interface { PixelFormat() string // Return the frame rate (fps) - FrameRate() int + Framerate() float64 // TODO: // Planar, number of planes, names of the planes, bits and bytes per pixel diff --git a/manager.go b/manager.go index 48e23a8..220a249 100644 --- a/manager.go +++ b/manager.go @@ -215,14 +215,14 @@ func (manager *manager) PixelFormats() []Metadata { // Return audio parameters for encoding // ChannelLayout, SampleFormat, Samplerate -func (manager *manager) AudioParameters(string, string, int) (AudioParameters, error) { - return nil, ErrNotImplemented +func (manager *manager) AudioParameters(channels string, samplefmt string, samplerate int) (AudioParameters, error) { + return newAudioParametersEx(channels, samplefmt, samplerate) } // Return video parameters for encoding // Width, Height, PixelFormat, Framerate -func (manager *manager) VideoParameters(int, int, string, float32) (VideoParameters, error) { - return nil, ErrNotImplemented +func (manager *manager) VideoParameters(width int, height int, pixelfmt string, framerate float64) (VideoParameters, error) { + return newVideoParametersEx(width, height, pixelfmt, framerate) } // Open a media file or device for reading, from a path or url. diff --git a/parameters.go b/parameters.go new file mode 100644 index 0000000..ab420b0 --- /dev/null +++ b/parameters.go @@ -0,0 +1,186 @@ +package media + +import ( + "encoding/json" + "math" + + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" + + // Namespace imports + . "github.com/djthorpe/go-errors" +) + +//////////////////////////////////////////////////////////////////////////////// +// TYPES + +type par struct { + t MediaType + audiopar + videopar +} + +type audiopar struct { + Ch ff.AVChannelLayout + SampleFormat ff.AVSampleFormat + Samplerate int +} + +type videopar struct { + PixelFormat ff.AVPixelFormat + Width int + Height int + Framerate ff.AVRational +} + +var _ Parameters = (*par)(nil) + +//////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// Create new parameters for audio sampling from number of channels, sample format and sample rate in Hz +func newAudioParameters(numchannels int, samplefmt string, samplerate int) (*par, error) { + // Get channel layout from number of channels + var ch ff.AVChannelLayout + ff.AVUtil_channel_layout_default(&ch, numchannels) + if name, err := ff.AVUtil_channel_layout_describe(&ch); err != nil { + return nil, err + } else { + return newAudioParametersEx(name, samplefmt, samplerate) + } +} + +// Create new parameters for audio sampling from a channel layout name, sample format and sample rate in Hz +func newAudioParametersEx(channels string, samplefmt string, samplerate int) (*par, error) { + par := new(par) + par.t = AUDIO + + // Set the parameters + if err := ff.AVUtil_channel_layout_from_string(&par.audiopar.Ch, channels); err != nil { + return nil, err + } + if fmt := ff.AVUtil_get_sample_fmt(samplefmt); fmt == ff.AV_SAMPLE_FMT_NONE { + return nil, ErrBadParameter.Withf("sample format %q", samplefmt) + } else { + par.audiopar.SampleFormat = fmt + } + if samplerate <= 0 { + return nil, ErrBadParameter.Withf("samplerate %v", samplerate) + } else { + par.audiopar.Samplerate = samplerate + } + + // Return success + return par, nil +} + +// Create new parameters for video from a width, height, pixel format and framerate in fps +func newVideoParametersEx(width int, height int, pixelfmt string, framerate float64) (*par, error) { + par := new(par) + par.t = VIDEO + + // Set the parameters + if width <= 0 { + // Negative widths might mean "flip" but not tested yet + return nil, ErrBadParameter.Withf("width %v", width) + } else { + par.videopar.Width = width + } + if height <= 0 { + // Negative heights might mean "flip" but not tested yet + return nil, ErrBadParameter.Withf("height %v", height) + } else { + par.videopar.Height = height + } + if fmt := ff.AVUtil_get_pix_fmt(pixelfmt); fmt == ff.AV_PIX_FMT_NONE { + return nil, ErrBadParameter.Withf("pixel format %q", pixelfmt) + } else { + par.videopar.PixelFormat = fmt + } + if framerate <= 0 { + return nil, ErrBadParameter.Withf("framerate %v", framerate) + } else { + par.videopar.Framerate = ff.AVUtil_rational_d2q(1/framerate, 0) + } + + // Return success + return par, nil +} + +// Create new parameters for video from a frame size, pixel format and framerate in fps +func newVideoParameters(frame string, pixelfmt string, framerate float64) (*par, error) { + // Parse the frame size + w, h, err := ff.AVUtil_parse_video_size(frame) + if err != nil { + return nil, err + } + return newVideoParametersEx(w, h, pixelfmt, framerate) +} + +//////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (par *par) MarshalJSON() ([]byte, error) { + if par.t == AUDIO { + return json.Marshal(par.audiopar) + } else { + return json.Marshal(par.videopar) + } +} + +func (par *par) String() string { + data, _ := json.MarshalIndent(par, "", " ") + return string(data) +} + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Return type +func (par *par) Type() MediaType { + return par.t +} + +// Return the channel layout +func (par *par) ChannelLayout() string { + if name, err := ff.AVUtil_channel_layout_describe(&par.audiopar.Ch); err != nil { + return "" + } else { + return name + } +} + +// Return the sample format +func (par *par) SampleFormat() string { + return ff.AVUtil_get_sample_fmt_name(par.audiopar.SampleFormat) +} + +// Return the sample rate (Hz) +func (par *par) Samplerate() int { + return par.audiopar.Samplerate +} + +// Return the sample format + +// Return the width of the video frame +func (par *par) Width() int { + return par.videopar.Width +} + +// Return the height of the video frame +func (par *par) Height() int { + return par.videopar.Height +} + +// Return the pixel format +func (par *par) PixelFormat() string { + return ff.AVUtil_get_pix_fmt_name(par.videopar.PixelFormat) +} + +// Return the frame rate (fps) +func (par *par) Framerate() float64 { + if v := par.videopar.Framerate.Float(1); v == 0 { + return math.Inf(1) + } else { + return 1 / v + } +} diff --git a/parameters_test.go b/parameters_test.go new file mode 100644 index 0000000..33f085d --- /dev/null +++ b/parameters_test.go @@ -0,0 +1,35 @@ +package media + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_parameters_001(t *testing.T) { + assert := assert.New(t) + + for ch := 1; ch < 8; ch++ { + params, err := newAudioParameters(ch, "flt", 44100) + if !assert.NoError(err) { + t.FailNow() + } + assert.NotNil(params) + t.Log(params) + } + +} + +func Test_parameters_002(t *testing.T) { + assert := assert.New(t) + + for ch := 1; ch < 8; ch++ { + params, err := newVideoParameters("100x100", "rgba", 25) + if !assert.NoError(err) { + t.FailNow() + } + assert.NotNil(params) + t.Log(params) + } + +} diff --git a/sys/ffmpeg61/avutil_pixfmt.go b/sys/ffmpeg61/avutil_pixfmt.go index f55d670..af495b3 100644 --- a/sys/ffmpeg61/avutil_pixfmt.go +++ b/sys/ffmpeg61/avutil_pixfmt.go @@ -3,6 +3,7 @@ package ffmpeg import ( "encoding/json" "fmt" + "unsafe" ) //////////////////////////////////////////////////////////////////////////////// @@ -284,6 +285,14 @@ func AVUtil_get_pix_fmt_name(pixfmt AVPixelFormat) string { return C.GoString(C.av_get_pix_fmt_name((C.enum_AVPixelFormat)(pixfmt))) } +// Return the pixel format corresponding to name. +// If no pixel format has been found, returns AV_PIX_FMT_NONE +func AVUtil_get_pix_fmt(name string) AVPixelFormat { + cName := C.CString(name) + defer C.free(unsafe.Pointer(cName)) + return AVPixelFormat(C.av_get_pix_fmt(cName)) +} + func AVUtil_get_pix_fmt_desc(pixfmt AVPixelFormat) *AVPixFmtDescriptor { return (*AVPixFmtDescriptor)(C.av_pix_fmt_desc_get(C.enum_AVPixelFormat(pixfmt))) } diff --git a/sys/ffmpeg61/avutil_rational.go b/sys/ffmpeg61/avutil_rational.go index 212733e..7692d5a 100644 --- a/sys/ffmpeg61/avutil_rational.go +++ b/sys/ffmpeg61/avutil_rational.go @@ -56,3 +56,13 @@ func (r AVRational) IsZero() bool { func (r AVRational) Float(multiplier int64) float64 { return float64(int64(r.num)*multiplier) / float64(r.den) } + +//////////////////////////////////////////////////////////////////////////////// +// BINDINGS + +func AVUtil_rational_d2q(d float64, max int) AVRational { + if max == 0 { + max = C.INT_MAX + } + return AVRational(C.av_d2q(C.double(d), C.int(max))) +} From 137cf4b6578e524bc1980bdd824eca0599261f46 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 21 Jun 2024 12:07:14 +0200 Subject: [PATCH 14/16] Added packet type --- cmd/cli/decode.go | 27 +++++++++++++++++++++++---- decoder.go | 2 +- packet.go | 23 +++++++++++++++++++++++ stream.go | 2 +- 4 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 packet.go diff --git a/cmd/cli/decode.go b/cmd/cli/decode.go index d5c5192..999cd83 100644 --- a/cmd/cli/decode.go +++ b/cmd/cli/decode.go @@ -1,10 +1,12 @@ package main import ( - "encoding/json" + "context" "fmt" + "os" // Packages + "github.com/djthorpe/go-tablewriter" "github.com/mutablelogic/go-media" ) @@ -29,14 +31,31 @@ func (cmd *DecodeCmd) Run(globals *Globals) error { } } + // Open media file reader, err := manager.Open(cmd.Path, format) if err != nil { return err } defer reader.Close() - data, _ := json.MarshalIndent(reader, "", " ") - fmt.Println(string(data)) + // Create a decoder - copy streams + decoder, err := reader.Decoder(nil) + if err != nil { + return err + } - return nil + // Demultiplex the stream + header := []tablewriter.TableOpt{tablewriter.OptHeader()} + tablewriter := tablewriter.New(os.Stdout, tablewriter.OptOutputText()) + return decoder.Demux(context.Background(), func(packet media.Packet) error { + if packet == nil { + return nil + } + if err := tablewriter.Write(packet, header...); err != nil { + return err + } + // Reset the header + header = header[:0] + return nil + }) } diff --git a/decoder.go b/decoder.go index a4e2aa2..b30e81b 100644 --- a/decoder.go +++ b/decoder.go @@ -225,7 +225,7 @@ func (d *demuxer) Decode(context.Context, FrameFunc) error { func (d *decoder) decode(fn DecoderFunc, packet *ff.AVPacket) error { // Send the packet to the user defined packet function or // to the default version - return fn(packet) + return fn(newPacket(packet)) } /* diff --git a/packet.go b/packet.go new file mode 100644 index 0000000..037956b --- /dev/null +++ b/packet.go @@ -0,0 +1,23 @@ +package media + +import ( + // Packages + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +//////////////////////////////////////////////////////////////////////////////// +// TYPES + +type packet struct { + X string `json:"type"` + *ff.AVPacket +} + +var _ Packet = (*packet)(nil) + +//////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +func newPacket(ctx *ff.AVPacket) *packet { + return &packet{"X", ctx} +} diff --git a/stream.go b/stream.go index caf2c48..4dc3142 100644 --- a/stream.go +++ b/stream.go @@ -39,5 +39,5 @@ func (stream *stream) Type() MediaType { func (stream *stream) Parameters() Parameters { // TODO - return nil + return new(par) } From 5e99d78515ea2919ebce9a826436ab18f789ff58 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 21 Jun 2024 12:13:02 +0200 Subject: [PATCH 15/16] Updated makefile --- Makefile | 7 ++++++- cmd/cli/version.go | 31 ++++++------------------------- metadata.go | 4 ++-- 3 files changed, 14 insertions(+), 28 deletions(-) diff --git a/Makefile b/Makefile index 5f8183c..fb55c56 100755 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ BUILD_DIR := "build" CMD_DIR := $(filter-out cmd/ffmpeg/README.md, $(wildcard cmd/ffmpeg/*)) BUILD_TAG := ${DOCKER_REGISTRY}/go-media-${OS}-${ARCH}:${VERSION} -all: clean cmds +all: clean cli cmds cmds: $(CMD_DIR) @@ -47,6 +47,11 @@ test: go-dep @${GO} test ./pkg/... @${GO} test . + +cli: go-dep mkdir + @echo Build media tool + @${GO} build ${BUILD_FLAGS} -o ${BUILD_DIR}/media ./cmd/cli + $(CMD_DIR): go-dep mkdir @echo Build cmd $(notdir $@) @${GO} build ${BUILD_FLAGS} -o ${BUILD_DIR}/$(notdir $@) ./$@ diff --git a/cmd/cli/version.go b/cmd/cli/version.go index bdcf862..9433953 100644 --- a/cmd/cli/version.go +++ b/cmd/cli/version.go @@ -1,37 +1,18 @@ package main import ( - "fmt" "os" - "runtime" - "github.com/mutablelogic/go-client/pkg/version" + "github.com/djthorpe/go-tablewriter" + "github.com/mutablelogic/go-media" ) type VersionCmd struct{} func (v *VersionCmd) Run(globals *Globals) error { - w := os.Stdout - if version.GitSource != "" { - if version.GitTag != "" { - fmt.Fprintf(w, " %v", version.GitTag) - } - if version.GitSource != "" { - fmt.Fprintf(w, " (%v)", version.GitSource) - } - fmt.Fprintln(w, "") + opts := []tablewriter.TableOpt{ + tablewriter.OptOutputText(), + tablewriter.OptDelimiter(' '), } - if runtime.Version() != "" { - fmt.Fprintf(w, "%v %v/%v\n", runtime.Version(), runtime.GOOS, runtime.GOARCH) - } - if version.GitBranch != "" { - fmt.Fprintf(w, "Branch: %v\n", version.GitBranch) - } - if version.GitHash != "" { - fmt.Fprintf(w, "Hash: %v\n", version.GitHash) - } - if version.GoBuildTime != "" { - fmt.Fprintf(w, "BuildTime: %v\n", version.GoBuildTime) - } - return nil + return tablewriter.New(os.Stdout, opts...).Write(media.NewManager().Version()) } diff --git a/metadata.go b/metadata.go index 9e75570..ff5db69 100644 --- a/metadata.go +++ b/metadata.go @@ -6,8 +6,8 @@ import "encoding/json" // TYPES type metadata struct { - Key string `json:"key"` - Value any `json:"value"` + Key string `json:"key" writer:",width:30"` + Value any `json:"value" writer:",width:50"` } //////////////////////////////////////////////////////////////////////////////// From b38244721f373d587f0ac23dd18e8e99f352e05e Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 21 Jun 2024 12:14:17 +0200 Subject: [PATCH 16/16] Updated --- go.mod | 1 - manager.go | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 2c224a9..f3b5f32 100755 --- a/go.mod +++ b/go.mod @@ -9,7 +9,6 @@ require ( github.com/djthorpe/go-errors v1.0.3 github.com/djthorpe/go-tablewriter v0.0.8 github.com/hashicorp/go-multierror v1.1.1 - github.com/mutablelogic/go-client v1.0.8 github.com/stretchr/testify v1.9.0 ) diff --git a/manager.go b/manager.go index 220a249..41db824 100644 --- a/manager.go +++ b/manager.go @@ -249,7 +249,7 @@ func (manager *manager) Write(io.Writer, Format) (Media, error) { func (manager *manager) Version() []Metadata { metadata := []Metadata{ newMetadata("libavcodec_version", ff.AVCodec_version()), - newMetadata("libavformat_versionn", ff.AVFormat_version()), + newMetadata("libavformat_version", ff.AVFormat_version()), newMetadata("libavutil_version", ff.AVUtil_version()), newMetadata("libavdevice_version", ff.AVDevice_version()), // newMetadata("libavfilter_version", ff.AVFilter_version()),