Skip to content

Added AVFilter #47

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jun 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 25 additions & 19 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Paths to packages
GO=$(shell which go)
DOCKER=$(shell which docker)
PKG_CONFIG=$(shell which pkg-config)

# Source version
FFMPEG_VERSION=ffmpeg-7.1.1
Expand Down Expand Up @@ -63,13 +64,12 @@ ${BUILD_DIR}/${FFMPEG_VERSION}:

# Configure ffmpeg
.PHONY: ffmpeg-configure
ffmpeg-configure: mkdir ${BUILD_DIR}/${FFMPEG_VERSION} ffmpeg-dep
ffmpeg-configure: mkdir pkconfig-dep ${BUILD_DIR}/${FFMPEG_VERSION} ffmpeg-dep
@echo "Configuring ${FFMPEG_VERSION} => ${PREFIX}"
@cd ${BUILD_DIR}/${FFMPEG_VERSION} && ./configure \
--enable-static --disable-doc --disable-programs \
--disable-doc --disable-programs \
--prefix="$(shell realpath ${PREFIX})" \
--pkg-config-flags="--static" \
--extra-libs="-lpthread" \
--enable-static --pkg-config="${PKG_CONFIG}" --pkg-config-flags="--static" --extra-libs="-lpthread" \
--enable-gpl --enable-nonfree ${FFMPEG_CONFIG}

# Build ffmpeg
Expand Down Expand Up @@ -161,14 +161,12 @@ test-ffmpeg: go-dep go-tidy ffmpeg chromaprint
@${CGO_ENV} ${GO} test ./pkg/segmenter
@echo ... test pkg/chromaprint
@${CGO_ENV} ${GO} test ./pkg/chromaprint
@echo ... test pkg/avcodec
${CGO_ENV} ${GO} test ./pkg/avcodec


# @echo ... test pkg/ffmpeg
# @${GO} test -v ./pkg/ffmpeg
# @echo ... test sys/chromaprint
# @${GO} test ./sys/chromaprint
# @echo ... test pkg/chromaprint
# @${GO} test ./pkg/chromaprint
# @echo ... test pkg/file
# @${GO} test ./pkg/file
# @echo ... test pkg/generator
Expand Down Expand Up @@ -198,6 +196,11 @@ go-dep:
docker-dep:
@test -f "${DOCKER}" && test -x "${DOCKER}" || (echo "Missing docker binary" && exit 1)

.PHONY: pkconfig-dep
pkconfig-dep:
@test -f "${PKG_CONFIG}" && test -x "${PKG_CONFIG}" || (echo "Missing pkg-config binary" && exit 1)


.PHONY: mkdir
mkdir:
@echo Mkdir ${BUILD_DIR}
Expand All @@ -218,15 +221,18 @@ clean: go-tidy
# Check for FFmpeg dependencies
.PHONY: ffmpeg-dep
ffmpeg-dep:
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists libass && echo "--enable-libass"))
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists fdk-aac && echo "--enable-libfdk-aac"))
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists lame && echo "--enable-libmp3lame"))
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists freetype2 && echo "--enable-libfreetype"))
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists theora && echo "--enable-libtheora"))
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists vorbis && echo "--enable-libvorbis"))
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists opus && echo "--enable-libopus"))
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists x264 && echo "--enable-libx264"))
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists x265 && echo "--enable-libx265"))
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists xvid && echo "--enable-libxvid"))
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell pkg-config --exists vpx && echo "--enable-libvpx"))
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists libass && echo "--enable-libass"))
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists fdk-aac && echo "--enable-libfdk-aac"))
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists lame && echo "--enable-libmp3lame"))
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists freetype2 && echo "--enable-libfreetype"))
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists vorbis && echo "--enable-libvorbis"))
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists opus && echo "--enable-libopus"))
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists x264 && echo "--enable-libx264"))
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists x265 && echo "--enable-libx265"))
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists xvid && echo "--enable-libxvid"))
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists vpx && echo "--enable-libvpx"))
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists libgcrypt && echo "--enable-gcrypt"))
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists aom && echo "--enable-libaom"))
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists libbluray && echo "--enable-libbluray"))
$(eval FFMPEG_CONFIG := $(FFMPEG_CONFIG) $(shell ${PKG_CONFIG} --exists dav1d && echo "--enable-libdav1d"))
@echo "FFmpeg configuration: $(FFMPEG_CONFIG)"
281 changes: 281 additions & 0 deletions pkg/avcodec/codec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
package avcodec

