From c657bb0ae9004d31ebb3f8d7ab4da2290993c7fa Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Mon, 19 May 2025 07:27:26 +0200 Subject: [PATCH 1/6] Added music match --- cmd/media/fingerprint.go | 94 ++++++++++++++++++++++++++++++++++ cmd/media/main.go | 1 + pkg/chromaprint/client.go | 4 +- pkg/chromaprint/client_test.go | 2 +- 4 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 cmd/media/fingerprint.go diff --git a/cmd/media/fingerprint.go b/cmd/media/fingerprint.go new file mode 100644 index 0000000..09bdcc6 --- /dev/null +++ b/cmd/media/fingerprint.go @@ -0,0 +1,94 @@ +package main + +import ( + "fmt" + "os" + + // Packages + chromaprint "github.com/mutablelogic/go-media/pkg/chromaprint" + "github.com/mutablelogic/go-media/pkg/ffmpeg" + server "github.com/mutablelogic/go-server" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type FingerprintCommands struct { + MatchMusic MatchMusic `cmd:"" group:"MATCH" help:"Match Music Track"` +} + +type MatchMusic struct { + Path string `arg:"" type:"path" help:"File"` + APIKey string `env:"CHROMAPRINT_KEY" help:"API key for the music matching service (https://acoustid.org/login)"` + Type []string `cmd:"" help:"Type of match to perform" enum:"any,recording,release,releasegroup,track" default:"any"` + Score float64 `cmd:"" help:"Minimum match scoreto perform" default:"0.9"` +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +func (cmd *MatchMusic) Run(app server.Cmd) error { + ffmpeg.SetLogging(false, nil) + + // Create a client + client, err := chromaprint.NewClient(cmd.APIKey) + if err != nil { + return err + } + + // Open the file + r, err := os.Open(cmd.Path) + if err != nil { + return err + } + defer r.Close() + + var meta chromaprint.Meta + for _, t := range cmd.Type { + switch t { + case "any": + meta |= chromaprint.META_ALL + case "recording": + meta |= chromaprint.META_RECORDING + case "release": + meta |= chromaprint.META_RELEASE + case "releasegroup": + meta |= chromaprint.META_RELEASEGROUP + case "track": + meta |= chromaprint.META_TRACK + default: + return fmt.Errorf("unknown type %q", t) + } + } + + // Create the matches + matches, err := client.Match(app.Context(), r, meta) + if err != nil { + return err + } + + // Filter by score + result := make([]*chromaprint.ResponseMatch, 0, len(matches)) + for _, m := range matches { + if m.Score >= cmd.Score { + result = append(result, m) + } + } + + fmt.Println(result) + return nil +} + +/* + + META_RECORDING Meta = (1 << iota) + META_RECORDINGID + META_RELEASE + META_RELEASEID + META_RELEASEGROUP + META_RELEASEGROUPID + META_TRACK + META_COMPRESS + META_USERMETA + META_SOURCE +*/ diff --git a/cmd/media/main.go b/cmd/media/main.go index ae9c86b..988f5e6 100644 --- a/cmd/media/main.go +++ b/cmd/media/main.go @@ -14,6 +14,7 @@ import ( type CLI struct { MetadataCommands CodecCommands + FingerprintCommands VersionCommands } diff --git a/pkg/chromaprint/client.go b/pkg/chromaprint/client.go index 87ab6cc..4e11915 100644 --- a/pkg/chromaprint/client.go +++ b/pkg/chromaprint/client.go @@ -95,7 +95,7 @@ func (c *Client) Lookup(fingerprint string, duration time.Duration, flags Meta) // FINGERPRINT // Match a media file and lookup any matches -func (c *Client) Match(ctx context.Context, r io.Reader) ([]*ResponseMatch, error) { +func (c *Client) Match(ctx context.Context, r io.Reader, flags Meta) ([]*ResponseMatch, error) { // Create a segmenter segmenter, err := segmenter.NewReader(r, 0, 32000) if err != nil { @@ -133,5 +133,5 @@ func (c *Client) Match(ctx context.Context, r io.Reader) ([]*ResponseMatch, erro } // Lookup fingerprint - return c.Lookup(value, segmenter.Duration(), META_ALL) + return c.Lookup(value, segmenter.Duration(), flags) } diff --git a/pkg/chromaprint/client_test.go b/pkg/chromaprint/client_test.go index 09fb8a9..4917b0b 100644 --- a/pkg/chromaprint/client_test.go +++ b/pkg/chromaprint/client_test.go @@ -89,7 +89,7 @@ func Test_client_004(t *testing.T) { } defer r.Close() - matches, err := client.Match(context.Background(), r) + matches, err := client.Match(context.Background(), r, META_ALL) assert.NoError(err) t.Log(matches) } From 1d18d979e4e652d1031ec00f061d270eaba2855e Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Mon, 19 May 2025 08:08:18 +0200 Subject: [PATCH 2/6] Added streams --- Makefile | 2 +- cmd/media/fingerprint.go | 2 +- cmd/media/metadata.go | 44 +++++++++++++++++++++++++ etc/docker/Dockerfile | 2 +- pkg/ffmpeg/manager.go | 5 +-- pkg/ffmpeg/reader.go | 32 ++++++++++++++++++ pkg/ffmpeg/stream.go | 69 +++++++++++++++++++++++++++++++++++++++ sys/chromaprint/README.md | 41 ----------------------- sys/chromaprint/doc.go | 3 -- 9 files changed, 149 insertions(+), 51 deletions(-) create mode 100644 pkg/ffmpeg/stream.go delete mode 100644 sys/chromaprint/README.md diff --git a/Makefile b/Makefile index 448edac..7c15c02 100755 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ PREFIX ?= ${BUILD_DIR}/install # TARGETS .PHONY: all -all: clean ffmpeg cli +all: clean ffmpeg chromaprint cli .PHONY: cmds cmds: $(CMD_DIR) diff --git a/cmd/media/fingerprint.go b/cmd/media/fingerprint.go index 09bdcc6..64b5208 100644 --- a/cmd/media/fingerprint.go +++ b/cmd/media/fingerprint.go @@ -6,7 +6,7 @@ import ( // Packages chromaprint "github.com/mutablelogic/go-media/pkg/chromaprint" - "github.com/mutablelogic/go-media/pkg/ffmpeg" + ffmpeg "github.com/mutablelogic/go-media/pkg/ffmpeg" server "github.com/mutablelogic/go-server" ) diff --git a/cmd/media/metadata.go b/cmd/media/metadata.go index 50026ab..847c591 100644 --- a/cmd/media/metadata.go +++ b/cmd/media/metadata.go @@ -24,6 +24,7 @@ import ( type MetadataCommands struct { Meta ListMetadata `cmd:"" group:"METADATA" help:"Examine metadata"` Artwork ExtractArtwork `cmd:"" group:"METADATA" help:"Extract artwork"` + Streams ListStreams `cmd:"" group:"METADATA" help:"List streams"` Thumbnails ExtractThumbnails `cmd:"" group:"METADATA" help:"Extract video thumbnails"` } @@ -38,6 +39,11 @@ type ExtractArtwork struct { Out string `required:"" help:"Output filename for artwork, relative to the source path. Use {count} {hash} {path} {name} or {ext} for placeholders" default:"{hash}{ext}"` } +type ListStreams struct { + Path string `arg:"" type:"path" help:"File or directory"` + Recursive bool `short:"r" help:"Recursively examine files"` +} + type ExtractThumbnails struct { Path string `arg:"" type:"path" help:"File"` Out string `required:"" help:"Output filename for thumbnail, relative to the source path. Use {timestamp} {frame} {path} {name} or {ext} for placeholders" default:"{frame}{ext}"` @@ -48,6 +54,44 @@ type ExtractThumbnails struct { /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS +func (cmd *ListStreams) Run(app server.Cmd) error { + // Create the media manager + manager, err := ffmpeg.NewManager(ffmpeg.OptLog(false, nil)) + if err != nil { + return err + } + + // Create a new file walker + walker := file.NewWalker(func(ctx context.Context, root, relpath string, info os.FileInfo) error { + if info.IsDir() { + if !cmd.Recursive && relpath != "." { + return file.SkipDir + } + return nil + } + + // Open file + f, err := manager.Open(filepath.Join(root, relpath), nil) + if err != nil { + return fmt.Errorf("%s: %w", info.Name(), err) + } + defer f.Close() + + // Enumerate streams + streams := f.(*ffmpeg.Reader).Streams(media.ANY) + result := make([]media.Metadata, 0, len(streams)) + result = append(result, ffmpeg.NewMetadata("path", filepath.Join(root, relpath))) + for _, meta := range streams { + result = append(result, ffmpeg.NewMetadata(fmt.Sprint(meta.Index()), meta)) + } + + return write(os.Stdout, result, nil) + }) + + // Perform the walk, return any errors + return walker.Walk(app.Context(), cmd.Path) +} + func (cmd *ListMetadata) Run(app server.Cmd) error { // Create the media manager manager, err := ffmpeg.NewManager(ffmpeg.OptLog(false, nil)) diff --git a/etc/docker/Dockerfile b/etc/docker/Dockerfile index 6b2a7a2..63c1c4a 100644 --- a/etc/docker/Dockerfile +++ b/etc/docker/Dockerfile @@ -10,7 +10,7 @@ COPY . . # Install dependencies RUN set -x && apt update -y \ - && apt install -y ca-certificates lsb-release nasm curl \ + && apt install -y ca-certificates lsb-release cmake nasm curl \ && apt install -y libfreetype-dev libmp3lame-dev libopus-dev libvorbis-dev libvpx-dev libx264-dev libx265-dev libnuma-dev # Build all the commands diff --git a/pkg/ffmpeg/manager.go b/pkg/ffmpeg/manager.go index eeb0480..d8a4fa1 100644 --- a/pkg/ffmpeg/manager.go +++ b/pkg/ffmpeg/manager.go @@ -7,9 +7,6 @@ import ( // Packages media "github.com/mutablelogic/go-media" ff "github.com/mutablelogic/go-media/sys/ffmpeg71" - - // Namespace imports - . "github.com/djthorpe/go-errors" ) /////////////////////////////////////////////////////////////////////////////// @@ -106,7 +103,7 @@ func (manager *Manager) Open(url string, format media.Format, opts ...string) (m if format_, ok := format.(*Format); ok && format_.Input != nil { o = append(o, optInputFormat(format_)) } else { - return nil, ErrBadParameter.With("invalid input format") + return nil, media.ErrBadParameter.With("invalid input format") } } if len(opts) > 0 { diff --git a/pkg/ffmpeg/reader.go b/pkg/ffmpeg/reader.go index 27edfed..425e12f 100644 --- a/pkg/ffmpeg/reader.go +++ b/pkg/ffmpeg/reader.go @@ -211,6 +211,38 @@ func (r *Reader) BestStream(t media.Type) int { return -1 } +// Return all streams of a specific type (video, audio, subtitle, data) +func (r *Reader) Streams(t media.Type) []*Stream { + var result []*Stream + for _, stream := range r.input.Streams() { + switch stream.CodecPar().CodecType() { + case ff.AVMEDIA_TYPE_VIDEO: + if t.Is(media.VIDEO) || t == media.ANY { + result = append(result, newStream(stream)) + } + case ff.AVMEDIA_TYPE_AUDIO: + if t.Is(media.AUDIO) || t == media.ANY { + result = append(result, newStream(stream)) + } + case ff.AVMEDIA_TYPE_SUBTITLE: + if t.Is(media.SUBTITLE) || t == media.ANY { + result = append(result, newStream(stream)) + } + case ff.AVMEDIA_TYPE_DATA: + if t.Is(media.DATA) || t == media.ANY { + result = append(result, newStream(stream)) + } + case ff.AVMEDIA_TYPE_ATTACHMENT: + if t.Is(media.DATA) || t == media.ANY { + result = append(result, newStream(stream)) + } + } + } + + // Return the streams + return result +} + // Return the metadata for the media stream, filtering by the specified keys // if there are any. Artwork is returned with the "artwork" key. func (r *Reader) Metadata(keys ...string) []*Metadata { diff --git a/pkg/ffmpeg/stream.go b/pkg/ffmpeg/stream.go new file mode 100644 index 0000000..8550f00 --- /dev/null +++ b/pkg/ffmpeg/stream.go @@ -0,0 +1,69 @@ +package ffmpeg + +import ( + "encoding/json" + + // Packages + media "github.com/mutablelogic/go-media" + ff "github.com/mutablelogic/go-media/sys/ffmpeg71" +) + +//////////////////////////////////////////////////////////////////////////////// +// TYPES + +type Stream struct { + ctx *ff.AVStream +} + +//////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// Create a new stream +func newStream(ctx *ff.AVStream) *Stream { + return &Stream{ + ctx: ctx, + } +} + +//////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (s *Stream) MarshalJSON() ([]byte, error) { + type j struct { + Index int `json:"index"` + Type media.Type `json:"type"` + Codec *ff.AVCodecParameters `json:"codec,omitempty"` + } + return json.Marshal(j{ + Index: s.Index(), + Type: s.Type(), + Codec: s.ctx.CodecPar(), + }) +} + +//////////////////////////////////////////////////////////////////////////////// +// PROPERTIES + +// Return the stream index +func (s *Stream) Index() int { + return int(s.ctx.Index()) +} + +// Return the stream type +func (s *Stream) Type() media.Type { + if s.ctx.Disposition()&ff.AV_DISPOSITION_ATTACHED_PIC != 0 { + return media.DATA + } + switch s.ctx.CodecPar().CodecType() { + case ff.AVMEDIA_TYPE_VIDEO: + return media.VIDEO + case ff.AVMEDIA_TYPE_AUDIO: + return media.AUDIO + case ff.AVMEDIA_TYPE_DATA: + return media.DATA + case ff.AVMEDIA_TYPE_SUBTITLE: + return media.SUBTITLE + default: + return media.UNKNOWN + } +} diff --git a/sys/chromaprint/README.md b/sys/chromaprint/README.md deleted file mode 100644 index 2a54226..0000000 --- a/sys/chromaprint/README.md +++ /dev/null @@ -1,41 +0,0 @@ - -# go-media chromaprint bindings - -This package provides bindings for [chromaprint](https://acoustid.org/chromaprint) audio fingerprinting. - -This package is part of a wider project, `github.com/mutablelogic/go-media`. -Please see the [module documentation](https://github.com/mutablelogic/go-media/blob/master/README.md) -for more information. - -## Building - -In order to use this package, you will need to install the chromaprint libraries. -On Darwin (Mac) with Homebrew installed: - -```bash -[zsh] brew install chromaprint -[zsh] go get git@github.com:djthorpe/go-media.git -[zsh] cd go-media/sys/chromaprint -``` - -For Linux Debian, - -```bash -[bash] sudo apt install libchromaprint-dev -[zsh] go get git@github.com:djthorpe/go-media.git -[zsh] cd go-media/sys/chromaprint -``` - -For more information: - - * API Documentation Sources: https://github.com/acoustid/chromaprint - * Web Service: https://acoustid.org/webservice - -This package provides low-level library bindings. There is also a -[client library for the web service](https://github.com/mutablelogic/go-media/tree/master/pkg/chromaprint). - -## Usage - -TODO - - diff --git a/sys/chromaprint/doc.go b/sys/chromaprint/doc.go index beee02d..c22973e 100644 --- a/sys/chromaprint/doc.go +++ b/sys/chromaprint/doc.go @@ -13,9 +13,6 @@ package chromaprint % sudo apt install libchromaprint-dev - You will also need to use -tags chromaprint when testing, building or - installing. - API Documentation Sources: https://github.com/acoustid/chromaprint From c7dca06f261c266d597bdac555c3e90e46c62bd4 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Mon, 19 May 2025 08:33:36 +0200 Subject: [PATCH 3/6] Updates --- Makefile | 5 ++--- etc/docker/Dockerfile | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 7c15c02..e3af199 100755 --- a/Makefile +++ b/Makefile @@ -100,7 +100,6 @@ ${BUILD_DIR}/${CHROMAPRINT_VERSION}: chromaprint-configure: mkdir ${BUILD_DIR}/${CHROMAPRINT_VERSION} @echo "Configuring ${CHROMAPRINT_VERSION} => ${PREFIX}" cmake \ - -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ -DCMAKE_BUILD_TYPE=Release \ -DBUILD_SHARED_LIBS=0 \ -DBUILD_TESTS=0 \ @@ -216,9 +215,9 @@ ffmpeg-dep: $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists freetype2 && echo "--enable-libfreetype")) $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists theora && echo "--enable-libtheora")) $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists vorbis && echo "--enable-libvorbis")) - $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists vpx && echo "--enable-libvpx")) + $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists opus && echo "--enable-libopus")) $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists x264 && echo "--enable-libx264")) $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists x265 && echo "--enable-libx265")) - $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists opus && echo "--enable-libopus")) $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists xvid && echo "--enable-libxvid")) + $(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists vpx && echo "--enable-libvpx")) @echo "FFmpeg configuration: $(FFMPEG_CONFIG)" diff --git a/etc/docker/Dockerfile b/etc/docker/Dockerfile index 63c1c4a..58bd0be 100644 --- a/etc/docker/Dockerfile +++ b/etc/docker/Dockerfile @@ -10,7 +10,7 @@ COPY . . # Install dependencies RUN set -x && apt update -y \ - && apt install -y ca-certificates lsb-release cmake nasm curl \ + && apt install -y ca-certificates lsb-release build-essential cmake nasm curl \ && apt install -y libfreetype-dev libmp3lame-dev libopus-dev libvorbis-dev libvpx-dev libx264-dev libx265-dev libnuma-dev # Build all the commands From 2d6df88941128162bb829813c64823fedcffa52e Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Mon, 19 May 2025 09:23:25 +0200 Subject: [PATCH 4/6] Updated --- Makefile | 23 +++++--- error.go | 7 ++- manager.go | 42 +++++++++++---- pkg/ffmpeg/manager.go | 95 +++++++++++++++++++++++++--------- pkg/ffmpeg/opts.go | 12 +++++ pkg/ffmpeg/writer.go | 6 +++ sys/chromaprint/chromaprint.go | 1 - 7 files changed, 142 insertions(+), 44 deletions(-) diff --git a/Makefile b/Makefile index e3af199..5fd2043 100755 --- a/Makefile +++ b/Makefile @@ -6,6 +6,9 @@ DOCKER=$(shell which docker) FFMPEG_VERSION=ffmpeg-7.1.1 CHROMAPRINT_VERSION=chromaprint-1.5.1 +# CGO configuration - set CGO vars for C++ libraries +CGO_ENV=PKG_CONFIG_PATH="$(shell realpath ${PREFIX})/lib/pkgconfig" CGO_LDFLAGS_ALLOW="-(W|D).*" CGO_LDFLAGS="-lstdc++" + # Build flags BUILD_MODULE := $(shell cat go.mod | head -1 | cut -d ' ' -f 2) BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/version.GitSource=${BUILD_MODULE} @@ -39,11 +42,11 @@ cmds: $(CMD_DIR) .PHONY: cli cli: go-dep go-tidy mkdir @echo Build media tool - @PKG_CONFIG_PATH="$(shell realpath ${PREFIX})/lib/pkgconfig" CGO_LDFLAGS_ALLOW="-(W|D).*" ${GO} build ${BUILD_FLAGS} -o ${BUILD_DIR}/media ./cmd/media + @${CGO_ENV} ${GO} build ${BUILD_FLAGS} -o ${BUILD_DIR}/media ./cmd/media $(CMD_DIR): go-dep go-tidy mkdir @echo Build cmd $(notdir $@) - @PKG_CONFIG_PATH="$(shell realpath ${PREFIX})/lib/pkgconfig" CGO_LDFLAGS_ALLOW="-(W|D).*" ${GO} build ${BUILD_FLAGS} -o ${BUILD_DIR}/$(notdir $@) ./$@ + @${CGO_ENV} ${GO} build ${BUILD_FLAGS} -o ${BUILD_DIR}/$(notdir $@) ./$@ ############################################################################### # FFMPEG @@ -100,6 +103,7 @@ ${BUILD_DIR}/${CHROMAPRINT_VERSION}: chromaprint-configure: mkdir ${BUILD_DIR}/${CHROMAPRINT_VERSION} @echo "Configuring ${CHROMAPRINT_VERSION} => ${PREFIX}" cmake \ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ -DCMAKE_BUILD_TYPE=Release \ -DBUILD_SHARED_LIBS=0 \ -DBUILD_TESTS=0 \ @@ -115,10 +119,13 @@ chromaprint-build: chromaprint-configure @cd $(BUILD_DIR) && make -j2 # Install chromaprint +# Create a modified pkg-config file that ensures correct linking order for C++ .PHONY: chromaprint chromaprint: chromaprint-build @echo "Installing ${CHROMAPRINT_VERSION} => ${PREFIX}" @cd $(BUILD_DIR) && make install + @sed -i.bak 's/Libs: -L\${libdir} -lchromaprint/Libs: -L\${libdir} -lchromaprint -lstdc++/g' "${PREFIX}/lib/pkgconfig/libchromaprint.pc" + @rm -f "${PREFIX}/lib/pkgconfig/libchromaprint.pc.bak" ############################################################################### # DOCKER @@ -147,11 +154,11 @@ test: test-ffmpeg test-ffmpeg: go-dep go-tidy ffmpeg chromaprint @echo Test @echo ... test sys/ffmpeg71 - @PKG_CONFIG_PATH="$(shell realpath ${PREFIX})/lib/pkgconfig" CGO_LDFLAGS_ALLOW="-(W|D).*" ${GO} test ./sys/ffmpeg71 + @${CGO_ENV} ${GO} test ./sys/ffmpeg71 @echo ... test pkg/segmenter - @PKG_CONFIG_PATH="$(shell realpath ${PREFIX})/lib/pkgconfig" CGO_LDFLAGS_ALLOW="-(W|D).*" ${GO} test ./pkg/segmenter + @${CGO_ENV} ${GO} test ./pkg/segmenter @echo ... test pkg/chromaprint - @PKG_CONFIG_PATH="$(shell realpath ${PREFIX})/lib/pkgconfig" CGO_LDFLAGS_ALLOW="-(W|D).*" ${GO} test ./pkg/chromaprint + @${CGO_ENV} ${GO} test ./pkg/chromaprint # @echo ... test pkg/ffmpeg @@ -172,11 +179,11 @@ test-ffmpeg: go-dep go-tidy ffmpeg chromaprint container-test: go-dep go-tidy ffmpeg chromaprint @echo Test @echo ... test sys/ffmpeg71 - @PKG_CONFIG_PATH="$(shell realpath ${PREFIX})/lib/pkgconfig" CGO_LDFLAGS_ALLOW="-(W|D).*" ${GO} test ./sys/ffmpeg71 + @${CGO_ENV} ${GO} test ./sys/ffmpeg71 @echo ... test pkg/segmenter - @PKG_CONFIG_PATH="$(shell realpath ${PREFIX})/lib/pkgconfig" CGO_LDFLAGS_ALLOW="-(W|D).*" ${GO} test ./pkg/segmenter + @${CGO_ENV} ${GO} test ./pkg/segmenter @echo ... test pkg/chromaprint - @PKG_CONFIG_PATH="$(shell realpath ${PREFIX})/lib/pkgconfig" CGO_LDFLAGS_ALLOW="-(W|D).*" ${GO} test ./pkg/chromaprint + @${CGO_ENV} ${GO} test ./pkg/chromaprint ############################################################################### # DEPENDENCIES, ETC diff --git a/error.go b/error.go index db9328e..80e9b24 100644 --- a/error.go +++ b/error.go @@ -14,8 +14,9 @@ type Err uint // GLOBALS const ( - ErrBadParameter Err = http.StatusBadRequest - ErrInternalError Err = http.StatusInternalServerError + ErrBadParameter Err = http.StatusBadRequest + ErrInternalError Err = http.StatusInternalServerError + ErrNotImplemented Err = http.StatusNotImplemented ) /////////////////////////////////////////////////////////////////////////////// @@ -31,6 +32,8 @@ func (code Err) Error() string { return "bad parameter" case ErrInternalError: return "internal error" + case ErrNotImplemented: + return "not implemented" default: return fmt.Sprintf("error code %d", code.Code()) } diff --git a/manager.go b/manager.go index 2747157..8935075 100644 --- a/manager.go +++ b/manager.go @@ -5,7 +5,10 @@ functions to determine capabilities and manage media files and devices. */ package media -import "io" +import ( + "context" + "io" +) // Manager represents a manager for media formats and devices. // Create a new manager object using the NewManager function. @@ -34,36 +37,36 @@ type Manager interface { // 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, ...string) (Media, error) + Read(io.Reader, Format, ...string) (Media, error) // 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 or else // the format is guessed from the path. If no parameters are provided, // then the default parameters for the format are used. - //Create(string, Format, []Metadata, ...Parameters) (Media, error) + Create(string, Format, []Metadata, ...Par) (Media, error) // Create a media stream for writing. The format will be used to // determine the format and one or more CodecParameters used to // create the streams. If no parameters are provided, then the // default parameters for the format are used. It is the responsibility // of the caller to also close the writer when done. - //Write(io.Writer, Format, []Metadata, ...Parameters) (Media, error) + //Write(io.Writer, Format, []Metadata, ...Par) (Media, error) // Return audio parameters for encoding // ChannelLayout, SampleFormat, Samplerate - //AudioParameters(string, string, int) (Parameters, error) + //AudioPar(string, string, int) (Par, error) // Return video parameters for encoding // Width, Height, PixelFormat - //VideoParameters(int, int, string) (Parameters, error) + //VideoPar(int, int, string) (Par, error) // Return codec parameters for audio encoding // Codec name and AudioParameters - //AudioCodecParameters(string, AudioParameters) (Parameters, error) + //AudioCodecParameters(string, AudioPar) (Par, error) // Return codec parameters for video encoding // Codec name, Profile name, Framerate (fps) and VideoParameters - //VideoCodecParameters(string, string, float64, VideoParameters) (Parameters, error) + //VideoCodecParameters(string, string, float64, VideoPar) (Par, error) // Return supported input and output container formats which match any filter, // which can be a name, extension (with preceeding period) or mimetype. The Type @@ -95,9 +98,30 @@ type Manager interface { // Log info messages with arguments Infof(string, ...any) + + // Decode an input stream, determining the streams to be decoded + // and the function to accept the decoded frames. If MapFunc is nil, + // all streams are passed through (demultiplexing). + Decode(context.Context, Media, MapFunc, FrameFunc) error } -// A container format for a media file or stream +// MapFunc return parameters if a stream should be decoded, +// resampled (for audio streams) or resized (for video streams). +// Return nil if you want to ignore the stream, or pass back the +// stream parameters if you want to copy the stream without any changes. +type MapFunc func(int, Par) (Par, error) + +// FrameFunc is a function which is called to send a frame after decoding. It should +// return nil to continue decoding or io.EOF to stop. +type FrameFunc func(int, Frame) error + +// Parameters for a stream or frame +type Par interface{} + +// A frame of decoded data +type Frame interface{} + +// A container format for a media file type Format interface { // The type of the format, which can be combinations of // INPUT, OUTPUT, DEVICE, AUDIO, VIDEO and SUBTITLE diff --git a/pkg/ffmpeg/manager.go b/pkg/ffmpeg/manager.go index d8a4fa1..7fbb697 100644 --- a/pkg/ffmpeg/manager.go +++ b/pkg/ffmpeg/manager.go @@ -1,6 +1,8 @@ package ffmpeg import ( + "context" + "io" "slices" "strings" @@ -69,35 +71,25 @@ func NewManager(opt ...Opt) (*Manager, error) { return manager, nil } -// 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. You can add additional options to the open call as -// key=value pairs -/* +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - READER + func (manager *Manager) Open(url string, format media.Format, opts ...string) (media.Media, error) { - opt := append([]Opt{OptInputOpt(opts...)}, manager.opts...) + o := append([]Opt{}, manager.opts[:]...) if format != nil { - opt = append(opt, OptInputFormat(format.Name())) + if format_, ok := format.(*Format); ok && format_.Input != nil { + o = append(o, optInputFormat(format_)) + } else { + return nil, media.ErrBadParameter.With("invalid input format") + } } - return Open(url, opt...) -} - -// Open an io.Reader for reading. If a format is specified, then the -// format will be used to open the file. You can add additional options -// to the open call as key=value pairs -func (manager *Manager) NewReader(r io.Reader, format media.Format, opts ...string) (media.Media, error) { - opt := append([]Opt{OptInputOpt(opts...)}, manager.opts...) - if format != nil { - opt = append(opt, OptInputFormat(format.Name())) + if len(opts) > 0 { + o = append(o, OptInputOpt(opts...)) } - return NewReader(r, opt...) + return Open(url, o...) } -*/ - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - READER -func (manager *Manager) Open(url string, format media.Format, opts ...string) (media.Media, error) { +func (manager *Manager) Read(r io.Reader, format media.Format, opts ...string) (media.Media, error) { o := append([]Opt{}, manager.opts[:]...) if format != nil { if format_, ok := format.(*Format); ok && format_.Input != nil { @@ -109,7 +101,40 @@ func (manager *Manager) Open(url string, format media.Format, opts ...string) (m if len(opts) > 0 { o = append(o, OptInputOpt(opts...)) } - return Open(url, o...) + return NewReader(r, o...) +} + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - WRITER + +func (manager *Manager) Create(url string, format media.Format, meta []media.Metadata, streams ...media.Par) (media.Media, error) { + o := slices.Clone(manager.opts[:]) + + // Append format + if format != nil { + if format_, ok := format.(*Format); ok && format_.Output != nil { + o = append(o, optOutputFormat(format_)) + } else { + return nil, media.ErrBadParameter.With("invalid output format") + } + } + + // Append metadata + for _, m := range meta { + o = append(o, OptMetadata(NewMetadata(m.Key(), m.Any()))) + } + + // Append streams + for i, stream := range streams { + par, ok := stream.(*Par) + if !ok || par == nil { + return nil, media.ErrBadParameter.With("invalid stream parameters") + } + o = append(o, OptStream(i, par)) + } + + // Create the writer + return Create(url, o...) } /////////////////////////////////////////////////////////////////////////////// @@ -407,3 +432,25 @@ func (manager *Manager) Warningf(v string, args ...any) { func (manager *Manager) Infof(v string, args ...any) { ff.AVUtil_log(nil, ff.AV_LOG_INFO, v, args...) } + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - DECODING + +func (manager *Manager) Decode(ctx context.Context, m media.Media, mapFunc media.MapFunc, frameFunc media.FrameFunc) error { + return media.ErrNotImplemented.With("decoding not implemented") +} + +/* + // Check if the media is valid + if m == nil || !m.Type().Is(media.INPUT) { + return media.ErrBadParameter.With("invalid media, cannot decode") + } + // Get the concrete reader object + reader, ok := m.(*Reader) + if !ok || reader == nil { + return media.ErrBadParameter.With("invalid media, cannot decode") + } + // Perform the decode + return reader.Decode(ctx, mapFunc, frameFunc) +} +*/ diff --git a/pkg/ffmpeg/opts.go b/pkg/ffmpeg/opts.go index 899744c..02e6477 100644 --- a/pkg/ffmpeg/opts.go +++ b/pkg/ffmpeg/opts.go @@ -101,6 +101,18 @@ func optInputFormat(format *Format) Opt { } } +// Output format from ff.AVInputFormat +func optOutputFormat(format *Format) Opt { + return func(o *opts) error { + if format != nil && format.Output != nil { + o.oformat = format.Output + } else { + return ErrBadParameter.With("invalid output format") + } + return nil + } +} + // Input format options func OptInputOpt(opt ...string) Opt { return func(o *opts) error { diff --git a/pkg/ffmpeg/writer.go b/pkg/ffmpeg/writer.go index 877292d..44de2c3 100644 --- a/pkg/ffmpeg/writer.go +++ b/pkg/ffmpeg/writer.go @@ -10,6 +10,7 @@ import ( "sort" // Packages + "github.com/mutablelogic/go-media" ff "github.com/mutablelogic/go-media/sys/ffmpeg71" maps "golang.org/x/exp/maps" @@ -227,6 +228,11 @@ func (w *Writer) String() string { ////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS +// Return a "stream" for encoding +func (w *Writer) Type() media.Type { + return media.OUTPUT +} + // Return a "stream" for encoding func (w *Writer) Stream(stream int) *Encoder { for _, encoder := range w.encoders { diff --git a/sys/chromaprint/chromaprint.go b/sys/chromaprint/chromaprint.go index ca1c74f..086e58f 100644 --- a/sys/chromaprint/chromaprint.go +++ b/sys/chromaprint/chromaprint.go @@ -11,7 +11,6 @@ import ( /* #cgo pkg-config: libchromaprint -#cgo LDFLAGS: -lstdc++ #cgo darwin LDFLAGS: -framework Accelerate #include */ From b3f09fde22ae4ce4fecddf4c28ff13f5b8a14170 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Mon, 19 May 2025 09:24:02 +0200 Subject: [PATCH 5/6] Updated --- manager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manager.go b/manager.go index 8935075..a9ae721 100644 --- a/manager.go +++ b/manager.go @@ -140,6 +140,6 @@ type Media interface { io.Closer // The type of the format, which can be combinations of - // INPUT, OUTPUT, DEVICE + // INPUT or OUTPUT Type() Type } From 70374be9bc7585fee5b2efb2d3006b8fac56773d Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Mon, 19 May 2025 09:57:51 +0200 Subject: [PATCH 6/6] Updated --- Makefile | 10 ++++++---- pkg/generator/sine.go | 2 +- pkg/generator/yuv420p.go | 2 +- sys/chromaprint/chromaprint.go | 3 ++- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 5fd2043..06c2b5f 100755 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ FFMPEG_VERSION=ffmpeg-7.1.1 CHROMAPRINT_VERSION=chromaprint-1.5.1 # CGO configuration - set CGO vars for C++ libraries -CGO_ENV=PKG_CONFIG_PATH="$(shell realpath ${PREFIX})/lib/pkgconfig" CGO_LDFLAGS_ALLOW="-(W|D).*" CGO_LDFLAGS="-lstdc++" +CGO_ENV=PKG_CONFIG_PATH="$(shell realpath ${PREFIX})/lib/pkgconfig" CGO_LDFLAGS_ALLOW="-(W|D).*" CGO_LDFLAGS="-lstdc++ -lavutil" # Build flags BUILD_MODULE := $(shell cat go.mod | head -1 | cut -d ' ' -f 2) @@ -87,7 +87,7 @@ ffmpeg: ffmpeg-build ############################################################################### # CHROMAPRINT -# Download ffmpeg sources +# Download chromaprint sources ${BUILD_DIR}/${CHROMAPRINT_VERSION}: @if [ ! -d "$(BUILD_DIR)/$(CHROMAPRINT_VERSION)" ]; then \ echo "Downloading $(CHROMAPRINT_VERSION)"; \ @@ -100,7 +100,7 @@ ${BUILD_DIR}/${CHROMAPRINT_VERSION}: # Configure chromaprint .PHONY: chromaprint-configure -chromaprint-configure: mkdir ${BUILD_DIR}/${CHROMAPRINT_VERSION} +chromaprint-configure: mkdir ${BUILD_DIR}/${CHROMAPRINT_VERSION} ffmpeg @echo "Configuring ${CHROMAPRINT_VERSION} => ${PREFIX}" cmake \ -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ @@ -108,6 +108,8 @@ chromaprint-configure: mkdir ${BUILD_DIR}/${CHROMAPRINT_VERSION} -DBUILD_SHARED_LIBS=0 \ -DBUILD_TESTS=0 \ -DBUILD_TOOLS=0 \ + -DFFT_LIB=avfft \ + -DCMAKE_PREFIX_PATH="$(shell realpath ${PREFIX})" \ --install-prefix "$(shell realpath ${PREFIX})" \ -S ${BUILD_DIR}/${CHROMAPRINT_VERSION} \ -B ${BUILD_DIR} @@ -124,7 +126,7 @@ chromaprint-build: chromaprint-configure chromaprint: chromaprint-build @echo "Installing ${CHROMAPRINT_VERSION} => ${PREFIX}" @cd $(BUILD_DIR) && make install - @sed -i.bak 's/Libs: -L\${libdir} -lchromaprint/Libs: -L\${libdir} -lchromaprint -lstdc++/g' "${PREFIX}/lib/pkgconfig/libchromaprint.pc" + @sed -i.bak 's/Libs: -L\${libdir} -lchromaprint/Libs: -L\${libdir} -lchromaprint -lstdc++ -lavutil/g' "${PREFIX}/lib/pkgconfig/libchromaprint.pc" @rm -f "${PREFIX}/lib/pkgconfig/libchromaprint.pc.bak" ############################################################################### diff --git a/pkg/generator/sine.go b/pkg/generator/sine.go index 32ce53a..cecb777 100644 --- a/pkg/generator/sine.go +++ b/pkg/generator/sine.go @@ -7,7 +7,7 @@ import ( "time" // Packages - "github.com/mutablelogic/go-media" + media "github.com/mutablelogic/go-media" ffmpeg "github.com/mutablelogic/go-media/pkg/ffmpeg" ff "github.com/mutablelogic/go-media/sys/ffmpeg71" ) diff --git a/pkg/generator/yuv420p.go b/pkg/generator/yuv420p.go index 87c8065..6e3afb4 100644 --- a/pkg/generator/yuv420p.go +++ b/pkg/generator/yuv420p.go @@ -5,7 +5,7 @@ import ( "errors" // Packages - "github.com/mutablelogic/go-media" + media "github.com/mutablelogic/go-media" ffmpeg "github.com/mutablelogic/go-media/pkg/ffmpeg" ff "github.com/mutablelogic/go-media/sys/ffmpeg71" ) diff --git a/sys/chromaprint/chromaprint.go b/sys/chromaprint/chromaprint.go index 086e58f..28ce477 100644 --- a/sys/chromaprint/chromaprint.go +++ b/sys/chromaprint/chromaprint.go @@ -10,7 +10,8 @@ import ( // CGO /* -#cgo pkg-config: libchromaprint +#cgo pkg-config: libchromaprint libavcodec +#cgo LDFLAGS: -lstdc++ #cgo darwin LDFLAGS: -framework Accelerate #include */