From a7d79524b5f40fc0ddd5e5ffd4ec93e208338f58 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Mon, 17 Jun 2024 21:50:39 +0200 Subject: [PATCH 1/5] Updated --- etc/docker/Dockerfile | 4 ++-- sys/ffmpeg61/avcodec.go | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/etc/docker/Dockerfile b/etc/docker/Dockerfile index 37721c3..3cc7735 100644 --- a/etc/docker/Dockerfile +++ b/etc/docker/Dockerfile @@ -12,7 +12,7 @@ COPY . . RUN set -x \ && apt update -y \ && apt install -y ca-certificates lsb-release \ - && echo "deb https://www.deb-multimedia.org $(lsb_release -sc) main non-free" >> /etc/apt/sources.list \ + && echo "deb https://www.deb-multimedia.org $(lsb_release -sc) main" >> /etc/apt/sources.list \ && apt update -y -oAcquire::AllowInsecureRepositories=true \ && apt install -y --force-yes deb-multimedia-keyring \ && apt install -y --allow-unauthenticated libavcodec-dev libavdevice-dev libavfilter-dev libavutil-dev libswscale-dev libswresample-dev @@ -29,7 +29,7 @@ ARG SOURCE RUN set -x \ && apt update -y \ && apt install -y ca-certificates lsb-release \ - && echo "deb https://www.deb-multimedia.org $(lsb_release -sc) main non-free" >> /etc/apt/sources.list \ + && echo "deb https://www.deb-multimedia.org $(lsb_release -sc) main" >> /etc/apt/sources.list \ && apt update -y -oAcquire::AllowInsecureRepositories=true \ && apt install -y --force-yes deb-multimedia-keyring \ && apt install -y --allow-unauthenticated ffmpeg diff --git a/sys/ffmpeg61/avcodec.go b/sys/ffmpeg61/avcodec.go index 0792d62..d96e755 100644 --- a/sys/ffmpeg61/avcodec.go +++ b/sys/ffmpeg61/avcodec.go @@ -297,7 +297,6 @@ type jsonAVCodecParameters struct { SampleAspectRatio AVRational `json:"sample_aspect_ratio,omitempty"` SampleRate int `json:"sample_rate,omitempty"` FrameSize int `json:"frame_size,omitempty"` - Framerate AVRational `json:"framerate,omitempty"` } func (ctx *AVCodecParameters) MarshalJSON() ([]byte, error) { @@ -312,14 +311,9 @@ func (ctx *AVCodecParameters) MarshalJSON() ([]byte, error) { SampleAspectRatio: (AVRational)(ctx.sample_aspect_ratio), SampleRate: int(ctx.sample_rate), FrameSize: int(ctx.frame_size), - Framerate: (AVRational)(ctx.framerate), }) } -func (ctx *AVCodecParameters) Format() int { - return int(ctx.format) -} - func (ctx *AVCodecParameters) CodecType() AVMediaType { return AVMediaType(ctx.codec_type) } @@ -336,6 +330,10 @@ func (ctx *AVCodecParameters) SetCodecTag(tag uint32) { ctx.codec_tag = C.uint32_t(tag) } +func (ctx *AVCodecParameters) Format() int { + return int(ctx.format) +} + //////////////////////////////////////////////////////////////////////////////// // AVCodec From 3d49cbf8a5c209fda11fa3cfec214e96173f2475 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Mon, 17 Jun 2024 23:10:51 +0200 Subject: [PATCH 2/5] updates --- manager.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ media.go | 8 ++++++++ mediatype.go | 16 ++++++++-------- reader.go | 6 +++--- 4 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 manager.go diff --git a/manager.go b/manager.go new file mode 100644 index 0000000..e6c2a12 --- /dev/null +++ b/manager.go @@ -0,0 +1,48 @@ +package media + +import ffmpeg "github.com/mutablelogic/go-media/sys/ffmpeg61" + +//////////////////////////////////////////////////////////////////////////// +// TYPES + +type manager struct { +} + +//////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +func NewManager() *manager { + return new(manager) +} + +//////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Return the list of input formats, filtering by name or mimetype +func (this *manager) InputFormats(mimetype string) []InputFormat { + var result []InputFormat + var opaque uintptr + for { + demuxer := ffmpeg.AVFormat_demuxer_iterate(&opaque) + if demuxer == nil { + break + } + if this.matchesInput(demuxer, mimetype) { + result = append(result, demuxer) + } + } + return result + +} + +// Return the list of output formats, filtering by name or mimetype +func (this *manager) OutputFormats(name string) []OutputFormat { + return nil +} + +//////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +func (this *manager) matchesInput(demuxer *ffmpeg.AVInputFormat, mimetype string) bool { + return true +} diff --git a/media.go b/media.go index fb304fc..62d60bd 100644 --- a/media.go +++ b/media.go @@ -3,6 +3,14 @@ package media import "io" +// InputFormat represents a container format for input +// of media streams. +type InputFormat interface{} + +// OuputFormat represents a container format for output +// of media streams. +type OutputFormat interface{} + // Media represents a media stream, which can // be input or output. A new media object is created // using NewReader, Open, NewWriter or Create. diff --git a/mediatype.go b/mediatype.go index 4bca96c..2ad080f 100644 --- a/mediatype.go +++ b/mediatype.go @@ -1,19 +1,19 @@ package media -import ( - ff "github.com/mutablelogic/go-media/sys/ffmpeg61" -) - //////////////////////////////////////////////////////////////////////////// // TYPES -// Media Types: Audio, Video, Subtitle or Data -type MediaType int +// Media type flags +type MediaType uint32 //////////////////////////////////////////////////////////////////////////// // GLOBALS const ( - AUDIO = MediaType(ff.AVMEDIA_TYPE_AUDIO) // Audio media type - VIDEO = MediaType(ff.AVMEDIA_TYPE_VIDEO) // Video media type + UNKNOWN MediaType = (1 << iota) // Usually treated as DATA + VIDEO // Video stream + AUDIO // Audio stream + DATA // Opaque data information usually continuous + SUBTITLE // Subtitle stream + ATTACHMENT ) diff --git a/reader.go b/reader.go index fa52c12..403105b 100644 --- a/reader.go +++ b/reader.go @@ -227,7 +227,7 @@ func (r *reader) Decode(fn FrameFunc) DecoderFunc { } } -type jsonMetadata struct { +type metadata struct { Key string `json:"key"` Value string `json:"value"` } @@ -237,10 +237,10 @@ 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(&jsonMetadata{ + result[i] = &metadata{ Key: entry.Key(), Value: entry.Value(), - }) + } } return result } From 358ffdf64a59357a3c30ce125aa7d52df156bd5b Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Tue, 18 Jun 2024 08:54:01 +0200 Subject: [PATCH 3/5] Updates --- cmd/cli/decode.go | 4 +- cmd/cli/demuxers.go | 26 +++++ cmd/cli/main.go | 2 + cmd/cli/muxers.go | 26 +++++ go.mod | 8 +- manager.go | 160 +++++++++++++++++++++++++--- manager_test.go | 34 ++++++ sys/ffmpeg61/avformat.go | 100 ----------------- sys/ffmpeg61/avformat_flags.go | 109 +++++++++++++++++++ sys/ffmpeg61/avformat_input.go | 34 +++++- sys/ffmpeg61/avformat_input_test.go | 6 +- sys/ffmpeg61/avformat_output.go | 24 ++++- 12 files changed, 406 insertions(+), 127 deletions(-) create mode 100644 cmd/cli/demuxers.go create mode 100644 cmd/cli/muxers.go create mode 100644 manager_test.go create mode 100644 sys/ffmpeg61/avformat_flags.go diff --git a/cmd/cli/decode.go b/cmd/cli/decode.go index 196198a..ceac6ef 100644 --- a/cmd/cli/decode.go +++ b/cmd/cli/decode.go @@ -1,12 +1,10 @@ package main import ( - - // Packages - "encoding/json" "fmt" + // Packages "github.com/mutablelogic/go-media" ) diff --git a/cmd/cli/demuxers.go b/cmd/cli/demuxers.go new file mode 100644 index 0000000..13043d8 --- /dev/null +++ b/cmd/cli/demuxers.go @@ -0,0 +1,26 @@ +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/main.go b/cmd/cli/main.go index b238bae..92c502e 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -15,6 +15,8 @@ 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"` Decode DecodeCmd `cmd:"decode" help:"Decode media"` } diff --git a/cmd/cli/muxers.go b/cmd/cli/muxers.go new file mode 100644 index 0000000..32f0d01 --- /dev/null +++ b/cmd/cli/muxers.go @@ -0,0 +1,26 @@ +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/go.mod b/go.mod index 2d3d2c2..2c224a9 100755 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.22.4 require ( github.com/alecthomas/kong v0.9.0 github.com/djthorpe/go-errors v1.0.3 - github.com/djthorpe/go-tablewriter v0.0.7 + 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 @@ -15,11 +15,11 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/term v0.20.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/term v0.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/manager.go b/manager.go index e6c2a12..0633628 100644 --- a/manager.go +++ b/manager.go @@ -1,6 +1,13 @@ package media -import ffmpeg "github.com/mutablelogic/go-media/sys/ffmpeg61" +import ( + "encoding/json" + "slices" + "strings" + + // Package imports + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" +) //////////////////////////////////////////////////////////////////////////// // TYPES @@ -8,6 +15,22 @@ import ffmpeg "github.com/mutablelogic/go-media/sys/ffmpeg61" type manager struct { } +type inputformat struct { + ctx *ff.AVInputFormat + Name string `json:"name"` + Description string `json:"description" writer:",wrap,width:40"` + Extensions string `json:"extensions,omitempty"` + MimeTypes string `json:"mimetypes,omitempty"` +} + +type outputformat struct { + ctx *ff.AVOutputFormat + Name string `json:"name"` + Description string `json:"description" writer:",wrap,width:40"` + Extensions string `json:"extensions,omitempty"` + MimeTypes string `json:"mimetypes,omitempty"` +} + //////////////////////////////////////////////////////////////////////////// // LIFECYCLE @@ -15,34 +38,147 @@ func NewManager() *manager { return new(manager) } +func newInputFormat(ctx *ff.AVInputFormat) *inputformat { + return &inputformat{ + ctx: ctx, + Name: ctx.Name(), + Description: ctx.LongName(), + Extensions: ctx.Extensions(), + MimeTypes: ctx.MimeTypes(), + } +} + +func newOutputFormat(ctx *ff.AVOutputFormat) *outputformat { + return &outputformat{ + ctx: ctx, + Name: ctx.Name(), + Description: ctx.LongName(), + Extensions: ctx.Extensions(), + MimeTypes: ctx.MimeTypes(), + } +} + +//////////////////////////////////////////////////////////////////////////// +// 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 -// Return the list of input formats, filtering by name or mimetype -func (this *manager) InputFormats(mimetype string) []InputFormat { +// 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 + + // Iterate over all input formats var opaque uintptr for { - demuxer := ffmpeg.AVFormat_demuxer_iterate(&opaque) + demuxer := ff.AVFormat_demuxer_iterate(&opaque) if demuxer == nil { break } - if this.matchesInput(demuxer, mimetype) { - result = append(result, demuxer) + if len(filter) == 0 { + result = append(result, newInputFormat(demuxer)) + } else if manager.matchesInput(demuxer, filter...) { + result = append(result, newInputFormat(demuxer)) } } - return result + // Return success + return result } -// Return the list of output formats, filtering by name or mimetype -func (this *manager) OutputFormats(name string) []OutputFormat { - return nil +// 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 + + // Iterate over all output formats + var opaque uintptr + for { + muxer := ff.AVFormat_muxer_iterate(&opaque) + if muxer == nil { + break + } + if len(filter) == 0 { + result = append(result, newOutputFormat(muxer)) + } else if manager.matchesOutput(muxer, filter...) { + result = append(result, newOutputFormat(muxer)) + } + } + + // Return success + return result } //////////////////////////////////////////////////////////////////////////// // PRIVATE METHODS -func (this *manager) matchesInput(demuxer *ffmpeg.AVInputFormat, mimetype string) bool { - return true +func (this *manager) matchesInput(demuxer *ff.AVInputFormat, mimetype ...string) bool { + // Match any + if len(mimetype) == 0 { + 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 (this *manager) matchesOutput(muxer *ff.AVOutputFormat, mimetype ...string) bool { + // Match any + if len(mimetype) == 0 { + return true + } + // Match mimetype + for _, mimetype := range mimetype { + mimetype = strings.ToLower(strings.TrimSpace(mimetype)) + if slices.Contains(strings.Split(muxer.Name(), ","), mimetype) { + return true + } + if strings.HasPrefix(mimetype, ".") { + ext := strings.TrimPrefix(mimetype, ".") + if slices.Contains(strings.Split(muxer.Extensions(), ","), ext) { + return true + } + } + if slices.Contains(strings.Split(muxer.MimeTypes(), ","), mimetype) { + return true + } + } + // No match + return false } diff --git a/manager_test.go b/manager_test.go new file mode 100644 index 0000000..990cc02 --- /dev/null +++ b/manager_test.go @@ -0,0 +1,34 @@ +package media_test + +import ( + // Import namespaces + "testing" + + // Package imports + "github.com/stretchr/testify/assert" + + // Namespace imports + . "github.com/mutablelogic/go-media" +) + +func Test_manager_001(t *testing.T) { + assert := assert.New(t) + + manager := NewManager() + assert.NotNil(manager) + + formats := manager.InputFormats() + assert.NotNil(formats) + t.Log(formats) +} + +func Test_manager_002(t *testing.T) { + assert := assert.New(t) + + manager := NewManager() + assert.NotNil(manager) + + formats := manager.OutputFormats() + assert.NotNil(formats) + t.Log(formats) +} diff --git a/sys/ffmpeg61/avformat.go b/sys/ffmpeg61/avformat.go index 605dcb6..75ece1a 100644 --- a/sys/ffmpeg61/avformat.go +++ b/sys/ffmpeg61/avformat.go @@ -61,46 +61,6 @@ const ( AVSEEK_FORCE = C.AVSEEK_FORCE ) -const ( - AVFMT_NONE AVFormat = 0 - // Demuxer will use avio_open, no opened file should be provided by the caller. - AVFMT_NOFILE AVFormat = C.AVFMT_NOFILE - // Needs '%d' in filename. - AVFMT_NEEDNUMBER AVFormat = C.AVFMT_NEEDNUMBER - // The muxer/demuxer is experimental and should be used with caution - AVFMT_EXPERIMENTAL AVFormat = C.AVFMT_EXPERIMENTAL - // Show format stream IDs numbers. - AVFMT_SHOWIDS AVFormat = C.AVFMT_SHOW_IDS - // Format wants global header. - AVFMT_GLOBALHEADER AVFormat = C.AVFMT_GLOBALHEADER - // Format does not need / have any timestamps. - AVFMT_NOTIMESTAMPS AVFormat = C.AVFMT_NOTIMESTAMPS - // Use generic index building code. - AVFMT_GENERICINDEX AVFormat = C.AVFMT_GENERIC_INDEX - // Format allows timestamp discontinuities. Note, muxers always require valid (monotone) timestamps - AVFMT_TSDISCONT AVFormat = C.AVFMT_TS_DISCONT - // Format allows variable fps. - AVFMT_VARIABLEFPS AVFormat = C.AVFMT_VARIABLE_FPS - // Format does not need width/height - AVFMT_NODIMENSIONS AVFormat = C.AVFMT_NODIMENSIONS - // Format does not require any streams - AVFMT_NOSTREAMS AVFormat = C.AVFMT_NOSTREAMS - // Format does not allow to fall back on binary search via read_timestamp - AVFMT_NOBINSEARCH AVFormat = C.AVFMT_NOBINSEARCH - // Format does not allow to fall back on generic search - AVFMT_NOGENSEARCH AVFormat = C.AVFMT_NOGENSEARCH - // Format does not allow seeking by bytes - AVFMT_NOBYTESEEK AVFormat = C.AVFMT_NO_BYTE_SEEK - // Format allows flushing. If not set, the muxer will not receive a NULL packet in the write_packet function. - AVFMT_ALLOWFLUSH AVFormat = C.AVFMT_ALLOW_FLUSH - // Format does not require strictly increasing timestamps, but they must still be monotonic - AVFMT_TS_NONSTRICT AVFormat = C.AVFMT_TS_NONSTRICT - // Format allows muxing negative timestamps - AVFMT_TS_NEGATIVE AVFormat = C.AVFMT_TS_NEGATIVE - AVFMT_MIN AVFormat = AVFMT_NOFILE - AVFMT_MAX AVFormat = AVFMT_TS_NEGATIVE -) - const ( AVIO_FLAG_NONE AVIOFlag = 0 AVIO_FLAG_READ AVIOFlag = C.AVIO_FLAG_READ @@ -224,66 +184,6 @@ func (ctx *AVFormatContext) Flags() AVFormat { return AVFormat(ctx.flags) } -//////////////////////////////////////////////////////////////////////////////// -// AVFormat - -func (f AVFormat) Is(flag AVFormat) bool { - return f&flag != 0 -} - -/* -func (v AVFormat) String() string { - if v == AVFMT_NONE { - return v.FlagString() - } - str := "" - for i := AVFMT_MIN; i <= AVFMT_MAX; i <<= 1 { - if v&i == i { - str += "|" + i.FlagString() - } - } - return str[1:] -} - -func (f AVFormat) FlagString() string { - switch f { - case AVFMT_NONE: - return "AVFMT_NONE" - case AVFMT_GENPTS: - return "AVFMT_GENPTS" - case AVFMT_IGNIDX: - return "AVFMT_IGNIDX" - case AVFMT_NONBLOCK: - return "AVFMT_NONBLOCK" - case AVFMT_IGNDTS: - return "AVFMT_IGNDTS" - case AVFMT_NOFILLIN: - return "AVFMT_NOFILLIN" - case AVFMT_NOPARSE: - return "AVFMT_NOPARSE" - case AVFMT_NOBUFFER: - return "AVFMT_NOBUFFER" - case AVFMT_CUSTOM_IO: - return "AVFMT_CUSTOM_IO" - case AVFMT_DISCARD_CORRUPT: - return "AVFMT_DISCARD_CORRUPT" - case AVFMT_FLUSH_PACKETS: - return "AVFMT_FLUSH_PACKETS" - case AVFMT_BITEXACT: - return "AVFMT_BITEXACT" - case AVFMT_SORT_DTS: - return "AVFMT_SORT_DTS" - case AVFMT_FAST_SEEK: - return "AVFMT_FAST_SEEK" - case AVFMT_SHORTEST: - return "AVFMT_SHORTEST" - case AVFMT_AUTO_BSF: - return "AVFMT_AUTO_BSF" - default: - return fmt.Sprintf("AVFormat(0x%08X)", uint32_t(f)) - } -} -*/ //////////////////////////////////////////////////////////////////////////////// // AVFormatFlag diff --git a/sys/ffmpeg61/avformat_flags.go b/sys/ffmpeg61/avformat_flags.go new file mode 100644 index 0000000..cec4e7e --- /dev/null +++ b/sys/ffmpeg61/avformat_flags.go @@ -0,0 +1,109 @@ +package ffmpeg + +import ( + "encoding/json" + "fmt" +) + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavformat +#include +*/ +import "C" + +const ( + AVFMT_NONE AVFormat = 0 + AVFMT_NOFILE AVFormat = C.AVFMT_NOFILE // Demuxer will use avio_open, no opened file should be provided by the caller. + AVFMT_NEEDNUMBER AVFormat = C.AVFMT_NEEDNUMBER // Needs '%d' in filename. + AVFMT_EXPERIMENTAL AVFormat = C.AVFMT_EXPERIMENTAL // The muxer/demuxer is experimental and should be used with caution + AVFMT_SHOWIDS AVFormat = C.AVFMT_SHOW_IDS // Show format stream IDs numbers. + AVFMT_GLOBALHEADER AVFormat = C.AVFMT_GLOBALHEADER // Format wants global header. + AVFMT_NOTIMESTAMPS AVFormat = C.AVFMT_NOTIMESTAMPS // Format does not need / have any timestamps. + AVFMT_GENERICINDEX AVFormat = C.AVFMT_GENERIC_INDEX // Use generic index building code. + AVFMT_TSDISCONT AVFormat = C.AVFMT_TS_DISCONT // Format allows timestamp discontinuities. Note, muxers always require valid (monotone) timestamps + AVFMT_VARIABLEFPS AVFormat = C.AVFMT_VARIABLE_FPS // Format allows variable fps. + AVFMT_NODIMENSIONS AVFormat = C.AVFMT_NODIMENSIONS // Format does not need width/height + AVFMT_NOSTREAMS AVFormat = C.AVFMT_NOSTREAMS // Format does not require any streams + AVFMT_NOBINSEARCH AVFormat = C.AVFMT_NOBINSEARCH // Format does not allow to fall back on binary search via read_timestamp + AVFMT_NOGENSEARCH AVFormat = C.AVFMT_NOGENSEARCH // Format does not allow to fall back on generic search + AVFMT_NOBYTESEEK AVFormat = C.AVFMT_NO_BYTE_SEEK // Format does not allow seeking by bytes + AVFMT_ALLOWFLUSH AVFormat = C.AVFMT_ALLOW_FLUSH // Format allows flushing. If not set, the muxer will not receive a NULL packet in the write_packet function. + AVFMT_TS_NONSTRICT AVFormat = C.AVFMT_TS_NONSTRICT // Format does not require strictly increasing timestamps, but they must still be monotonic + AVFMT_TS_NEGATIVE AVFormat = C.AVFMT_TS_NEGATIVE // Format allows muxing negative timestamps + AVFMT_SEEK_TO_PTS AVFormat = C.AVFMT_SEEK_TO_PTS // Seeking is based on PTS + AVFMT_MIN AVFormat = AVFMT_NOFILE + AVFMT_MAX AVFormat = AVFMT_SEEK_TO_PTS +) + +//////////////////////////////////////////////////////////////////////////////// +// AVFormat + +func (f AVFormat) Is(flag AVFormat) bool { + return f&flag != 0 +} + +func (v AVFormat) MarshalJSON() ([]byte, error) { + return json.Marshal(v.String()) +} + +func (v AVFormat) String() string { + if v == AVFMT_NONE { + return v.FlagString() + } + str := "" + for i := AVFMT_MIN; i <= AVFMT_MAX; i <<= 1 { + if v&i == i { + str += "|" + i.FlagString() + } + } + if str != "" { + str = str[1:] + } + return str +} + +func (f AVFormat) FlagString() string { + switch f { + case AVFMT_NONE: + return "AVFMT_NONE" + case AVFMT_NOFILE: + return "AVFMT_NOFILE" + case AVFMT_NEEDNUMBER: + return "AVFMT_NEEDNUMBER" + case AVFMT_EXPERIMENTAL: + return "AVFMT_EXPERIMENTAL" + case AVFMT_SHOWIDS: + return "AVFMT_SHOWIDS" + case AVFMT_GLOBALHEADER: + return "AVFMT_GLOBALHEADER" + case AVFMT_NOTIMESTAMPS: + return "AVFMT_NOTIMESTAMPS" + case AVFMT_GENERICINDEX: + return "AVFMT_GENERICINDEX" + case AVFMT_TSDISCONT: + return "AVFMT_TSDISCONT" + case AVFMT_VARIABLEFPS: + return "AVFMT_VARIABLEFPS" + case AVFMT_NODIMENSIONS: + return "AVFMT_NODIMENSIONS" + case AVFMT_NOSTREAMS: + return "AVFMT_NOSTREAMS" + case AVFMT_NOBINSEARCH: + return "AVFMT_NOBINSEARCH" + case AVFMT_NOGENSEARCH: + return "AVFMT_NOGENSEARCH" + case AVFMT_NOBYTESEEK: + return "AVFMT_NOBYTESEEK" + case AVFMT_ALLOWFLUSH: + return "AVFMT_ALLOWFLUSH" + case AVFMT_TS_NONSTRICT: + return "AVFMT_TS_NONSTRICT" + case AVFMT_TS_NEGATIVE: + return "AVFMT_TS_NEGATIVE" + default: + return fmt.Sprintf("AVFormat(0x%08X)", uint32(f)) + } +} diff --git a/sys/ffmpeg61/avformat_input.go b/sys/ffmpeg61/avformat_input.go index 445cb8c..b27a4c2 100644 --- a/sys/ffmpeg61/avformat_input.go +++ b/sys/ffmpeg61/avformat_input.go @@ -20,9 +20,9 @@ import "C" type jsonAVInputFormat struct { Name string `json:"name,omitempty"` LongName string `json:"long_name,omitempty"` - Flags AVFormat `json:"flags,omitempty"` - Extensions string `json:"extensions,omitempty"` MimeTypes string `json:"mime_types,omitempty"` + Extensions string `json:"extensions,omitempty"` + Flags AVFormat `json:"flags,omitempty"` } func (ctx *AVInputFormat) MarshalJSON() ([]byte, error) { @@ -46,7 +46,37 @@ func (ctx *AVInputFormat) String() string { //////////////////////////////////////////////////////////////////////////////// // BINDINGS +// Find AVInputFormat based on the short name of the input format. +func AVFormat_find_input_format(name string) *AVInputFormat { + cString := C.CString(name) + defer C.free(unsafe.Pointer(cString)) + return (*AVInputFormat)(C.av_find_input_format(cString)) +} + // Iterate over all AVInputFormats func AVFormat_demuxer_iterate(opaque *uintptr) *AVInputFormat { return (*AVInputFormat)(C.av_demuxer_iterate((*unsafe.Pointer)(unsafe.Pointer(opaque)))) } + +//////////////////////////////////////////////////////////////////////////////// +// PROPERTIES + +func (ctx *AVInputFormat) Name() string { + return C.GoString(ctx.name) +} + +func (ctx *AVInputFormat) LongName() string { + return C.GoString(ctx.long_name) +} + +func (ctx *AVInputFormat) Flags() AVFormat { + return AVFormat(ctx.flags) +} + +func (ctx *AVInputFormat) MimeTypes() string { + return C.GoString(ctx.mime_type) +} + +func (ctx *AVInputFormat) Extensions() string { + return C.GoString(ctx.extensions) +} diff --git a/sys/ffmpeg61/avformat_input_test.go b/sys/ffmpeg61/avformat_input_test.go index a51a484..dcc5c39 100644 --- a/sys/ffmpeg61/avformat_input_test.go +++ b/sys/ffmpeg61/avformat_input_test.go @@ -7,9 +7,11 @@ import ( // Namespace imports . "github.com/mutablelogic/go-media/sys/ffmpeg61" + "github.com/stretchr/testify/assert" ) func Test_avformat_input_001(t *testing.T) { + assert := assert.New(t) // Iterate over all input formats var opaque uintptr for { @@ -17,7 +19,7 @@ func Test_avformat_input_001(t *testing.T) { if demuxer == nil { break } - - t.Log(demuxer) + demuxer2 := AVFormat_find_input_format(demuxer.Name()) + assert.Equal(demuxer, demuxer2) } } diff --git a/sys/ffmpeg61/avformat_output.go b/sys/ffmpeg61/avformat_output.go index b97558f..1aeb213 100644 --- a/sys/ffmpeg61/avformat_output.go +++ b/sys/ffmpeg61/avformat_output.go @@ -52,6 +52,26 @@ func (ctx *AVOutputFormat) String() string { //////////////////////////////////////////////////////////////////////////////// // PROPERTIES +func (ctx *AVOutputFormat) Name() string { + return C.GoString(ctx.name) +} + +func (ctx *AVOutputFormat) LongName() string { + return C.GoString(ctx.long_name) +} + +func (ctx *AVOutputFormat) Flags() AVFormat { + return AVFormat(ctx.flags) +} + +func (ctx *AVOutputFormat) MimeTypes() string { + return C.GoString(ctx.mime_type) +} + +func (ctx *AVOutputFormat) Extensions() string { + return C.GoString(ctx.extensions) +} + func (ctx *AVOutputFormat) VideoCodec() AVCodecID { return AVCodecID(ctx.video_codec) } @@ -64,10 +84,6 @@ func (ctx *AVOutputFormat) SubtitleCodec() AVCodecID { return AVCodecID(ctx.subtitle_codec) } -func (ctx *AVOutputFormat) Flags() AVFormat { - return AVFormat(ctx.flags) -} - //////////////////////////////////////////////////////////////////////////////// // BINDINGS From 1b513e34939bac3f5614acf1c9063d4379273066 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Tue, 18 Jun 2024 09:11:39 +0200 Subject: [PATCH 4/5] Updated --- cmd/cli/decode.go | 2 +- cmd/ffmpeg/README.md | 2 +- cmd/ffmpeg/encode_audio/main.go | 2 +- cmd/ffmpeg/encode_video/main.go | 2 +- .../mux/{generate.go => generate.go_old} | 11 ++--- cmd/ffmpeg/mux/main.go | 8 +++- cmd/ffmpeg/mux/stream.go | 4 +- manager.go | 41 +++++++++++-------- sys/ffmpeg61/avutil_frame.go | 11 +++++ sys/ffmpeg61/swresample.go_old | 10 ----- 10 files changed, 52 insertions(+), 41 deletions(-) rename cmd/ffmpeg/mux/{generate.go => generate.go_old} (96%) delete mode 100755 sys/ffmpeg61/swresample.go_old diff --git a/cmd/cli/decode.go b/cmd/cli/decode.go index ceac6ef..142327e 100644 --- a/cmd/cli/decode.go +++ b/cmd/cli/decode.go @@ -9,7 +9,7 @@ import ( ) type DecodeCmd struct { - Path string `arg required help:"Media file" type:"path"` + Path string `arg:"" required:"" help:"Media file" type:"path"` } func (cmd *DecodeCmd) Run(globals *Globals) error { diff --git a/cmd/ffmpeg/README.md b/cmd/ffmpeg/README.md index 10b54b5..c301d60 100644 --- a/cmd/ffmpeg/README.md +++ b/cmd/ffmpeg/README.md @@ -15,7 +15,7 @@ using the low-level golang bindings. Generate a synthetic audio signal and encode it to an output MP2 file. * [encode_video](encode_video) - libavcodec encoding video API usage example. Generate synthetic video data and encode it to an output file. -* [mux](mux) - Muxing - libavformat/libavcodec muxing API usage example. +* [mux](mux) - Muxing - libavformat/libavcodec muxing API usage example - NOT COMPLETED Generate a synthetic audio signal and mux it into a container format. * [remux](remux) - Remuxing - libavformat/libavcodec demuxing and muxing API usage example. Remux streams from one container format to another. Data is copied from the input to the output diff --git a/cmd/ffmpeg/encode_audio/main.go b/cmd/ffmpeg/encode_audio/main.go index c42cd81..1b2b5bf 100644 --- a/cmd/ffmpeg/encode_audio/main.go +++ b/cmd/ffmpeg/encode_audio/main.go @@ -83,7 +83,7 @@ func main() { } // Allocate the data buffers - if err := ff.AVUtil_frame_get_buffer(frame, 0); err != nil { + if err := ff.AVUtil_frame_get_buffer(frame, false); err != nil { log.Fatal(err) } diff --git a/cmd/ffmpeg/encode_video/main.go b/cmd/ffmpeg/encode_video/main.go index cb6e36e..f31281f 100644 --- a/cmd/ffmpeg/encode_video/main.go +++ b/cmd/ffmpeg/encode_video/main.go @@ -92,7 +92,7 @@ func main() { frame.SetHeight(ctx.Height()) // Allocate the data buffers - if err := ff.AVUtil_frame_get_buffer(frame, 0); err != nil { + if err := ff.AVUtil_frame_get_buffer(frame, false); err != nil { log.Fatal(err) } diff --git a/cmd/ffmpeg/mux/generate.go b/cmd/ffmpeg/mux/generate.go_old similarity index 96% rename from cmd/ffmpeg/mux/generate.go rename to cmd/ffmpeg/mux/generate.go_old index 9207b6f..02657a9 100644 --- a/cmd/ffmpeg/mux/generate.go +++ b/cmd/ffmpeg/mux/generate.go_old @@ -67,7 +67,7 @@ func write_video_frame(ctx *ff.AVFormatContext, stream *Stream) bool { - +/* static int write_audio_frame(AVFormatContext *oc, OutputStream *ost) { AVCodecContext *c; @@ -80,8 +80,8 @@ func write_video_frame(ctx *ff.AVFormatContext, stream *Stream) bool { frame = get_audio_frame(ost); if (frame) { - /* convert samples from native format to destination codec format, using the resampler */ - /* compute destination number of samples */ + /* convert samples from native format to destination codec format, using the resampler + /* compute destination number of samples dst_nb_samples = av_rescale_rnd(swr_get_delay(ost->swr_ctx, c->sample_rate) + frame->nb_samples, c->sample_rate, c->sample_rate, AV_ROUND_UP); av_assert0(dst_nb_samples == frame->nb_samples); @@ -94,7 +94,7 @@ func write_video_frame(ctx *ff.AVFormatContext, stream *Stream) bool { if (ret < 0) exit(1); - /* convert to destination format */ + /* convert to destination format ret = swr_convert(ost->swr_ctx, ost->frame->data, dst_nb_samples, (const uint8_t **)frame->data, frame->nb_samples); @@ -109,4 +109,5 @@ func write_video_frame(ctx *ff.AVFormatContext, stream *Stream) bool { } return write_frame(oc, c, ost->st, frame, ost->tmp_pkt); - } \ No newline at end of file + } + */ diff --git a/cmd/ffmpeg/mux/main.go b/cmd/ffmpeg/mux/main.go index 8d70f7a..1c10c3b 100644 --- a/cmd/ffmpeg/mux/main.go +++ b/cmd/ffmpeg/mux/main.go @@ -90,9 +90,13 @@ func main() { for encode_audio || encode_video { // Choose video if both are available, and video is earlier than audio if (encode_video && !encode_audio) || (encode_video && ff.AVUtil_compare_ts(video.next_pts, video.Encoder.TimeBase(), audio.next_pts, audio.Encoder.TimeBase()) <= 0) { - encode_video = !write_video_frame(ctx, video) + fmt.Println("TODO: Write video frame") + encode_video = false + // encode_video = !write_video_frame(ctx, video) } else { - encode_audio = !write_audio_frame(ctx, audio) + fmt.Println("TODO: Write audio frame") + encode_audio = false + // encode_audio = !write_audio_frame(ctx, audio) } } diff --git a/cmd/ffmpeg/mux/stream.go b/cmd/ffmpeg/mux/stream.go index 096e702..63ea0c9 100644 --- a/cmd/ffmpeg/mux/stream.go +++ b/cmd/ffmpeg/mux/stream.go @@ -223,7 +223,7 @@ func alloc_video_frame(pix_fmt ff.AVPixelFormat, width, height int) (*ff.AVFrame frame.SetPixFmt(pix_fmt) // allocate the buffers for the frame data - if err := ff.AVUtil_frame_get_buffer(frame, 0); err != nil { + if err := ff.AVUtil_frame_get_buffer(frame, false); err != nil { ff.AVUtil_frame_free(frame) return nil, err } @@ -246,7 +246,7 @@ func alloc_audio_frame(sample_fmt ff.AVSampleFormat, channel_layout ff.AVChannel } // allocate the buffers for the frame data - if err := ff.AVUtil_frame_get_buffer(frame, 0); err != nil { + if err := ff.AVUtil_frame_get_buffer(frame, false); err != nil { ff.AVUtil_frame_free(frame) return nil, err } diff --git a/manager.go b/manager.go index 0633628..9405ccd 100644 --- a/manager.go +++ b/manager.go @@ -15,20 +15,21 @@ import ( type manager struct { } -type inputformat struct { - ctx *ff.AVInputFormat - Name string `json:"name"` +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"` } +type inputformat struct { + formatmeta + ctx *ff.AVInputFormat +} + type outputformat struct { - ctx *ff.AVOutputFormat - Name string `json:"name"` - Description string `json:"description" writer:",wrap,width:40"` - Extensions string `json:"extensions,omitempty"` - MimeTypes string `json:"mimetypes,omitempty"` + formatmeta + ctx *ff.AVOutputFormat } //////////////////////////////////////////////////////////////////////////// @@ -40,21 +41,25 @@ func NewManager() *manager { func newInputFormat(ctx *ff.AVInputFormat) *inputformat { return &inputformat{ - ctx: ctx, - Name: ctx.Name(), - Description: ctx.LongName(), - Extensions: ctx.Extensions(), - MimeTypes: ctx.MimeTypes(), + ctx: ctx, + formatmeta: formatmeta{ + Name: ctx.Name(), + Description: ctx.LongName(), + Extensions: ctx.Extensions(), + MimeTypes: ctx.MimeTypes(), + }, } } func newOutputFormat(ctx *ff.AVOutputFormat) *outputformat { return &outputformat{ - ctx: ctx, - Name: ctx.Name(), - Description: ctx.LongName(), - Extensions: ctx.Extensions(), - MimeTypes: ctx.MimeTypes(), + ctx: ctx, + formatmeta: formatmeta{ + Name: ctx.Name(), + Description: ctx.LongName(), + Extensions: ctx.Extensions(), + MimeTypes: ctx.MimeTypes(), + }, } } diff --git a/sys/ffmpeg61/avutil_frame.go b/sys/ffmpeg61/avutil_frame.go index 185a45d..7aa1188 100644 --- a/sys/ffmpeg61/avutil_frame.go +++ b/sys/ffmpeg61/avutil_frame.go @@ -239,3 +239,14 @@ func (ctx *AVFrame) Int16(plane int) []int16 { return cInt16Slice(unsafe.Pointer(buf.data), C.int(buf.size)>>1) } } + +// Returns the data as a set of planes and strides +func (ctx *AVFrame) Data() ([][]byte, []int) { + planes := make([][]byte, int(C.AV_NUM_DATA_POINTERS)) + strides := make([]int, int(C.AV_NUM_DATA_POINTERS)) + for i := 0; i < int(C.AV_NUM_DATA_POINTERS); i++ { + planes[i] = ctx.Uint8(i) + strides[i] = ctx.Linesize(i) + } + return planes, strides +} diff --git a/sys/ffmpeg61/swresample.go_old b/sys/ffmpeg61/swresample.go_old deleted file mode 100755 index 878cdeb..0000000 --- a/sys/ffmpeg61/swresample.go_old +++ /dev/null @@ -1,10 +0,0 @@ - -// Set/reset common parameters. -func SWR_alloc_set_opts2(ctx *SWRContext, out_ch_layout *AVChannelLayout, out_sample_fmt AVSampleFormat, out_sample_rate int, in_ch_layout *AVChannelLayout, in_sample_fmt AVSampleFormat, in_sample_rate int, log_offset AVLogLevel, log_context *AVClass) error { - ctx_ := (*C.struct_SwrContext)(ctx) - if err := AVError(C.swr_alloc_set_opts2(&ctx_, (*C.struct_AVChannelLayout)(out_ch_layout), C.enum_AVSampleFormat(out_sample_fmt), C.int(out_sample_rate), (*C.struct_AVChannelLayout)(in_ch_layout), C.enum_AVSampleFormat(in_sample_fmt), C.int(in_sample_rate), C.int(log_offset), unsafe.Pointer(log_context))); err == 0 { - return nil - } else { - return err - } -} From 6e24c544a1ff6b670aa0d1c00082aca59f3bc912 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Tue, 18 Jun 2024 09:12:56 +0200 Subject: [PATCH 5/5] Updated tests --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 5b06f42..5f8183c 100755 --- a/Makefile +++ b/Makefile @@ -44,6 +44,8 @@ test: go-dep @echo Test @${GO} mod tidy @${GO} test ./sys/ffmpeg61 + @${GO} test ./pkg/... + @${GO} test . $(CMD_DIR): go-dep mkdir @echo Build cmd $(notdir $@)