import (
"encoding/json"
"fmt"

// Packages
media "github.com/mutablelogic/go-media"
metadata "github.com/mutablelogic/go-media/pkg/metadata"
ff "github.com/mutablelogic/go-media/sys/ffmpeg71"
)

///////////////////////////////////////////////////////////////////////////////
// TYPES

type Codec struct {
codec *ff.AVCodec
context *ff.AVCodecContext
}

///////////////////////////////////////////////////////////////////////////////
// LIFECYCLE

// Return all codecs. Codecs can be either encoders or decoders. The argument
// can be ANY or any combination of INPUT, OUTPUT, VIDEO, AUDIO and SUBTITLE,
// in order to return a subset of codecs.
func Codecs(t media.Type) []media.Metadata {
result := make([]media.Metadata, 0, 100)
var opaque uintptr
for {
codec := ff.AVCodec_iterate(&opaque)
if codec == nil {
break
}
// Filter codecs by type
if t.Is(media.INPUT) && !ff.AVCodec_is_decoder(codec) {
continue
}
if t.Is(media.OUTPUT) && !ff.AVCodec_is_encoder(codec) {
continue
}
if t.Is(media.VIDEO) && codec.Type() != ff.AVMEDIA_TYPE_VIDEO {
continue
}
if t.Is(media.AUDIO) && codec.Type() != ff.AVMEDIA_TYPE_AUDIO {
continue
}
if t.Is(media.SUBTITLE) && codec.Type() != ff.AVMEDIA_TYPE_SUBTITLE {
continue
}
if codec.Capabilities().Is(ff.AV_CODEC_CAP_EXPERIMENTAL) {
// Skip experimental codecs
continue
}
result = append(result, metadata.New(codec.Name(), &Codec{codec, nil}))
}
return result
}

// NewEncodingCodec returns an encoder by name. Options for encoding can be passed.
// Call Close() to release the codec. Codec options are listed at
// <https://ffmpeg.org/ffmpeg-codecs.html>
func NewEncodingCodec(name string, opts ...Opt) (*Codec, error) {
return newCodec(ff.AVCodec_find_encoder_by_name(name), opts...)
}

// NewDecodingCodec returns a decoder by name. Options for decoding can be passed.
// Call Close() to release the codec context. Codec options are listed at
// <https://ffmpeg.org/ffmpeg-codecs.html>
func NewDecodingCodec(name string, opts ...Opt) (*Codec, error) {
return newCodec(ff.AVCodec_find_decoder_by_name(name), opts...)
}

func newCodec(codec *ff.AVCodec, opts ...Opt) (*Codec, error) {
ctx := new(Codec)

// Check parameters
if codec == nil {
return nil, media.ErrBadParameter.Withf("unknown codec %q", codec.Name())
}

// Apply options
o, err := applyOptions(opts)
if err != nil {
return nil, err
}

// Create a dictionary of codec options
dict := ff.AVUtil_dict_alloc()
defer ff.AVUtil_dict_free(dict)
for _, opt := range o.meta {
if err := ff.AVUtil_dict_set(dict, opt.Key(), opt.Value(), ff.AV_DICT_APPEND); err != nil {
ff.AVUtil_dict_free(dict)
return nil, err
}
}

// Codec context
if context := ff.AVCodec_alloc_context(codec); context == nil {
return nil, media.ErrInternalError.Withf("failed to allocate codec context for %q", codec.Name())
} else if err := set_par(context, codec, o); err != nil {
ff.AVCodec_free_context(context)
return nil, err
} else if err := ff.AVCodec_open(context, codec, dict); err != nil {
ff.AVCodec_free_context(context)
return nil, err
} else {
ctx.context = context
}

// TODO: Get the options which were not consumed
fmt.Println("TODO: Codec options not consumed:", dict)

// Return success
return ctx, nil
}

// Release the codec resources
func (ctx *Codec) Close() error {
ctx.codec = nil
if ctx != nil && ctx.context != nil {
ff.AVCodec_free_context(ctx.context)
ctx.context = nil
}
return nil
}

////////////////////////////////////////////////////////////////////////////////
// STRINGIFY

func (ctx *Codec) MarshalJSON() ([]byte, error) {
if ctx != nil && ctx.codec != nil {
return ctx.codec.MarshalJSON()
}
if ctx != nil && ctx.context != nil {
return ctx.context.MarshalJSON()
}
return []byte("null"), nil
}

func (ctx *Codec) String() string {
data, err := json.MarshalIndent(ctx, "", " ")
if err != nil {
return err.Error()
}
return string(data)
}

////////////////////////////////////////////////////////////////////////////////
// PUBLIC METHODS

func (ctx *Codec) Type() media.Type {
var t media.Type
switch {
case ctx != nil && ctx.codec != nil:
if ff.AVCodec_is_decoder(ctx.codec) {
t |= media.INPUT
}
if ff.AVCodec_is_encoder(ctx.codec) {
t |= media.OUTPUT
}
t |= type2type(ctx.codec.Type())
case ctx != nil && ctx.context != nil:
if ff.AVCodec_is_decoder(ctx.context.Codec()) {
t |= media.INPUT
}
if ff.AVCodec_is_encoder(ctx.context.Codec()) {
t |= media.OUTPUT
}
t |= type2type(ctx.context.Codec().Type())
}
return t
}

func (ctx *Codec) Name() string {
switch {
case ctx != nil && ctx.codec != nil:
return ctx.codec.Name()
case ctx != nil && ctx.context != nil:
return ctx.context.Codec().Name()
}
return ""
}

////////////////////////////////////////////////////////////////////////////////
// PRIVATE METHODS

func type2type(t ff.AVMediaType) media.Type {
switch t {
case ff.AVMEDIA_TYPE_AUDIO:
return media.AUDIO
case ff.AVMEDIA_TYPE_VIDEO:
return media.VIDEO
case ff.AVMEDIA_TYPE_SUBTITLE:
return media.SUBTITLE
case ff.AVMEDIA_TYPE_ATTACHMENT:
return media.DATA
case ff.AVMEDIA_TYPE_DATA:
return media.DATA
}
return media.NONE
}

func set_par(ctx *ff.AVCodecContext, codec *ff.AVCodec, opt *opt) error {
switch codec.Type() {
case ff.AVMEDIA_TYPE_AUDIO:
return set_audio_par(ctx, codec, opt)
case ff.AVMEDIA_TYPE_VIDEO:
return set_video_par(ctx, codec, opt)
case ff.AVMEDIA_TYPE_SUBTITLE:
return set_subtitle_par(ctx, codec, opt)
}
return nil
}

func set_audio_par(ctx *ff.AVCodecContext, codec *ff.AVCodec, opt *opt) error {
// Channel layout
if ff.AVUtil_channel_layout_check(&opt.channel_layout) {
ctx.SetChannelLayout(opt.channel_layout)
} else if supported_layouts := codec.ChannelLayouts(); len(supported_layouts) > 0 {
ctx.SetChannelLayout(supported_layouts[0])
} else {
ctx.SetChannelLayout(ff.AV_CHANNEL_LAYOUT_MONO)
}

// Sample format
if opt.sample_format != ff.AV_SAMPLE_FMT_NONE {
ctx.SetSampleFormat(opt.sample_format)
} else if supported_formats := codec.SampleFormats(); len(supported_formats) > 0 {
ctx.SetSampleFormat(supported_formats[0])
}

// Sample rate
if opt.sample_rate > 0 {
ctx.SetSampleRate(opt.sample_rate)
} else if supported_rates := codec.SupportedSamplerates(); len(supported_rates) > 0 {
ctx.SetSampleRate(supported_rates[0])
}

// TODO: Time base
ctx.SetTimeBase(ff.AVUtil_rational(1, ctx.SampleRate()))

return nil
}

func set_video_par(ctx *ff.AVCodecContext, codec *ff.AVCodec, opt *opt) error {
// Pixel Format
if opt.pixel_format != ff.AV_PIX_FMT_NONE {
ctx.SetPixFmt(opt.pixel_format)
} else if supported_formats := codec.PixelFormats(); len(supported_formats) > 0 {
ctx.SetPixFmt(supported_formats[0])
} else {
ctx.SetPixFmt(ff.AV_PIX_FMT_YUV420P)
}

// Frame size
if opt.width > 0 {
ctx.SetWidth(opt.width)
}
if opt.height > 0 {
ctx.SetHeight(opt.height)
}

// Frame rate
if !opt.frame_rate.IsZero() {
ctx.SetFramerate(opt.frame_rate)
} else if supported_rates := codec.SupportedFramerates(); len(supported_rates) > 0 {
ctx.SetFramerate(supported_rates[0])
}

// Time base
if frame_rate := ctx.Framerate(); !frame_rate.IsZero() {
ctx.SetTimeBase(ff.AVUtil_rational_invert(frame_rate))
}

return nil
}

func set_subtitle_par(ctx *ff.AVCodecContext, codec *ff.AVCodec, opt *opt) error {
return nil
}
Loading
Loading