diff --git a/Makefile b/Makefile index b818aab..5b06f42 100755 --- a/Makefile +++ b/Makefile @@ -2,55 +2,63 @@ GO=$(shell which go) DOCKER=$(shell which docker) -# Paths to locations, etc -BUILD_DIR := "build" -CMD_DIR := $(filter-out cmd/README.md, $(wildcard cmd/*)) - # Build flags -BUILD_MODULE := $(shell go list -m) -BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/config.GitSource=${BUILD_MODULE} -BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/config.GitTag=$(shell git describe --tags) -BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/config.GitBranch=$(shell git name-rev HEAD --name-only --always) -BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/config.GitHash=$(shell git rev-parse HEAD) -BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/config.GoBuildTime=$(shell date -u '+%Y-%m-%dT%H:%M:%SZ') +BUILD_MODULE := $(shell cat go.mod | head -1 | cut -d ' ' -f 2) +BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/version.GitSource=${BUILD_MODULE} +BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/version.GitTag=$(shell git describe --tags --always) +BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/version.GitBranch=$(shell git name-rev HEAD --name-only --always) +BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/version.GitHash=$(shell git rev-parse HEAD) +BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/version.GoBuildTime=$(shell date -u '+%Y-%m-%dT%H:%M:%SZ') BUILD_FLAGS = -ldflags "-s -w $(BUILD_LD_FLAGS)" -all: clean test cmd - -cmd: clean dependencies $(CMD_DIR) +# Set OS and Architecture +ARCH ?= $(shell arch | tr A-Z a-z | sed 's/x86_64/amd64/' | sed 's/i386/amd64/' | sed 's/armv7l/arm/' | sed 's/aarch64/arm64/') +OS ?= $(shell uname | tr A-Z a-z) +VERSION ?= $(shell git describe --tags --always | sed 's/^v//') +DOCKER_REGISTRY ?= ghcr.io/mutablelogic -$(CMD_DIR): FORCE - @echo Build cmd $(notdir $@) - @${GO} build -o ${BUILD_DIR}/$(notdir $@) ${BUILD_FLAGS} ./$@ +# Paths to locations, etc +BUILD_DIR := "build" +CMD_DIR := $(filter-out cmd/ffmpeg/README.md, $(wildcard cmd/ffmpeg/*)) +BUILD_TAG := ${DOCKER_REGISTRY}/go-media-${OS}-${ARCH}:${VERSION} -$(PLUGIN_DIR): FORCE - @echo Build plugin $(notdir $@) - @${GO} build -buildmode=plugin -o ${BUILD_DIR}/$(notdir $@).plugin ${BUILD_FLAGS} ./$@ +all: clean cmds -FORCE: +cmds: $(CMD_DIR) -docker: - @echo Build docker image +docker: docker-dep + @echo build docker image: ${BUILD_TAG} for ${OS}/${ARCH} @${DOCKER} build \ - --tag go-media:$(shell git describe --tags) \ - --build-arg PLATFORM=$(shell ${GO} env GOOS) \ - --build-arg ARCH=$(shell ${GO} env GOARCH) \ - --build-arg VERSION=kinetic \ + --tag ${BUILD_TAG} \ + --build-arg ARCH=${ARCH} \ + --build-arg OS=${OS} \ + --build-arg SOURCE=${BUILD_MODULE} \ + --build-arg VERSION=${VERSION} \ -f etc/docker/Dockerfile . -test: clean dependencies - @echo Test sys/ - @${GO} test ./sys/... - @echo Test pkg/ - @${GO} test ./pkg/... +docker-push: docker-dep + @echo push docker image: ${BUILD_TAG} + @${DOCKER} push ${BUILD_TAG} + +test: go-dep + @echo Test + @${GO} mod tidy + @${GO} test ./sys/ffmpeg61 + +$(CMD_DIR): go-dep mkdir + @echo Build cmd $(notdir $@) + @${GO} build ${BUILD_FLAGS} -o ${BUILD_DIR}/$(notdir $@) ./$@ + +FORCE: + +go-dep: + @test -f "${GO}" && test -x "${GO}" || (echo "Missing go binary" && exit 1) -dependencies: mkdir -ifeq (,${GO}) - $(error "Missing go binary") -endif +docker-dep: + @test -f "${DOCKER}" && test -x "${DOCKER}" || (echo "Missing docker binary" && exit 1) mkdir: - @echo Mkdir + @echo Mkdir ${BUILD_DIR} @install -d ${BUILD_DIR} clean: diff --git a/README.md b/README.md index eb82fb3..b1cf141 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,11 @@ This module provides an interface for media services, including: - * Bindings in golang for [ffmpeg 5.1](https://ffmpeg.org/); - * Opening media files, devices and network sockets for reading - and writing; - * Retrieving metadata and artwork from audio and video media; - * Re-multiplexing media files from one format to another; - * Fingerprinting audio files to identify music. +* Bindings in golang for [ffmpeg 6](https://ffmpeg.org/); +* Opening media files, devices and network sockets for reading and writing; +* Retrieving metadata and artwork from audio and video media; +* Re-multiplexing media files from one format to another; +* Fingerprinting audio files to identify music. ## Current Status @@ -17,66 +16,48 @@ you are interested in, please see below "Contributing & Distribution" below. ## Requirements -In order to build the examples, you'll need the library and header files for [ffmpeg 5.1](https://ffmpeg.org/download.html) installed. The `chromaprint` library is also required for fingerprinting audio files. - -On Macintosh with [homebrew](http://bew.sh/), for example: +In order to build the examples, you'll need the library and header files for [ffmpeg 6](https://ffmpeg.org/download.html) installed. +The `chromaprint` library is also required for fingerprinting audio files. On Macintosh with [homebrew](http://bew.sh/), for example: ```bash -brew install ffmpeg chromaprint make +brew install ffmpeg@6 chromaprint make ``` There are some examples in the `cmd` folder of the main repository on how to use the package. The various make targets are: - * `make all` will perform tests, build all examples and the backend API; - * `make test` will perform tests; - * `make cmd` will build example command-line tools into the `build` folder; - * `make clean` will remove all build artifacts. +* `make all` will perform tests, build all examples and the backend API; +* `make test` will perform tests; +* `make cmd` will build example command-line tools into the `build` folder; +* `make clean` will remove all build artifacts. + +There are also some targets to build a docker image: + +* `DOCKER_REGISTRY=docker.io/user make docker` will build a docker image; +* `DOCKER_REGISTRY=docker.io/user make docker-push` will push the docker image to the registry. For example, ```bash git clone git@github.com:djthorpe/go-media.git cd go-media -make +DOCKER_REGISTRY=ghcr.io/mutablelogic make docker ``` ## Examples -There are two example [Command Line applications](https://github.com/mutablelogic/go-media/tree/master/cmd): - - * `extractartwork` can be used to walk through a directory and extract artwork from media - files and save the artwork into files; - * `transcode` can be used to copy, re-mux and re-sample media files from one format to another. - -You can compile both applications with `make cmd`which places the binaries into the `build` folder. -Use the `-help` option on either application to see the options. - - ## Media Transcoding -You can programmatically demultiplex, re-multiplex and re-sample media files using the following packages: - - * `sys/ffmpeg51` provides the implementation of the lower-level function calls - to ffmpeg. The documentation is [here](https://pkg.go.dev/github.com/mutablelogic/go-media/sys/ffmpeg51) - * `pkg/media` provides the higher-level API for opening media files, reading, - transcoding, resampling and writing media files. The interfaces and documentation - are best read here: - * [Audio](https://github.com/mutablelogic/go-media/blob/master/audio.go) - * [Video](https://github.com/mutablelogic/go-media/blob/master/video.go) - * [Media](https://github.com/mutablelogic/go-media/blob/master/media.go) - * And [here](https://pkg.go.dev/github.com/mutablelogic/go-media/) - ## Audio Fingerprinting You can programmatically fingerprint audio files, compare fingerprints and identify music using the following packages: - * `sys/chromaprint` provides the implementation of the lower-level function calls - to chromaprint. The documentation is [here](https://pkg.go.dev/github.com/mutablelogic/go-media/sys/chromaprint) - * `pkg/chromaprint` provides the higher-level API for fingerprinting and identifying music. The documentation - is [here](https://pkg.go.dev/github.com/mutablelogic/go-media/pkg/chromaprint). +* `sys/chromaprint` provides the implementation of the lower-level function calls + to chromaprint. The documentation is [here](https://pkg.go.dev/github.com/mutablelogic/go-media/sys/chromaprint) +* `pkg/chromaprint` provides the higher-level API for fingerprinting and identifying music. The documentation + is [here](https://pkg.go.dev/github.com/mutablelogic/go-media/pkg/chromaprint). -You'll need an API key in order to use the [AcoustID](https://acoustid.org/) service. You can get a key +You'll need an API key in order to use the [AcoustID](https://acoustid.org/) service. You can get a key [here](https://acoustid.org/login). ## Contributing & Distribution @@ -88,9 +69,9 @@ The license is Apache 2 so feel free to redistribute. Redistributions in either code or binary form must reproduce the copyright notice, and please link back to this repository for more information: -> Copyright (c) 2021-2023 David Thorpe, All rights reserved. +> Copyright (c) 2021-2024 David Thorpe, All rights reserved. ## References - * https://ffmpeg.org/doxygen/5.1/index.html + * https://ffmpeg.org/doxygen/6.1/index.html diff --git a/_old/audio.go b/_old/audio.go index d8e8c75..6375b91 100755 --- a/_old/audio.go +++ b/_old/audio.go @@ -1,72 +1,353 @@ package media +import ( + "fmt" + "io" +) + //////////////////////////////////////////////////////////////////////////////// // TYPES -// AudioFormat represents how the samples are stored -type AudioFormat int +// SampleFormat specifies the type of a single sample +type SampleFormat uint + +// ChannelLayout specifies the layout of channels +type ChannelLayout uint + +// AudioChannel specifies a single audio channel +type AudioChannel uint + +// AudioFormat specifies the interface for audio format +type AudioFormat struct { + // Sample rate in Hz + Rate uint -// AudioChannelLayout represents number of channels and layout of those channels -type AudioChannelLayout struct { - Channels uint + // Sample format + Format SampleFormat + + // Channel layout + Layout ChannelLayout } +// SWResampleFn is a function that accepts an "output" audio frame, +// which can be nil if the conversion has not started yet, and should +// fill the audio frame provided to the Convert function. Should return +// io.EOF on end of conversion, or any other error to stop the conversion. +type SWResampleFn func(AudioFrame) error + //////////////////////////////////////////////////////////////////////////////// -// GLOBALS +// INTERFACES + +/* +// AudioFrame is a slice of audio samples +type AudioFrame interface { + io.Closer + + // Audio format + AudioFormat() AudioFormat + + // Number of samples in a single channel + Samples() int + + // Audio channels + Channels() []AudioChannel + + // Duration of the frame + Duration() time.Duration + + // Returns true if planar format (one set of samples per channel) + IsPlanar() bool + + // Returns the samples for a specified channel, as array of bytes. For packed + // audio format, the channel should be 0. + Bytes(channel int) []byte +} +*/ + +// SWResample is an interface to the ffmpeg swresample library +// which resamples audio. +type SWResample interface { + io.Closer + + // Create a new empty context object for conversion, with an input frame which + // will be used to store the data and the target output format. + Convert(AudioFrame, AudioFormat, SWResampleFn) error +} + +//////////////////////////////////////////////////////////////////////////////// +// CONSTANTS const ( - AUDIO_FMT_NONE AudioFormat = iota - AUDIO_FMT_U8 // unsigned 8 bits - AUDIO_FMT_U8P // unsigned 8 bits, planar - AUDIO_FMT_S16 // signed 16 bits - AUDIO_FMT_S16P // signed 16 bits, planar - AUDIO_FMT_S32 // signed 32 bits - AUDIO_FMT_S32P // signed 32 bits, planar - AUDIO_FMT_F32 // float32 - AUDIO_FMT_F32P // float32, planar - AUDIO_FMT_F64 // float64 - AUDIO_FMT_F64P // float64, planar - AUDIO_FMT_S64 // signed 64 bits - AUDIO_FMT_S64P // signed 64 bits, planar + SAMPLE_FORMAT_NONE SampleFormat = iota + SAMPLE_FORMAT_U8 // Byte + SAMPLE_FORMAT_S16 // Signed 16-bit + SAMPLE_FORMAT_S32 // Signed 32-bit + SAMPLE_FORMAT_S64 // Signed 64-bit + SAMPLE_FORMAT_FLT // Float 32-bit + SAMPLE_FORMAT_DBL // Float 64-bit + SAMPLE_FORMAT_U8P // Planar byte + SAMPLE_FORMAT_S16P // Planar signed 16-bit + SAMPLE_FORMAT_S32P // Planar signed 32-bit + SAMPLE_FORMAT_S64P // Planar signed 64-bit + SAMPLE_FORMAT_FLTP // Planar float 32-bit + SAMPLE_FORMAT_DBLP // Planar float 64-bit + SAMPLE_FORMAT_MAX = SAMPLE_FORMAT_DBLP ) -var ( - AudioLayoutMono = AudioChannelLayout{1} - AudioLayoutStereo = AudioChannelLayout{2} +const ( + CHANNEL_LAYOUT_NONE ChannelLayout = iota + CHANNEL_LAYOUT_MONO + CHANNEL_LAYOUT_STEREO + CHANNEL_LAYOUT_2POINT1 + CHANNEL_LAYOUT_2_1 + CHANNEL_LAYOUT_SURROUND + CHANNEL_LAYOUT_3POINT1 + CHANNEL_LAYOUT_4POINT0 + CHANNEL_LAYOUT_4POINT1 + CHANNEL_LAYOUT_2_2 + CHANNEL_LAYOUT_QUAD + CHANNEL_LAYOUT_5POINT0 + CHANNEL_LAYOUT_5POINT1 + CHANNEL_LAYOUT_5POINT0_BACK + CHANNEL_LAYOUT_5POINT1_BACK + CHANNEL_LAYOUT_6POINT0 + CHANNEL_LAYOUT_6POINT0_FRONT + CHANNEL_LAYOUT_HEXAGONAL + CHANNEL_LAYOUT_6POINT1 + CHANNEL_LAYOUT_6POINT1_BACK + CHANNEL_LAYOUT_6POINT1_FRONT + CHANNEL_LAYOUT_7POINT0 + CHANNEL_LAYOUT_7POINT0_FRONT + CHANNEL_LAYOUT_7POINT1 + CHANNEL_LAYOUT_7POINT1_WIDE + CHANNEL_LAYOUT_7POINT1_WIDE_BACK + CHANNEL_LAYOUT_OCTAGONAL + CHANNEL_LAYOUT_HEXADECAGONAL + CHANNEL_LAYOUT_STEREO_DOWNMIX + CHANNEL_LAYOUT_22POINT2 + CHANNEL_LAYOUT_AMBISONIC_FIRST_ORDER + CHANNEL_LAYOUT_MAX = CHANNEL_LAYOUT_AMBISONIC_FIRST_ORDER +) + +const ( + CHANNEL_NONE AudioChannel = iota + CHANNEL_FRONT_LEFT + CHANNEL_FRONT_RIGHT + CHANNEL_FRONT_CENTER + CHANNEL_LOW_FREQUENCY + CHANNEL_BACK_LEFT + CHANNEL_BACK_RIGHT + CHANNEL_FRONT_LEFT_OF_CENTER + CHANNEL_FRONT_RIGHT_OF_CENTER + CHANNEL_BACK_CENTER + CHANNEL_SIDE_LEFT + CHANNEL_SIDE_RIGHT + CHANNEL_TOP_CENTER + CHANNEL_TOP_FRONT_LEFT + CHANNEL_TOP_FRONT_CENTER + CHANNEL_TOP_FRONT_RIGHT + CHANNEL_TOP_BACK_LEFT + CHANNEL_TOP_BACK_CENTER + CHANNEL_TOP_BACK_RIGHT + CHANNEL_STEREO_LEFT + CHANNEL_STEREO_RIGHT + CHANNEL_WIDE_LEFT + CHANNEL_WIDE_RIGHT + CHANNEL_SURROUND_DIRECT_LEFT + CHANNEL_SURROUND_DIRECT_RIGHT + CHANNEL_LOW_FREQUENCY_2 + CHANNEL_TOP_SIDE_LEFT + CHANNEL_TOP_SIDE_RIGHT + CHANNEL_BOTTOM_FRONT_CENTER + CHANNEL_BOTTOM_FRONT_LEFT + CHANNEL_BOTTOM_FRONT_RIGHT + CHANNEL_MAX = CHANNEL_BOTTOM_FRONT_RIGHT ) //////////////////////////////////////////////////////////////////////////////// // STRINGIFY -func (f AudioFormat) String() string { - switch f { - case AUDIO_FMT_NONE: - return "AUDIO_FMT_NONE" - case AUDIO_FMT_U8: - return "AUDIO_FMT_U8" - case AUDIO_FMT_U8P: - return "AUDIO_FMT_U8P" - case AUDIO_FMT_S16: - return "AUDIO_FMT_S16" - case AUDIO_FMT_S16P: - return "AUDIO_FMT_S16P" - case AUDIO_FMT_S32: - return "AUDIO_FMT_S32" - case AUDIO_FMT_S32P: - return "AUDIO_FMT_S32P" - case AUDIO_FMT_F32: - return "AUDIO_FMT_F32" - case AUDIO_FMT_F32P: - return "AUDIO_FMT_F32P" - case AUDIO_FMT_F64: - return "AUDIO_FMT_F64" - case AUDIO_FMT_F64P: - return "AUDIO_FMT_F64P" - case AUDIO_FMT_S64: - return "AUDIO_FMT_S64" - case AUDIO_FMT_S64P: - return "AUDIO_FMT_S64P" +func (v AudioFormat) String() string { + str := "" +} + +func (v AudioChannel) String() string { + switch v { + case CHANNEL_NONE: + return "CHANNEL_NONE" + case CHANNEL_FRONT_LEFT: + return "CHANNEL_FRONT_LEFT" + case CHANNEL_FRONT_RIGHT: + return "CHANNEL_FRONT_RIGHT" + case CHANNEL_FRONT_CENTER: + return "CHANNEL_FRONT_CENTER" + case CHANNEL_LOW_FREQUENCY: + return "CHANNEL_LOW_FREQUENCY" + case CHANNEL_BACK_LEFT: + return "CHANNEL_BACK_LEFT" + case CHANNEL_BACK_RIGHT: + return "CHANNEL_BACK_RIGHT" + case CHANNEL_FRONT_LEFT_OF_CENTER: + return "CHANNEL_FRONT_LEFT_OF_CENTER" + case CHANNEL_FRONT_RIGHT_OF_CENTER: + return "CHANNEL_FRONT_RIGHT_OF_CENTER" + case CHANNEL_BACK_CENTER: + return "CHANNEL_BACK_CENTER" + case CHANNEL_SIDE_LEFT: + return "CHANNEL_SIDE_LEFT" + case CHANNEL_SIDE_RIGHT: + return "CHANNEL_SIDE_RIGHT" + case CHANNEL_TOP_CENTER: + return "CHANNEL_TOP_CENTER" + case CHANNEL_TOP_FRONT_LEFT: + return "CHANNEL_TOP_FRONT_LEFT" + case CHANNEL_TOP_FRONT_CENTER: + return "CHANNEL_TOP_FRONT_CENTER" + case CHANNEL_TOP_FRONT_RIGHT: + return "CHANNEL_TOP_FRONT_RIGHT" + case CHANNEL_TOP_BACK_LEFT: + return "CHANNEL_TOP_BACK_LEFT" + case CHANNEL_TOP_BACK_CENTER: + return "CHANNEL_TOP_BACK_CENTER" + case CHANNEL_TOP_BACK_RIGHT: + return "CHANNEL_TOP_BACK_RIGHT" + case CHANNEL_STEREO_LEFT: + return "CHANNEL_STEREO_LEFT" + case CHANNEL_STEREO_RIGHT: + return "CHANNEL_STEREO_RIGHT" + case CHANNEL_WIDE_LEFT: + return "CHANNEL_WIDE_LEFT" + case CHANNEL_WIDE_RIGHT: + return "CHANNEL_WIDE_RIGHT" + case CHANNEL_SURROUND_DIRECT_LEFT: + return "CHANNEL_SURROUND_DIRECT_LEFT" + case CHANNEL_SURROUND_DIRECT_RIGHT: + return "CHANNEL_SURROUND_DIRECT_RIGHT" + case CHANNEL_LOW_FREQUENCY_2: + return "CHANNEL_LOW_FREQUENCY_2" + case CHANNEL_TOP_SIDE_LEFT: + return "CHANNEL_TOP_SIDE_LEFT" + case CHANNEL_TOP_SIDE_RIGHT: + return "CHANNEL_TOP_SIDE_RIGHT" + case CHANNEL_BOTTOM_FRONT_CENTER: + return "CHANNEL_BOTTOM_FRONT_CENTER" + case CHANNEL_BOTTOM_FRONT_LEFT: + return "CHANNEL_BOTTOM_FRONT_LEFT" + case CHANNEL_BOTTOM_FRONT_RIGHT: + return "CHANNEL_BOTTOM_FRONT_RIGHT" + default: + return "[?? Invalid AudioChannel value]" + } +} + +func (v SampleFormat) String() string { + switch v { + case SAMPLE_FORMAT_NONE: + return "SAMPLE_FORMAT_NONE" + case SAMPLE_FORMAT_U8: + return "SAMPLE_FORMAT_U8" + case SAMPLE_FORMAT_S16: + return "SAMPLE_FORMAT_S16" + case SAMPLE_FORMAT_S32: + return "SAMPLE_FORMAT_S32" + case SAMPLE_FORMAT_FLT: + return "SAMPLE_FORMAT_FLT" + case SAMPLE_FORMAT_DBL: + return "SAMPLE_FORMAT_DBL" + case SAMPLE_FORMAT_U8P: + return "SAMPLE_FORMAT_U8P" + case SAMPLE_FORMAT_S16P: + return "SAMPLE_FORMAT_S16P" + case SAMPLE_FORMAT_S32P: + return "SAMPLE_FORMAT_S32P" + case SAMPLE_FORMAT_FLTP: + return "SAMPLE_FORMAT_FLTP" + case SAMPLE_FORMAT_DBLP: + return "SAMPLE_FORMAT_DBLP" + case SAMPLE_FORMAT_S64: + return "SAMPLE_FORMAT_S64" + case SAMPLE_FORMAT_S64P: + return "SAMPLE_FORMAT_S64P" + default: + return "[?? Invalid SampleFormat value]" + } +} + +func (v ChannelLayout) String() string { + switch v { + case CHANNEL_LAYOUT_NONE: + return "CHANNEL_LAYOUT_NONE" + case CHANNEL_LAYOUT_MONO: + return "CHANNEL_LAYOUT_MONO" + case CHANNEL_LAYOUT_STEREO: + return "CHANNEL_LAYOUT_STEREO" + case CHANNEL_LAYOUT_2POINT1: + return "CHANNEL_LAYOUT_2POINT1" + case CHANNEL_LAYOUT_2_1: + return "CHANNEL_LAYOUT_2_1" + case CHANNEL_LAYOUT_SURROUND: + return "CHANNEL_LAYOUT_SURROUND" + case CHANNEL_LAYOUT_3POINT1: + return "CHANNEL_LAYOUT_3POINT1" + case CHANNEL_LAYOUT_4POINT0: + return "CHANNEL_LAYOUT_4POINT0" + case CHANNEL_LAYOUT_4POINT1: + return "CHANNEL_LAYOUT_4POINT1" + case CHANNEL_LAYOUT_2_2: + return "CHANNEL_LAYOUT_2_2" + case CHANNEL_LAYOUT_QUAD: + return "CHANNEL_LAYOUT_QUAD" + case CHANNEL_LAYOUT_5POINT0: + return "CHANNEL_LAYOUT_5POINT0" + case CHANNEL_LAYOUT_5POINT1: + return "CHANNEL_LAYOUT_5POINT1" + case CHANNEL_LAYOUT_5POINT0_BACK: + return "CHANNEL_LAYOUT_5POINT0_BACK" + case CHANNEL_LAYOUT_5POINT1_BACK: + return "CHANNEL_LAYOUT_5POINT1_BACK" + case CHANNEL_LAYOUT_6POINT0: + return "CHANNEL_LAYOUT_6POINT0" + case CHANNEL_LAYOUT_6POINT0_FRONT: + return "CHANNEL_LAYOUT_6POINT0_FRONT" + case CHANNEL_LAYOUT_HEXAGONAL: + return "CHANNEL_LAYOUT_HEXAGONAL" + case CHANNEL_LAYOUT_6POINT1: + return "CHANNEL_LAYOUT_6POINT1" + case CHANNEL_LAYOUT_6POINT1_BACK: + return "CHANNEL_LAYOUT_6POINT1_BACK" + case CHANNEL_LAYOUT_6POINT1_FRONT: + return "CHANNEL_LAYOUT_6POINT1_FRONT" + case CHANNEL_LAYOUT_7POINT0: + return "CHANNEL_LAYOUT_7POINT0" + case CHANNEL_LAYOUT_7POINT0_FRONT: + return "CHANNEL_LAYOUT_7POINT0_FRONT" + case CHANNEL_LAYOUT_7POINT1: + return "CHANNEL_LAYOUT_7POINT1" + case CHANNEL_LAYOUT_7POINT1_WIDE: + return "CHANNEL_LAYOUT_7POINT1_WIDE" + case CHANNEL_LAYOUT_7POINT1_WIDE_BACK: + return "CHANNEL_LAYOUT_7POINT1_WIDE_BACK" + case CHANNEL_LAYOUT_OCTAGONAL: + return "CHANNEL_LAYOUT_OCTAGONAL" + case CHANNEL_LAYOUT_HEXADECAGONAL: + return "CHANNEL_LAYOUT_HEXADECAGONAL" + case CHANNEL_LAYOUT_STEREO_DOWNMIX: + return "CHANNEL_LAYOUT_STEREO_DOWNMIX" + case CHANNEL_LAYOUT_22POINT2: + return "CHANNEL_LAYOUT_22POINT2" + case CHANNEL_LAYOUT_AMBISONIC_FIRST_ORDER: + return "CHANNEL_LAYOUT_AMBISONIC_FIRST_ORDER" default: - return "[?? Invalid AudioFormat value]" + return "[?? Invalid ChannelLayout value]" } } diff --git a/chromaprint.go b/_old/chromaprint.go similarity index 100% rename from chromaprint.go rename to _old/chromaprint.go diff --git a/pkg/config/config.go b/_old/config/config.go similarity index 100% rename from pkg/config/config.go rename to _old/config/config.go diff --git a/cmd/extractartwork/main.go b/_old/extractartwork/main.go similarity index 100% rename from cmd/extractartwork/main.go rename to _old/extractartwork/main.go diff --git a/cmd/extractartwork/process.go b/_old/extractartwork/process.go similarity index 100% rename from cmd/extractartwork/process.go rename to _old/extractartwork/process.go diff --git a/cmd/fingerprint/main.go b/_old/fingerprint/main.go similarity index 100% rename from cmd/fingerprint/main.go rename to _old/fingerprint/main.go diff --git a/_old/media.go b/_old/media.go old mode 100755 new mode 100644 index 7d97edf..bb28af6 --- a/_old/media.go +++ b/_old/media.go @@ -2,30 +2,231 @@ package media import ( "context" - "net/url" - "strings" + "io" + "time" ) //////////////////////////////////////////////////////////////////////////////// // TYPES -type ( - MediaKey string - MediaFlag uint64 - DecodeIteratorFunc func(context.Context, MediaPacket) error -) +// MediaFlag is a bitfield of flags for media, including type of media +type MediaFlag uint + +// MediaKey is a string which is used for media metadata +type MediaKey string + +// Demux is a function which is called for each packet in the media, which +// is associated with a single stream. The function should return an error if +// the decode should be terminated. +type DemuxFn func(context.Context, Packet) error + +// DecodeFn is a function which is called for each frame in the media, which +// is associated with a single stream. The function should return an error if +// the decode should be terminated. +type DecodeFn func(context.Context, Frame) error //////////////////////////////////////////////////////////////////////////////// // INTERFACES -// Media represents either input or output media +// Manager is an interface to the ffmpeg media library for media manipulation +type Manager interface { + io.Closer + + // Enumerate formats with MEDIA_FLAG_ENCODER, MEDIA_FLAG_DECODER, + // MEDIA_FLAG_FILE and MEDIA_FLAG_DEVICE flags to filter. + // Lookups can be further filtered by name, mimetype and extension + MediaFormats(MediaFlag, ...string) []MediaFormat + + // Open media file for reading and return it. A format can be specified + // to "force" a specific format + OpenFile(string, MediaFormat) (Media, error) + + // Open media URL for reading and return it. A format can be specified + // to "force" a specific format + OpenURL(string, MediaFormat) (Media, error) + + // Open media device with a specific name for reading and return it. + OpenDevice(string) (Media, error) + + // Create file for writing and return it + CreateFile(string) (Media, error) + + // Create an output device with a specific name for writing and return it + CreateDevice(string) (Media, error) + + // Create a map of input media. If MediaFlag is MEDIA_FLAG_NONE, then + // all audio, video and subtitle streams are mapped, or else a + // combination of MEDIA_FLAG_AUDIO, + // MEDIA_FLAG_VIDEO, MEDIA_FLAG_SUBTITLE and MEDIA_FLAG_DATA + // can be used to map specific types of streams. + Map(Media, MediaFlag) (Map, error) + + // Demux a media file, passing packets to a callback function + Demux(context.Context, Map, DemuxFn) error + + // Decode a packet into a series of frames, passing decoded frames to + // a callback function + Decode(context.Context, Map, Packet, DecodeFn) error + + // Log messages from ffmpeg + SetDebug(bool) +} + +// MediaFormat is an input or output format for media items +type MediaFormat interface { + // Return the names of the media format + Name() []string + + // Return a longer description of the media format + Description() string + + // Return MEDIA_FLAG_ENCODER, MEDIA_FLAG_DECODER, MEDIA_FLAG_FILE + // and MEDIA_FLAG_DEVICE flags + Flags() MediaFlag + + // Return mimetypes handled + MimeType() []string + + // Return file extensions handled + Ext() []string + + // Return the default audio codec for the format + DefaultAudioCodec() Codec + + // Return the default video codec for the format + DefaultVideoCodec() Codec + + // Return the default subtitle codec for the format + DefaultSubtitleCodec() Codec +} + +// Map is a mapping of input media, potentially to output media +type Map interface { + // Return input media + Input() Media + + // Return a single stream which is mapped for decoding, filtering by + // stream type. Returns nil if there is no selection of that type + Streams(MediaFlag) []Stream + + // Print a summary of the mapping + PrintMap(w io.Writer) + + // Resample an audio stream + Resample(AudioFormat, Stream) error + + // Encode to output media using default codec from a specific stream + //Encode(Media, Stream) error +} + +// Media is a source or destination of media type Media interface { - URL() *url.URL // Return URL for the media location - Flags() MediaFlag // Return flags + io.Closer + + // URL for the media + URL() string + + // Return enumeration of streams + Streams() []Stream + + // Return media flags for the media + Flags() MediaFlag + + // Return the format of the media + Format() MediaFormat + + // Return metadata for the media + Metadata() Metadata + + // Set metadata value by key, or remove it if the value is nil + Set(MediaKey, any) error +} + +// Stream of data multiplexed in the media +type Stream interface { + // Return index of stream in the media + Index() int + + // Return media flags for the stream + Flags() MediaFlag + + // Return artwork for the stream - if MEDIA_FLAG_ARTWORK is set + Artwork() []byte +} + +// Metadata embedded in the media +type Metadata interface { + // Return enumeration of keys + Keys() []MediaKey + + // Return value for key + Value(MediaKey) any +} + +// Packet is a single unit of data in the media +type Packet interface { + // Flags returns the flags for the packet from the stream + Flags() MediaFlag + + // Stream returns the stream which the packet belongs to + Stream() Stream + + // IsKeyFrame returns true if the packet contains a key frame + IsKeyFrame() bool + + // Pos returns the byte position of the packet in the media + Pos() int64 + + // Duration returns the duration of the packet + Duration() time.Duration + + // Size of the packet in bytes + Size() int + + // Bytes returns the raw bytes of the packet + Bytes() []byte +} + +// Frame is a decoded video or audio frame +type Frame interface { + AudioFrame + VideoFrame + + // Returns MEDIA_FLAG_VIDEO, MEDIA_FLAG_AUDIO + Flags() MediaFlag + + // Returns true if planar format + //IsPlanar() bool + + // Returns the samples for a specified channel, as array of bytes. For packed + // audio format, the channel should be 0. + //Bytes(channel int) []byte } -// MediaCodec is the codec and parameters -type MediaCodec interface { +type AudioFrame interface { + // Returns the audio format, if MEDIA_FLAG_AUDIO is set + AudioFormat() AudioFormat + + // Number of samples, if MEDIA_FLAG_AUDIO is set + NumSamples() int + + // Audio channels, if MEDIA_FLAG_AUDIO is set + Channels() []AudioChannel + + // Duration of the frame, if MEDIA_FLAG_AUDIO is set + Duration() time.Duration +} + +type VideoFrame interface { + // Returns the audio format, if MEDIA_FLAG_VIDEO is set + PixelFormat() PixelFormat + + // Return frame width and height, if MEDIA_FLAG_VIDEO is set + Size() (int, int) +} + +// Codec is an encoder or decoder for a specific media type +type Codec interface { // Name returns the unique name for the codec Name() string @@ -36,23 +237,17 @@ type MediaCodec interface { Flags() MediaFlag } -// MediaPacket is a packet of data from a stream -type MediaPacket interface { - Size() int - Bytes() []byte - Stream() int -} - //////////////////////////////////////////////////////////////////////////////// // CONSTANTS const ( MEDIA_FLAG_ALBUM MediaFlag = (1 << iota) // Is part of an album MEDIA_FLAG_ALBUM_TRACK // Is an album track - MEDIA_FLAG_ALBUM_COMPILATION // Album is a compliation + MEDIA_FLAG_ALBUM_COMPILATION // Album is a compilation MEDIA_FLAG_TVSHOW // Is part of a TV Show MEDIA_FLAG_TVSHOW_EPISODE // Is a TV Show episode MEDIA_FLAG_FILE // Is a file + MEDIA_FLAG_DEVICE // Is a device MEDIA_FLAG_VIDEO // Contains video MEDIA_FLAG_AUDIO // Contains audio MEDIA_FLAG_SUBTITLE // Contains subtitles @@ -63,7 +258,6 @@ const ( MEDIA_FLAG_ENCODER // Is an encoder MEDIA_FLAG_DECODER // Is an decoder MEDIA_FLAG_NONE MediaFlag = 0 - MEDIA_FLAG_MIN = MEDIA_FLAG_ALBUM MEDIA_FLAG_MAX = MEDIA_FLAG_DECODER ) @@ -78,7 +272,7 @@ const ( MEDIA_KEY_COMPOSER MediaKey = "composer" // string MEDIA_KEY_COPYRIGHT MediaKey = "copyright" // string MEDIA_KEY_YEAR MediaKey = "date" // uint - MEDIA_KEY_DISC MediaKey = "disc" // uint + MEDIA_KEY_DISC MediaKey = "disc" // uint xx or xx/yy MEDIA_KEY_ENCODED_BY MediaKey = "encoded_by" // string MEDIA_KEY_FILENAME MediaKey = "filename" // string MEDIA_KEY_GENRE MediaKey = "genre" // string @@ -88,7 +282,7 @@ const ( MEDIA_KEY_SERVICE_NAME MediaKey = "service_name" // string MEDIA_KEY_SERVICE_PROVIDER MediaKey = "service_provider" // string MEDIA_KEY_TITLE MediaKey = "title" // string - MEDIA_KEY_TRACK MediaKey = "track" // uint + MEDIA_KEY_TRACK MediaKey = "track" // uint xx or xx/yy MEDIA_KEY_VERSION_MAJOR MediaKey = "major_version" // string MEDIA_KEY_VERSION_MINOR MediaKey = "minor_version" // string MEDIA_KEY_SHOW MediaKey = "show" // string @@ -116,12 +310,12 @@ func (f MediaFlag) String() string { return f.FlagString() } str := "" - for v := MEDIA_FLAG_MIN; v <= MEDIA_FLAG_MAX; v <<= 1 { + for v := MediaFlag(1); v <= MEDIA_FLAG_MAX; v <<= 1 { if f&v == v { - str += v.FlagString() + "|" + str += "|" + v.FlagString() } } - return strings.TrimSuffix(str, "|") + return str[1:] } func (f MediaFlag) FlagString() string { @@ -140,6 +334,8 @@ func (f MediaFlag) FlagString() string { return "MEDIA_FLAG_TVSHOW_EPISODE" case MEDIA_FLAG_FILE: return "MEDIA_FLAG_FILE" + case MEDIA_FLAG_DEVICE: + return "MEDIA_FLAG_DEVICE" case MEDIA_FLAG_VIDEO: return "MEDIA_FLAG_VIDEO" case MEDIA_FLAG_AUDIO: diff --git a/pkg/media/codec.go b/_old/media/codec.go similarity index 100% rename from pkg/media/codec.go rename to _old/media/codec.go diff --git a/pkg/media/codec.go_old b/_old/media/codec.go_old similarity index 100% rename from pkg/media/codec.go_old rename to _old/media/codec.go_old diff --git a/pkg/media/consts.go b/_old/media/consts.go similarity index 100% rename from pkg/media/consts.go rename to _old/media/consts.go diff --git a/pkg/media/decoder.go b/_old/media/decoder.go similarity index 100% rename from pkg/media/decoder.go rename to _old/media/decoder.go diff --git a/pkg/media/doc.go b/_old/media/doc.go similarity index 100% rename from pkg/media/doc.go rename to _old/media/doc.go diff --git a/pkg/media/encoder.go b/_old/media/encoder.go similarity index 100% rename from pkg/media/encoder.go rename to _old/media/encoder.go diff --git a/pkg/media/format.go b/_old/media/format.go similarity index 100% rename from pkg/media/format.go rename to _old/media/format.go diff --git a/pkg/media/frame.go b/_old/media/frame.go similarity index 100% rename from pkg/media/frame.go rename to _old/media/frame.go diff --git a/pkg/media/input.go b/_old/media/input.go similarity index 100% rename from pkg/media/input.go rename to _old/media/input.go diff --git a/pkg/media/manager.go b/_old/media/manager.go similarity index 100% rename from pkg/media/manager.go rename to _old/media/manager.go diff --git a/pkg/media/manager_test.go b/_old/media/manager_test.go similarity index 100% rename from pkg/media/manager_test.go rename to _old/media/manager_test.go diff --git a/pkg/media/map.go b/_old/media/map.go similarity index 100% rename from pkg/media/map.go rename to _old/media/map.go diff --git a/pkg/media/map_test.go b/_old/media/map_test.go similarity index 100% rename from pkg/media/map_test.go rename to _old/media/map_test.go diff --git a/pkg/media/media.go b/_old/media/media.go similarity index 100% rename from pkg/media/media.go rename to _old/media/media.go diff --git a/pkg/media/metadata.go b/_old/media/metadata.go similarity index 100% rename from pkg/media/metadata.go rename to _old/media/metadata.go diff --git a/pkg/media/metadata_test.go b/_old/media/metadata_test.go similarity index 100% rename from pkg/media/metadata_test.go rename to _old/media/metadata_test.go diff --git a/pkg/media/output.go b/_old/media/output.go similarity index 100% rename from pkg/media/output.go rename to _old/media/output.go diff --git a/pkg/media/packet.go b/_old/media/packet.go similarity index 100% rename from pkg/media/packet.go rename to _old/media/packet.go diff --git a/pkg/media/resampler.go b/_old/media/resampler.go similarity index 100% rename from pkg/media/resampler.go rename to _old/media/resampler.go diff --git a/pkg/media/rescaler.go b/_old/media/rescaler.go similarity index 100% rename from pkg/media/rescaler.go rename to _old/media/rescaler.go diff --git a/pkg/media/stream.go b/_old/media/stream.go similarity index 100% rename from pkg/media/stream.go rename to _old/media/stream.go diff --git a/pkg/media/version.go b/_old/media/version.go similarity index 100% rename from pkg/media/version.go rename to _old/media/version.go diff --git a/pkg/_audio/audioframe.go b/_old/old/audioframe.go similarity index 100% rename from pkg/_audio/audioframe.go rename to _old/old/audioframe.go diff --git a/pkg/_audio/audioframe_test.go b/_old/old/audioframe_test.go similarity index 100% rename from pkg/_audio/audioframe_test.go rename to _old/old/audioframe_test.go diff --git a/pkg/_audio/consts.go b/_old/old/consts.go similarity index 100% rename from pkg/_audio/consts.go rename to _old/old/consts.go diff --git a/pkg/_audio/context.go b/_old/old/context.go similarity index 100% rename from pkg/_audio/context.go rename to _old/old/context.go diff --git a/pkg/_audio/doc.go b/_old/old/doc.go similarity index 100% rename from pkg/_audio/doc.go rename to _old/old/doc.go diff --git a/pkg/_audio/manager.go b/_old/old/manager.go similarity index 100% rename from pkg/_audio/manager.go rename to _old/old/manager.go diff --git a/pkg/_audio/manager_test.go b/_old/old/manager_test.go similarity index 100% rename from pkg/_audio/manager_test.go rename to _old/old/manager_test.go diff --git a/cmd/transcode/flags.go b/_old/transcode/flags.go similarity index 100% rename from cmd/transcode/flags.go rename to _old/transcode/flags.go diff --git a/cmd/transcode/main.go b/_old/transcode/main.go similarity index 100% rename from cmd/transcode/main.go rename to _old/transcode/main.go diff --git a/video.go b/_old/video.go similarity index 100% rename from video.go rename to _old/video.go diff --git a/audio.go b/audio.go deleted file mode 100755 index 6375b91..0000000 --- a/audio.go +++ /dev/null @@ -1,353 +0,0 @@ -package media - -import ( - "fmt" - "io" -) - -//////////////////////////////////////////////////////////////////////////////// -// TYPES - -// SampleFormat specifies the type of a single sample -type SampleFormat uint - -// ChannelLayout specifies the layout of channels -type ChannelLayout uint - -// AudioChannel specifies a single audio channel -type AudioChannel uint - -// AudioFormat specifies the interface for audio format -type AudioFormat struct { - // Sample rate in Hz - Rate uint - - // Sample format - Format SampleFormat - - // Channel layout - Layout ChannelLayout -} - -// SWResampleFn is a function that accepts an "output" audio frame, -// which can be nil if the conversion has not started yet, and should -// fill the audio frame provided to the Convert function. Should return -// io.EOF on end of conversion, or any other error to stop the conversion. -type SWResampleFn func(AudioFrame) error - -//////////////////////////////////////////////////////////////////////////////// -// INTERFACES - -/* -// AudioFrame is a slice of audio samples -type AudioFrame interface { - io.Closer - - // Audio format - AudioFormat() AudioFormat - - // Number of samples in a single channel - Samples() int - - // Audio channels - Channels() []AudioChannel - - // Duration of the frame - Duration() time.Duration - - // Returns true if planar format (one set of samples per channel) - IsPlanar() bool - - // Returns the samples for a specified channel, as array of bytes. For packed - // audio format, the channel should be 0. - Bytes(channel int) []byte -} -*/ - -// SWResample is an interface to the ffmpeg swresample library -// which resamples audio. -type SWResample interface { - io.Closer - - // Create a new empty context object for conversion, with an input frame which - // will be used to store the data and the target output format. - Convert(AudioFrame, AudioFormat, SWResampleFn) error -} - -//////////////////////////////////////////////////////////////////////////////// -// CONSTANTS - -const ( - SAMPLE_FORMAT_NONE SampleFormat = iota - SAMPLE_FORMAT_U8 // Byte - SAMPLE_FORMAT_S16 // Signed 16-bit - SAMPLE_FORMAT_S32 // Signed 32-bit - SAMPLE_FORMAT_S64 // Signed 64-bit - SAMPLE_FORMAT_FLT // Float 32-bit - SAMPLE_FORMAT_DBL // Float 64-bit - SAMPLE_FORMAT_U8P // Planar byte - SAMPLE_FORMAT_S16P // Planar signed 16-bit - SAMPLE_FORMAT_S32P // Planar signed 32-bit - SAMPLE_FORMAT_S64P // Planar signed 64-bit - SAMPLE_FORMAT_FLTP // Planar float 32-bit - SAMPLE_FORMAT_DBLP // Planar float 64-bit - SAMPLE_FORMAT_MAX = SAMPLE_FORMAT_DBLP -) - -const ( - CHANNEL_LAYOUT_NONE ChannelLayout = iota - CHANNEL_LAYOUT_MONO - CHANNEL_LAYOUT_STEREO - CHANNEL_LAYOUT_2POINT1 - CHANNEL_LAYOUT_2_1 - CHANNEL_LAYOUT_SURROUND - CHANNEL_LAYOUT_3POINT1 - CHANNEL_LAYOUT_4POINT0 - CHANNEL_LAYOUT_4POINT1 - CHANNEL_LAYOUT_2_2 - CHANNEL_LAYOUT_QUAD - CHANNEL_LAYOUT_5POINT0 - CHANNEL_LAYOUT_5POINT1 - CHANNEL_LAYOUT_5POINT0_BACK - CHANNEL_LAYOUT_5POINT1_BACK - CHANNEL_LAYOUT_6POINT0 - CHANNEL_LAYOUT_6POINT0_FRONT - CHANNEL_LAYOUT_HEXAGONAL - CHANNEL_LAYOUT_6POINT1 - CHANNEL_LAYOUT_6POINT1_BACK - CHANNEL_LAYOUT_6POINT1_FRONT - CHANNEL_LAYOUT_7POINT0 - CHANNEL_LAYOUT_7POINT0_FRONT - CHANNEL_LAYOUT_7POINT1 - CHANNEL_LAYOUT_7POINT1_WIDE - CHANNEL_LAYOUT_7POINT1_WIDE_BACK - CHANNEL_LAYOUT_OCTAGONAL - CHANNEL_LAYOUT_HEXADECAGONAL - CHANNEL_LAYOUT_STEREO_DOWNMIX - CHANNEL_LAYOUT_22POINT2 - CHANNEL_LAYOUT_AMBISONIC_FIRST_ORDER - CHANNEL_LAYOUT_MAX = CHANNEL_LAYOUT_AMBISONIC_FIRST_ORDER -) - -const ( - CHANNEL_NONE AudioChannel = iota - CHANNEL_FRONT_LEFT - CHANNEL_FRONT_RIGHT - CHANNEL_FRONT_CENTER - CHANNEL_LOW_FREQUENCY - CHANNEL_BACK_LEFT - CHANNEL_BACK_RIGHT - CHANNEL_FRONT_LEFT_OF_CENTER - CHANNEL_FRONT_RIGHT_OF_CENTER - CHANNEL_BACK_CENTER - CHANNEL_SIDE_LEFT - CHANNEL_SIDE_RIGHT - CHANNEL_TOP_CENTER - CHANNEL_TOP_FRONT_LEFT - CHANNEL_TOP_FRONT_CENTER - CHANNEL_TOP_FRONT_RIGHT - CHANNEL_TOP_BACK_LEFT - CHANNEL_TOP_BACK_CENTER - CHANNEL_TOP_BACK_RIGHT - CHANNEL_STEREO_LEFT - CHANNEL_STEREO_RIGHT - CHANNEL_WIDE_LEFT - CHANNEL_WIDE_RIGHT - CHANNEL_SURROUND_DIRECT_LEFT - CHANNEL_SURROUND_DIRECT_RIGHT - CHANNEL_LOW_FREQUENCY_2 - CHANNEL_TOP_SIDE_LEFT - CHANNEL_TOP_SIDE_RIGHT - CHANNEL_BOTTOM_FRONT_CENTER - CHANNEL_BOTTOM_FRONT_LEFT - CHANNEL_BOTTOM_FRONT_RIGHT - CHANNEL_MAX = CHANNEL_BOTTOM_FRONT_RIGHT -) - -//////////////////////////////////////////////////////////////////////////////// -// STRINGIFY - -func (v AudioFormat) String() string { - str := "" -} - -func (v AudioChannel) String() string { - switch v { - case CHANNEL_NONE: - return "CHANNEL_NONE" - case CHANNEL_FRONT_LEFT: - return "CHANNEL_FRONT_LEFT" - case CHANNEL_FRONT_RIGHT: - return "CHANNEL_FRONT_RIGHT" - case CHANNEL_FRONT_CENTER: - return "CHANNEL_FRONT_CENTER" - case CHANNEL_LOW_FREQUENCY: - return "CHANNEL_LOW_FREQUENCY" - case CHANNEL_BACK_LEFT: - return "CHANNEL_BACK_LEFT" - case CHANNEL_BACK_RIGHT: - return "CHANNEL_BACK_RIGHT" - case CHANNEL_FRONT_LEFT_OF_CENTER: - return "CHANNEL_FRONT_LEFT_OF_CENTER" - case CHANNEL_FRONT_RIGHT_OF_CENTER: - return "CHANNEL_FRONT_RIGHT_OF_CENTER" - case CHANNEL_BACK_CENTER: - return "CHANNEL_BACK_CENTER" - case CHANNEL_SIDE_LEFT: - return "CHANNEL_SIDE_LEFT" - case CHANNEL_SIDE_RIGHT: - return "CHANNEL_SIDE_RIGHT" - case CHANNEL_TOP_CENTER: - return "CHANNEL_TOP_CENTER" - case CHANNEL_TOP_FRONT_LEFT: - return "CHANNEL_TOP_FRONT_LEFT" - case CHANNEL_TOP_FRONT_CENTER: - return "CHANNEL_TOP_FRONT_CENTER" - case CHANNEL_TOP_FRONT_RIGHT: - return "CHANNEL_TOP_FRONT_RIGHT" - case CHANNEL_TOP_BACK_LEFT: - return "CHANNEL_TOP_BACK_LEFT" - case CHANNEL_TOP_BACK_CENTER: - return "CHANNEL_TOP_BACK_CENTER" - case CHANNEL_TOP_BACK_RIGHT: - return "CHANNEL_TOP_BACK_RIGHT" - case CHANNEL_STEREO_LEFT: - return "CHANNEL_STEREO_LEFT" - case CHANNEL_STEREO_RIGHT: - return "CHANNEL_STEREO_RIGHT" - case CHANNEL_WIDE_LEFT: - return "CHANNEL_WIDE_LEFT" - case CHANNEL_WIDE_RIGHT: - return "CHANNEL_WIDE_RIGHT" - case CHANNEL_SURROUND_DIRECT_LEFT: - return "CHANNEL_SURROUND_DIRECT_LEFT" - case CHANNEL_SURROUND_DIRECT_RIGHT: - return "CHANNEL_SURROUND_DIRECT_RIGHT" - case CHANNEL_LOW_FREQUENCY_2: - return "CHANNEL_LOW_FREQUENCY_2" - case CHANNEL_TOP_SIDE_LEFT: - return "CHANNEL_TOP_SIDE_LEFT" - case CHANNEL_TOP_SIDE_RIGHT: - return "CHANNEL_TOP_SIDE_RIGHT" - case CHANNEL_BOTTOM_FRONT_CENTER: - return "CHANNEL_BOTTOM_FRONT_CENTER" - case CHANNEL_BOTTOM_FRONT_LEFT: - return "CHANNEL_BOTTOM_FRONT_LEFT" - case CHANNEL_BOTTOM_FRONT_RIGHT: - return "CHANNEL_BOTTOM_FRONT_RIGHT" - default: - return "[?? Invalid AudioChannel value]" - } -} - -func (v SampleFormat) String() string { - switch v { - case SAMPLE_FORMAT_NONE: - return "SAMPLE_FORMAT_NONE" - case SAMPLE_FORMAT_U8: - return "SAMPLE_FORMAT_U8" - case SAMPLE_FORMAT_S16: - return "SAMPLE_FORMAT_S16" - case SAMPLE_FORMAT_S32: - return "SAMPLE_FORMAT_S32" - case SAMPLE_FORMAT_FLT: - return "SAMPLE_FORMAT_FLT" - case SAMPLE_FORMAT_DBL: - return "SAMPLE_FORMAT_DBL" - case SAMPLE_FORMAT_U8P: - return "SAMPLE_FORMAT_U8P" - case SAMPLE_FORMAT_S16P: - return "SAMPLE_FORMAT_S16P" - case SAMPLE_FORMAT_S32P: - return "SAMPLE_FORMAT_S32P" - case SAMPLE_FORMAT_FLTP: - return "SAMPLE_FORMAT_FLTP" - case SAMPLE_FORMAT_DBLP: - return "SAMPLE_FORMAT_DBLP" - case SAMPLE_FORMAT_S64: - return "SAMPLE_FORMAT_S64" - case SAMPLE_FORMAT_S64P: - return "SAMPLE_FORMAT_S64P" - default: - return "[?? Invalid SampleFormat value]" - } -} - -func (v ChannelLayout) String() string { - switch v { - case CHANNEL_LAYOUT_NONE: - return "CHANNEL_LAYOUT_NONE" - case CHANNEL_LAYOUT_MONO: - return "CHANNEL_LAYOUT_MONO" - case CHANNEL_LAYOUT_STEREO: - return "CHANNEL_LAYOUT_STEREO" - case CHANNEL_LAYOUT_2POINT1: - return "CHANNEL_LAYOUT_2POINT1" - case CHANNEL_LAYOUT_2_1: - return "CHANNEL_LAYOUT_2_1" - case CHANNEL_LAYOUT_SURROUND: - return "CHANNEL_LAYOUT_SURROUND" - case CHANNEL_LAYOUT_3POINT1: - return "CHANNEL_LAYOUT_3POINT1" - case CHANNEL_LAYOUT_4POINT0: - return "CHANNEL_LAYOUT_4POINT0" - case CHANNEL_LAYOUT_4POINT1: - return "CHANNEL_LAYOUT_4POINT1" - case CHANNEL_LAYOUT_2_2: - return "CHANNEL_LAYOUT_2_2" - case CHANNEL_LAYOUT_QUAD: - return "CHANNEL_LAYOUT_QUAD" - case CHANNEL_LAYOUT_5POINT0: - return "CHANNEL_LAYOUT_5POINT0" - case CHANNEL_LAYOUT_5POINT1: - return "CHANNEL_LAYOUT_5POINT1" - case CHANNEL_LAYOUT_5POINT0_BACK: - return "CHANNEL_LAYOUT_5POINT0_BACK" - case CHANNEL_LAYOUT_5POINT1_BACK: - return "CHANNEL_LAYOUT_5POINT1_BACK" - case CHANNEL_LAYOUT_6POINT0: - return "CHANNEL_LAYOUT_6POINT0" - case CHANNEL_LAYOUT_6POINT0_FRONT: - return "CHANNEL_LAYOUT_6POINT0_FRONT" - case CHANNEL_LAYOUT_HEXAGONAL: - return "CHANNEL_LAYOUT_HEXAGONAL" - case CHANNEL_LAYOUT_6POINT1: - return "CHANNEL_LAYOUT_6POINT1" - case CHANNEL_LAYOUT_6POINT1_BACK: - return "CHANNEL_LAYOUT_6POINT1_BACK" - case CHANNEL_LAYOUT_6POINT1_FRONT: - return "CHANNEL_LAYOUT_6POINT1_FRONT" - case CHANNEL_LAYOUT_7POINT0: - return "CHANNEL_LAYOUT_7POINT0" - case CHANNEL_LAYOUT_7POINT0_FRONT: - return "CHANNEL_LAYOUT_7POINT0_FRONT" - case CHANNEL_LAYOUT_7POINT1: - return "CHANNEL_LAYOUT_7POINT1" - case CHANNEL_LAYOUT_7POINT1_WIDE: - return "CHANNEL_LAYOUT_7POINT1_WIDE" - case CHANNEL_LAYOUT_7POINT1_WIDE_BACK: - return "CHANNEL_LAYOUT_7POINT1_WIDE_BACK" - case CHANNEL_LAYOUT_OCTAGONAL: - return "CHANNEL_LAYOUT_OCTAGONAL" - case CHANNEL_LAYOUT_HEXADECAGONAL: - return "CHANNEL_LAYOUT_HEXADECAGONAL" - case CHANNEL_LAYOUT_STEREO_DOWNMIX: - return "CHANNEL_LAYOUT_STEREO_DOWNMIX" - case CHANNEL_LAYOUT_22POINT2: - return "CHANNEL_LAYOUT_22POINT2" - case CHANNEL_LAYOUT_AMBISONIC_FIRST_ORDER: - return "CHANNEL_LAYOUT_AMBISONIC_FIRST_ORDER" - default: - return "[?? Invalid ChannelLayout value]" - } -} diff --git a/cmd/cli/decode.go b/cmd/cli/decode.go new file mode 100644 index 0000000..196198a --- /dev/null +++ b/cmd/cli/decode.go @@ -0,0 +1,28 @@ +package main + +import ( + + // Packages + + "encoding/json" + "fmt" + + "github.com/mutablelogic/go-media" +) + +type DecodeCmd struct { + Path string `arg required help:"Media file" type:"path"` +} + +func (cmd *DecodeCmd) Run(globals *Globals) error { + reader, err := media.Open(cmd.Path, "") + if err != nil { + return err + } + defer reader.Close() + + data, _ := json.MarshalIndent(reader, "", " ") + fmt.Println(string(data)) + + return nil +} diff --git a/cmd/cli/main.go b/cmd/cli/main.go new file mode 100644 index 0000000..b238bae --- /dev/null +++ b/cmd/cli/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "os" + "path/filepath" + + // Packages + kong "github.com/alecthomas/kong" +) + +type Globals struct { + Debug bool `name:"debug" help:"Enable debug mode"` +} + +type CLI struct { + Globals + Version VersionCmd `cmd:"version" help:"Print version information"` + Metadata MetadataCmd `cmd:"metadata" help:"Display media metadata information"` + Decode DecodeCmd `cmd:"decode" help:"Decode media"` +} + +func main() { + name, err := os.Executable() + if err != nil { + panic(err) + } + + cli := CLI{} + ctx := kong.Parse(&cli, + kong.Name(filepath.Base(name)), + kong.Description("commands for media processing"), + kong.UsageOnError(), + kong.ConfigureHelp(kong.HelpOptions{Compact: true}), + ) + if err := ctx.Run(&cli.Globals); err != nil { + ctx.FatalIfErrorf(err) + } +} diff --git a/cmd/cli/metadata.go b/cmd/cli/metadata.go new file mode 100644 index 0000000..a200ae2 --- /dev/null +++ b/cmd/cli/metadata.go @@ -0,0 +1,29 @@ +package main + +import ( + "os" + + // Packages + + "github.com/djthorpe/go-tablewriter" + "github.com/mutablelogic/go-media" +) + +type MetadataCmd struct { + Path string `arg required help:"Media file" type:"path"` +} + +func (cmd *MetadataCmd) Run(globals *Globals) error { + reader, err := media.Open(cmd.Path, "") + if err != nil { + return err + } + defer reader.Close() + + // Print metadata + opts := []tablewriter.TableOpt{ + tablewriter.OptHeader(), + tablewriter.OptOutputText(), + } + return tablewriter.New(os.Stdout, opts...).Write(reader.Metadata()) +} diff --git a/cmd/cli/version.go b/cmd/cli/version.go new file mode 100644 index 0000000..bdcf862 --- /dev/null +++ b/cmd/cli/version.go @@ -0,0 +1,37 @@ +package main + +import ( + "fmt" + "os" + "runtime" + + "github.com/mutablelogic/go-client/pkg/version" +) + +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, "") + } + 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 +} diff --git a/cmd/decode/main.go b/cmd/decode/main.go new file mode 100644 index 0000000..e074181 --- /dev/null +++ b/cmd/decode/main.go @@ -0,0 +1,63 @@ +package main + +import ( + "flag" + "log" + "os" + + // Packages + ffmpeg "github.com/mutablelogic/go-media/pkg/ffmpeg" +) + +var ( + in = flag.String("in", "", "input file to decode") + audio_stream = flag.Int("audio", -1, "audio stream to decode") + video_stream = flag.Int("video", -1, "video stream to decode") +) + +func main() { + flag.Parse() + + // Check input file - read it + if *in == "" { + log.Fatal("-in flag must be specified") + } + r, err := os.Open(*in) + if err != nil { + log.Fatal(err) + } + defer r.Close() + + input, err := ffmpeg.NewReader(r, "") + if err != nil { + log.Fatal(err) + } + defer input.Close() + + // Create a decoder for audio + audio, err := input.NewDecoder(ffmpeg.AUDIO, *audio_stream) + if err != nil { + log.Fatal(err) + } else if err := audio.ResampleS16Mono(22000); err != nil { + log.Fatal(err) + } + + // Create a decoder for video + video, err := input.NewDecoder(ffmpeg.VIDEO, *video_stream) + if err != nil { + log.Fatal(err) + } else if err := video.Rescale(1024, 720); err != nil { + log.Fatal(err) + } + + // Demux and decode the audio and video + n := 0 + if err := input.Demux(input.Decode(func(frame ffmpeg.Frame) error { + log.Print("frame: ", n, "=>", frame) + n++ + return nil + })); err != nil { + + log.Fatal(err) + } +} diff --git a/cmd/ffmpeg/README.md b/cmd/ffmpeg/README.md new file mode 100644 index 0000000..10b54b5 --- /dev/null +++ b/cmd/ffmpeg/README.md @@ -0,0 +1,34 @@ +# ffmpeg examples + +This directory contains examples of how to use ffmpeg based on the +examples [here](https://ffmpeg.org/doxygen/6.1/examples.html) but +using the low-level golang bindings. + +* [decode_audio](decode_audio) - libavcodec decoding audio API usage example. + Decode data from an MP2 input file and generate a raw audio file to be played with ffplay. +* [decode_video](decode_video) - libavcodec decoding video API usage example. + Read from an MPEG1 video file, decode frames, and generate PGM images as output. +* [demux_decode](demux_decode) - ibavformat and libavcodec demuxing and decoding API usage example. + Show how to use the libavformat and libavcodec API to demux and decode audio + and video data. Write the output as raw audio and video files to be played by ffplay. +* [encode_audio](encode_audio) - libavcodec encoding audio API usage example. + 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. + 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 + without transcoding. +* [resample_audio](resample_audio) - libswresample API usage example. + Generate a synthetic audio signal, and Use libswresample API to perform audio resampling. The output + is written to a raw audio file to be played with ffplay. +* [scale_video](scale_video) - libswscale API usage example. + Generate a synthetic video signal and use libswscale to perform rescaling. +* [show_metadata](show_metadata) - libavformat metadata extraction API usage example. + Show metadata from an input file. + +## Running the examples + +To run the examples, use `make cmd` in the root of the repository. This will build the examples into the `build` folder. +You can use a `-help` flag to see the options for each example. diff --git a/cmd/ffmpeg/decode_audio/main.go b/cmd/ffmpeg/decode_audio/main.go new file mode 100644 index 0000000..26c2e2c --- /dev/null +++ b/cmd/ffmpeg/decode_audio/main.go @@ -0,0 +1,241 @@ +package main + +import ( + "encoding/binary" + "errors" + "flag" + "io" + "log" + "os" + "syscall" + "unsafe" + + // Packages + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +const ( + AUDIO_INBUF_SIZE = 20480 + AUDIO_REFILL_THRESH = 4096 +) + +// NativeEndian is the ByteOrder of the current system. +var NativeEndian binary.ByteOrder + +func init() { + // Examine the memory layout of an int16 to determine system + // endianness. + var one int16 = 1 + b := (*byte)(unsafe.Pointer(&one)) + if *b == 0 { + NativeEndian = binary.BigEndian + } else { + NativeEndian = binary.LittleEndian + } +} + +func main() { + in := flag.String("in", "", "input file") + codec_name := flag.String("codec", "mp2", "input codec to use") + out := flag.String("out", "", "output file") + flag.Parse() + + // Check in and out + if *in == "" || *out == "" { + log.Fatal("-in and -out files must be specified") + } + + // Find audio decoder + codec := ff.AVCodec_find_decoder_by_name(*codec_name) + if codec == nil { + log.Fatal("Codec not found") + } + parser := ff.AVCodec_parser_init(codec.ID()) + if parser == nil { + log.Fatal("Parser not found") + } + defer ff.AVCodec_parser_close(parser) + + ctx := ff.AVCodec_alloc_context(codec) + if ctx == nil { + log.Fatal("Could not allocate audio codec context") + } + defer ff.AVCodec_free_context(ctx) + + // open codec + if err := ff.AVCodec_open(ctx, codec, nil); err != nil { + log.Fatal(err) + } + + // open file for reading + r, err := os.Open(*in) + if err != nil { + log.Fatal(err) + } + defer r.Close() + + // open file for writing + w, err := os.Create(*out) + if err != nil { + log.Fatal(err) + } + defer w.Close() + + // Create a packet + packet := ff.AVCodec_packet_alloc() + if packet == nil { + log.Fatal("Could not allocate packet") + } + defer ff.AVCodec_packet_free(packet) + + // Decoded frame + frame := ff.AVUtil_frame_alloc() + if frame == nil { + log.Fatal("Could not allocate frame") + } + defer ff.AVUtil_frame_free(frame) + + // Decode until EOF + inbuf := make([]byte, AUDIO_INBUF_SIZE+ff.AV_INPUT_BUFFER_PADDING_SIZE) + data_size, err := r.Read(inbuf) + if err != nil { + log.Fatal(err) + } + + for { + if data_size == 0 { + break + } + + // Parse the data + size := ff.AVCodec_parser_parse(parser, ctx, packet, inbuf, ff.AV_NOPTS_VALUE, ff.AV_NOPTS_VALUE, 0) + if size < 0 { + log.Fatal("Error while parsing") + } + + // Adjust the input buffer beyond the parsed data + inbuf = inbuf[size:] + data_size -= size + + // Decode the data + if packet.Size() > 0 { + if err := decode(w, ctx, packet, frame); err != nil { + log.Fatal(err) + } + } + + // TODO + /* + if (data_size < AUDIO_REFILL_THRESH) { + memmove(inbuf, data, data_size); + data = inbuf; + len = fread(data + data_size, 1, + AUDIO_INBUF_SIZE - data_size, f); + if (len > 0) + data_size += len; + }*/ + } + + // Flush the decoder + ff.AVCodec_packet_unref(packet) + if err := decode(w, ctx, packet, frame); err != nil { + log.Fatal(err) + } + + // Print output pcm infomations, because there have no metadata of pcm + sfmt := ctx.SampleFormat() + if ff.AVUtil_sample_fmt_is_planar(sfmt) { + packed := ff.AVUtil_get_sample_fmt_name(sfmt) + log.Printf("Warning: the sample format the decoder produced is planar (%s). This example will output the first channel only.\n", packed) + sfmt = ff.AVUtil_get_packed_sample_fmt(ctx.SampleFormat()) + } + + n_channels := ctx.ChannelLayout().NumChannels() + fmt, err := get_format_from_sample_fmt(sfmt) + if err != nil { + log.Fatal(err) + } + + // Print output pcm infomations + log.Printf("Play the output audio file with the command:\n ffplay -f %s -ac %d -ar %d %s\n", fmt, n_channels, ctx.SampleRate(), *out) +} + +func decode(w io.Writer, ctx *ff.AVCodecContext, packet *ff.AVPacket, frame *ff.AVFrame) error { + // bytes per sample + bytes_per_sample := ff.AVUtil_get_bytes_per_sample(ctx.SampleFormat()) + if bytes_per_sample < 0 { + return errors.New("failed to calculate bytes per sample") + } + + // send the packet with the compressed data to the decoder + if err := ff.AVCodec_send_packet(ctx, packet); err != nil { + return err + } + + // Read all the output frames (in general there may be any number of them) + for { + if err := ff.AVCodec_receive_frame(ctx, frame); errors.Is(err, syscall.EAGAIN) || errors.Is(err, io.EOF) { + return nil + } else if err != nil { + return err + } + + for i := 0; i < frame.NumSamples(); i++ { + for ch := 0; ch < ctx.ChannelLayout().NumChannels(); ch++ { + buf := frame.Uint8(ch) + _, err := w.Write(buf[i*bytes_per_sample : (i+1)*bytes_per_sample]) + if err != nil { + return err + } + } + } + } +} + +func get_format_from_sample_fmt(sample_fmt ff.AVSampleFormat) (string, error) { + type sample_fmt_entry struct { + sample_fmt ff.AVSampleFormat + fmt_be string + fmt_le string + } + sample_fmt_entries := []sample_fmt_entry{ + {ff.AV_SAMPLE_FMT_U8, "u8", "u8"}, + {ff.AV_SAMPLE_FMT_S16, "s16be", "s16le"}, + {ff.AV_SAMPLE_FMT_S32, "s32be", "s32le"}, + {ff.AV_SAMPLE_FMT_FLT, "f32be", "f32le"}, + {ff.AV_SAMPLE_FMT_DBL, "f64be", "f64le"}, + } + + for _, entry := range sample_fmt_entries { + if sample_fmt == entry.sample_fmt { + if NativeEndian == binary.LittleEndian { + return entry.fmt_le, nil + } else { + return entry.fmt_be, nil + } + } + } + return "", errors.New("sample format is not supported as output format") +} + +/* + * Copyright (c) 2001 Fabrice Bellard + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ diff --git a/cmd/ffmpeg/decode_video/main.go b/cmd/ffmpeg/decode_video/main.go new file mode 100644 index 0000000..1f2f961 --- /dev/null +++ b/cmd/ffmpeg/decode_video/main.go @@ -0,0 +1,190 @@ +package main + +import ( + "errors" + "flag" + "fmt" + "io" + "log" + "os" + "path/filepath" + "syscall" + + // Packages + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +const ( + INBUF_SIZE = 4096 +) + +func main() { + in := flag.String("in", "", "input file") + codec_name := flag.String("codec", "mpeg", "input codec to use") + out := flag.String("out", "", "output file") + flag.Parse() + + // Check in and out + if *in == "" || *out == "" { + log.Fatal("-in and -out files must be specified") + } + + // Find video decoder + codec := ff.AVCodec_find_decoder_by_name(*codec_name) + if codec == nil { + log.Fatal("Codec not found") + } + parser := ff.AVCodec_parser_init(codec.ID()) + if parser == nil { + log.Fatal("Parser not found") + } + defer ff.AVCodec_parser_close(parser) + + ctx := ff.AVCodec_alloc_context(codec) + if ctx == nil { + log.Fatal("Could not allocate audio codec context") + } + defer ff.AVCodec_free_context(ctx) + + // For some codecs, such as msmpeg4 and mpeg4, width and height MUST be initialized + // there because this information is not available in the bitstream. + + // open it + if err := ff.AVCodec_open(ctx, codec, nil); err != nil { + log.Fatal(err) + } + + // Read file + r, err := os.Open(*in) + if err != nil { + log.Fatal(err) + } + defer r.Close() + + // Decode packets and frames + frame := ff.AVUtil_frame_alloc() + if frame == nil { + log.Fatal("Could not allocate video frame") + } + defer ff.AVUtil_frame_free(frame) + + packet := ff.AVCodec_packet_alloc() + if packet == nil { + log.Fatal("Could not allocate packet") + } + defer ff.AVCodec_packet_free(packet) + + inbuf := make([]byte, INBUF_SIZE+ff.AV_INPUT_BUFFER_PADDING_SIZE) + data := inbuf +FOR_LOOP: + for { + var eof bool + data_size, err := r.Read(inbuf) + if err == io.EOF || data_size == 0 { + eof = true + } else if err != nil { + log.Fatal(err) + } + + // Use the parser to split the data into frames + data = inbuf[:data_size] + for data_size > 0 { + fmt.Println("parsing input data ", data_size) + size := ff.AVCodec_parser_parse(parser, ctx, packet, data, ff.AV_NOPTS_VALUE, ff.AV_NOPTS_VALUE, 0) + if size < 0 { + log.Fatal("Error while parsing") + } else { + fmt.Println(" parsed data ", size) + fmt.Println(" packet ", packet.Size()) + } + + // Adjust the input buffer beyond the parsed data + data = data[size:] + data_size -= size + + // Decode the packet + if packet.Size() > 0 { + if err := decode(ctx, frame, packet, *out); err != nil { + log.Fatal(err) + } + } + } + if eof { + break FOR_LOOP + } + } +} + +func decode(ctx *ff.AVCodecContext, frame *ff.AVFrame, packet *ff.AVPacket, out string) error { + if err := ff.AVCodec_send_packet(ctx, packet); err != nil { + return err + } + + for { + if err := ff.AVCodec_receive_frame(ctx, frame); errors.Is(err, syscall.EAGAIN) || errors.Is(err, io.EOF) { + return nil + } else if err != nil { + return err + } + + // The picture is allocated by the decoder. no need to free it + filename := filepath.Join(out, fmt.Sprintf("frame-%d.pgm", ctx.FrameNum())) + w, err := os.Create(filename) + if err != nil { + return err + } + defer w.Close() + + // Save frame + if err := pgm_save(w, frame); err != nil { + return err + } + } +} + +func pgm_save(w io.Writer, frame *ff.AVFrame) error { + width := frame.Width() + height := frame.Height() + + // Write the header + if _, err := fmt.Fprintf(w, "P5\n%d %d\n%d\n", width, height, 255); err != nil { + return err + } + + // Write the data + stride := frame.Linesize(0) + log.Print("stride ", stride) + log.Print("width ", width) + log.Print("height ", height) + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + pix := frame.Uint8(0)[y*stride+x] + if _, err := w.Write([]byte{pix}); err != nil { + return err + } + } + } + return nil +} + +/* + * Copyright (c) 2001 Fabrice Bellard + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ diff --git a/cmd/ffmpeg/demux_decode/main.go b/cmd/ffmpeg/demux_decode/main.go new file mode 100644 index 0000000..e396edc --- /dev/null +++ b/cmd/ffmpeg/demux_decode/main.go @@ -0,0 +1,327 @@ +package main + +import ( + "encoding/binary" + "errors" + "flag" + "fmt" + "io" + "log" + "os" + "syscall" + "unsafe" + + // Packages + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +// Create a structure to hold the image +type Image struct { + Data [][]byte + Stride []int + Size int +} + +func NewImage(width, height int, fmt ff.AVPixelFormat, align int) (*Image, error) { + // Allocate image + data, stride, size, err := ff.AVUtil_image_alloc(width, height, fmt, align) + if err != nil { + return nil, err + } else { + return &Image{Data: data, Stride: stride, Size: size}, nil + } +} + +func (i *Image) Free() { + ff.AVUtil_image_free(i.Data) +} + +// NativeEndian is the ByteOrder of the current system. +var NativeEndian binary.ByteOrder + +func init() { + // Examine the memory layout of an int16 to determine system + // endianness. + var one int16 = 1 + b := (*byte)(unsafe.Pointer(&one)) + if *b == 0 { + NativeEndian = binary.BigEndian + } else { + NativeEndian = binary.LittleEndian + } +} + +func main() { + in := flag.String("in", "", "input file") + aout := flag.String("audio-out", "", "raw audio output file") + vout := flag.String("video-out", "", "raw video output file") + flag.Parse() + + // Check in and out + if *in == "" || (*aout == "" && *vout == "") { + log.Fatal("-in and at least one of -audio-out and -video-out flags must be specified") + } + + // Ppen input file, and allocate format context + ctx, err := ff.AVFormat_open_url(*in, nil, nil) + if err != nil { + log.Fatal(err) + } + defer ff.AVFormat_close_input(ctx) + + // Find stream information + if err := ff.AVFormat_find_stream_info(ctx, nil); err != nil { + log.Fatal(err) + } + + // Dump the input format + ff.AVFormat_dump_format(ctx, 0, *in) + + var audio_decoder_ctx, video_decoder_ctx *ff.AVCodecContext + var wa, wv io.Writer + var image *Image + + audio_stream, video_stream := -1, -1 + + // Get decoder for audio, and create an output file + if *aout != "" { + if stream, ctx, err := open_codec_context(ctx, ff.AVMEDIA_TYPE_AUDIO); err != nil { + log.Fatal(err) + } else { + audio_decoder_ctx = ctx + audio_stream = stream + } + defer ff.AVCodec_free_context(audio_decoder_ctx) + + // Create output file + if w, err := os.Create(*aout); err != nil { + log.Fatal(err) + } else { + wa = w + } + } + + // Get decoder for video, and create an output file + if *vout != "" { + if stream, ctx, err := open_codec_context(ctx, ff.AVMEDIA_TYPE_VIDEO); err != nil { + log.Fatal(err) + } else { + video_decoder_ctx = ctx + video_stream = stream + } + defer ff.AVCodec_free_context(video_decoder_ctx) + + // Create output file + if w, err := os.Create(*vout); err != nil { + log.Fatal(err) + } else { + wv = w + } + + // Allocate an image for the video frame + if frame, err := NewImage(video_decoder_ctx.Width(), video_decoder_ctx.Height(), video_decoder_ctx.PixFmt(), 1); err != nil { + log.Fatal(err) + } else { + image = frame + } + defer image.Free() + } + + // Allocate a frame + frame := ff.AVUtil_frame_alloc() + if frame == nil { + log.Fatal("Could not allocate frame") + } + defer ff.AVUtil_frame_free(frame) + + // Allocate a packet + packet := ff.AVCodec_packet_alloc() + if packet == nil { + log.Fatal("failed to allocate packet") + } + defer ff.AVCodec_packet_free(packet) + + // Read frames + for { + err := ff.AVFormat_read_frame(ctx, packet) + if errors.Is(err, io.EOF) { + break + } + // check if the packet belongs to a stream we are interested in, otherwise skip it + if packet.StreamIndex() == audio_stream { + if err := decode_packet(wa, wv, audio_decoder_ctx, packet, frame, image); err != nil { + log.Fatal(err) + } + } else if packet.StreamIndex() == video_stream { + if err := decode_packet(wa, wv, video_decoder_ctx, packet, frame, image); err != nil { + log.Fatal(err) + } + } + + // Unreference the packet + ff.AVCodec_packet_unref(packet) + } + + // Flush the decoders + if audio_decoder_ctx != nil { + if err := decode_packet(wa, wv, audio_decoder_ctx, nil, frame, nil); err != nil { + log.Fatal(err) + } + } + if video_decoder_ctx != nil { + if err := decode_packet(wa, wv, video_decoder_ctx, nil, frame, image); err != nil { + log.Fatal(err) + } + } + + // Output command for playing the video + if *vout != "" { + log.Print("Play the output video file with the command:") + log.Printf(" ffplay -f rawvideo -pixel_format %s -video_size %dx%d %s", ff.AVUtil_get_pix_fmt_name(video_decoder_ctx.PixFmt()), video_decoder_ctx.Width(), video_decoder_ctx.Height(), *vout) + } + + // Output command for playing the audio + if *aout != "" { + fmt := audio_decoder_ctx.SampleFormat() + num_channels := audio_decoder_ctx.ChannelLayout().NumChannels() + if ff.AVUtil_sample_fmt_is_planar(audio_decoder_ctx.SampleFormat()) { + fmt = ff.AVUtil_get_packed_sample_fmt(fmt) + num_channels = 1 + log.Print("Warning: the sample format the decoder produced is planar. This example will output the first channel only.") + } + if fmt, err := get_format_from_sample_fmt(fmt); err != nil { + log.Fatal(err) + } else { + log.Print("Play the output audio file with the command:") + log.Printf(" ffplay -f %s -ar %d -ac %d -i %s", fmt, audio_decoder_ctx.SampleRate(), num_channels, *aout) + } + } +} + +func open_codec_context(ctx *ff.AVFormatContext, media_type ff.AVMediaType) (int, *ff.AVCodecContext, error) { + stream_num, codec, err := ff.AVFormat_find_best_stream(ctx, media_type, -1, -1) + if err != nil { + return -1, nil, err + } + + // Find the decoder for the stream + decoder := ff.AVCodec_find_decoder(codec.ID()) + if decoder == nil { + return -1, nil, fmt.Errorf("failed to find decoder for codec %s", codec.Name()) + } + + // Allocate a codec context for the decoder + dec_ctx := ff.AVCodec_alloc_context(decoder) + if dec_ctx == nil { + return -1, nil, fmt.Errorf("failed to allocate codec context for codec %s", codec.Name()) + } + + // Copy codec parameters from input stream to output codec context + stream := ctx.Stream(stream_num) + if err := ff.AVCodec_parameters_to_context(dec_ctx, stream.CodecPar()); err != nil { + ff.AVCodec_free_context(dec_ctx) + return -1, nil, fmt.Errorf("failed to copy codec parameters to decoder context for codec %s", codec.Name()) + } + + // Init the decoder + if err := ff.AVCodec_open(dec_ctx, decoder, nil); err != nil { + ff.AVCodec_free_context(dec_ctx) + return -1, nil, err + } + + // Return success + return stream_num, dec_ctx, nil +} + +func decode_packet(wa, wv io.Writer, ctx *ff.AVCodecContext, packet *ff.AVPacket, frame *ff.AVFrame, image *Image) error { + // submit the packet to the decoder + if err := ff.AVCodec_send_packet(ctx, packet); err != nil { + return err + } + + // get all the available frames from the decoder + for { + if err := ff.AVCodec_receive_frame(ctx, frame); errors.Is(err, syscall.EAGAIN) || errors.Is(err, io.EOF) { + // Finished decoding packet or EOF + return nil + } else if err != nil { + return err + } + + // write the frame data to output file + if ctx.Codec().Type() == ff.AVMEDIA_TYPE_AUDIO { + if err := write_audio_frame(wa, frame); err != nil { + return err + } + } else if ctx.Codec().Type() == ff.AVMEDIA_TYPE_VIDEO { + if err := write_video_frame(wv, frame, image); err != nil { + return err + } + } + } +} + +func write_audio_frame(w io.Writer, frame *ff.AVFrame) error { + /* Write the raw audio data samples of the first plane. This works + * fine for packed formats (e.g. AV_SAMPLE_FMT_S16). However, + * most audio decoders output planar audio, which uses a separate + * plane of audio samples for each channel (e.g. AV_SAMPLE_FMT_S16P). + * In other words, this code will write only the first audio channel + * in these cases. + * You should use libswresample or libavfilter to convert the frame + * to packed data. */ + log.Printf("audio_frame format:%s nb_samples:%d pts:%s", ff.AVUtil_get_sample_fmt_name(frame.SampleFormat()), frame.NumSamples(), ff.AVUtil_ts2str(frame.Pts())) + + n := frame.NumSamples() * ff.AVUtil_get_bytes_per_sample(frame.SampleFormat()) + data := frame.Uint8(0) + if _, err := w.Write(data[:n]); err != nil { + return err + } + + // Return success + return nil +} + +func write_video_frame(w io.Writer, frame *ff.AVFrame, image *Image) error { + // copy decoded frame to destination buffer: this is required since rawvideo expects non aligned data + log.Printf("video_frame format:%s size:%dx%d pts:%s", ff.AVUtil_get_pix_fmt_name(frame.PixFmt()), frame.Width(), frame.Height(), ff.AVUtil_ts2str(frame.Pts())) + + src_data, src_stride := frame.Data() + ff.AVUtil_image_copy(image.Data, image.Stride, src_data, src_stride, frame.PixFmt(), frame.Width(), frame.Height()) + + // Write each plane + for i := 0; i < len(image.Data); i++ { + if _, err := w.Write(image.Data[i]); err != nil { + return err + } + } + + // Return success + return nil +} + +func get_format_from_sample_fmt(sample_fmt ff.AVSampleFormat) (string, error) { + type sample_fmt_entry struct { + sample_fmt ff.AVSampleFormat + fmt_be string + fmt_le string + } + sample_fmt_entries := []sample_fmt_entry{ + {ff.AV_SAMPLE_FMT_U8, "u8", "u8"}, + {ff.AV_SAMPLE_FMT_S16, "s16be", "s16le"}, + {ff.AV_SAMPLE_FMT_S32, "s32be", "s32le"}, + {ff.AV_SAMPLE_FMT_FLT, "f32be", "f32le"}, + {ff.AV_SAMPLE_FMT_DBL, "f64be", "f64le"}, + } + + for _, entry := range sample_fmt_entries { + if sample_fmt == entry.sample_fmt { + if NativeEndian == binary.LittleEndian { + return entry.fmt_le, nil + } else { + return entry.fmt_be, nil + } + } + } + return "", errors.New("sample format is not supported as output format") +} diff --git a/cmd/ffmpeg/encode_audio/main.go b/cmd/ffmpeg/encode_audio/main.go new file mode 100644 index 0000000..c42cd81 --- /dev/null +++ b/cmd/ffmpeg/encode_audio/main.go @@ -0,0 +1,198 @@ +package main + +import ( + "errors" + "flag" + "io" + "log" + "math" + "os" + "slices" + "syscall" + + // Packages + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func main() { + out := flag.String("out", "", "output file") + flag.Parse() + + // Check out and size + if *out == "" { + log.Fatal("-out argument must be specified") + } + + // find the MP2 encoder + codec := ff.AVCodec_find_encoder(ff.AV_CODEC_ID_MP2) + if codec == nil { + log.Fatal("Codec not found") + } + + // Allocate a codec + ctx := ff.AVCodec_alloc_context(codec) + if ctx == nil { + log.Fatal("Could not allocate audio codec context") + } + defer ff.AVCodec_free_context(ctx) + + // Set codec parameters + ctx.SetBitRate(64000) + ctx.SetSampleFormat(ff.AV_SAMPLE_FMT_S16) + ctx.SetSampleRate(select_sample_rate(codec)) + if err := ctx.SetChannelLayout(ff.AV_CHANNEL_LAYOUT_MONO); err != nil { + log.Fatal(err) + } + + // Check + if !check_sample_fmt(codec, ctx.SampleFormat()) { + log.Fatalf("Encoder does not support sample format %v", ctx.SampleFormat()) + } + + // Open the codec + if err := ff.AVCodec_open(ctx, codec, nil); err != nil { + log.Fatal(err) + } + + // Create the file + w, err := os.Create(*out) + if err != nil { + log.Fatal(err) + } + defer w.Close() + + // Packet for holding encoded data + pkt := ff.AVCodec_packet_alloc() + if pkt == nil { + log.Fatal("Could not allocate packet") + } + defer ff.AVCodec_packet_free(pkt) + + // Frame containing input raw audio + frame := ff.AVUtil_frame_alloc() + if frame == nil { + log.Fatal("Could not allocate audio frame") + } + defer ff.AVUtil_frame_free(frame) + + // Set the frame parameters + frame.SetNumSamples(ctx.FrameSize()) + frame.SetSampleFormat(ctx.SampleFormat()) + if err := frame.SetChannelLayout(ctx.ChannelLayout()); err != nil { + log.Fatal(err) + } + + // Allocate the data buffers + if err := ff.AVUtil_frame_get_buffer(frame, 0); err != nil { + log.Fatal(err) + } + + // Encode a single tone sound + t := float64(0) + tincr := 2 * math.Pi * 440.0 / float64(ctx.SampleRate()) + num_channels := ctx.ChannelLayout().NumChannels() + + for i := 0; i < 200; i++ { + log.Println("frame", i) + + // Make sure the frame is writable -- makes a copy if the encoder kept a reference internally + if err := ff.AVUtil_frame_make_writable(frame); err != nil { + log.Fatal(err) + } + + // Set samples in the frame + samples := frame.Int16(0) + for j := 0; j < ctx.FrameSize(); j++ { + // Set sample on first channel + samples[j*num_channels] = (int16)(math.Sin(t) * 10000) + + // Copy to other channels + for k := 1; k < num_channels; k++ { + samples[j+k] = samples[j] + } + + // Increment the time + t += tincr + } + + // Encode the frame + if err := encode(w, ctx, frame, pkt); err != nil { + log.Fatal(err) + } + } + + // Flush the encoder + log.Println("flush") + if err := encode(w, ctx, nil, pkt); err != nil { + log.Fatal(err) + } +} + +// Check that a given sample format is supported by the encoder +func check_sample_fmt(codec *ff.AVCodec, sample_fmt ff.AVSampleFormat) bool { + return slices.Contains(codec.SampleFormats(), sample_fmt) +} + +// Pick the highest supported samplerate +func select_sample_rate(codec *ff.AVCodec) int { + samplerates := codec.SupportedSamplerates() + if len(samplerates) == 0 { + return 44100 + } + best_samplerate := 0 + for _, rate := range samplerates { + if rate > best_samplerate { + best_samplerate = rate + } + } + return best_samplerate +} + +func encode(w io.Writer, ctx *ff.AVCodecContext, frame *ff.AVFrame, pkt *ff.AVPacket) error { + // Send the frame for encoding, if the frame is nil then flush instead + log.Println(" send frame") + if err := ff.AVCodec_send_frame(ctx, frame); err != nil { + log.Println("Error sending frame", err) + return err + } + + // Read all the available output packets (in general there may be any number of them) + for { + log.Println(" receive_packet") + if err := ff.AVCodec_receive_packet(ctx, pkt); errors.Is(err, syscall.EAGAIN) || errors.Is(err, io.EOF) { + return nil + } else if err != nil { + log.Println("AVCodec_receive_packet error", err) + return err + } + + // Write the packet to the output file + if _, err := w.Write(pkt.Bytes()); err != nil { + return err + } + // Release packet data + ff.AVCodec_packet_unref(pkt) + } +} + +/* + * Copyright (c) 2001 Fabrice Bellard + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ diff --git a/cmd/ffmpeg/encode_video/main.go b/cmd/ffmpeg/encode_video/main.go new file mode 100644 index 0000000..cb6e36e --- /dev/null +++ b/cmd/ffmpeg/encode_video/main.go @@ -0,0 +1,218 @@ +package main + +import ( + "errors" + "flag" + "fmt" + "io" + "log" + "os" + "syscall" + + // Packages + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func main() { + out := flag.String("out", "", "output file") + codec_name := flag.String("codec", "mpeg1video", "codec to use") + size := flag.String("size", "352x288", "video size") + flag.Parse() + + // Check out and size + if *out == "" { + log.Fatal("-out argument must be specified") + } + width, height, err := ff.AVUtil_parse_video_size(*size) + if err != nil { + log.Fatal(err) + } + + /* find the mpeg1video encoder */ + codec := ff.AVCodec_find_encoder_by_name(*codec_name) + if codec == nil { + log.Fatal("Codec not found") + } + + // Allocate a codec + ctx := ff.AVCodec_alloc_context(codec) + if ctx == nil { + log.Fatal("Could not allocate video codec context") + } + defer ff.AVCodec_free_context(ctx) + + // Set codec parameters + ctx.SetBitRate(400000) + ctx.SetWidth(width) // resolution must be a multiple of two + ctx.SetHeight(height) + ctx.SetTimeBase(ff.AVUtil_rational(1, 25)) + ctx.SetFramerate(ff.AVUtil_rational(25, 1)) + + // Emit one intra frame every ten frames. Check frame pict_type before passing frame + // to encoder, if frame->pict_type is AV_PICTURE_TYPE_I then gop_size is ignored and + // the output of encoder will always be I frame irrespective to gop_size + ctx.SetGopSize(10) + ctx.SetMaxBFrames(1) + ctx.SetPixFmt(ff.AV_PIX_FMT_YUV420P) + if codec.ID() == ff.AV_CODEC_ID_H264 { + if err := ctx.SetPrivDataKV("preset", "slow"); err != nil { + log.Fatal(err) + } + } + + // Open the codec + if err := ff.AVCodec_open(ctx, codec, nil); err != nil { + log.Fatal(err) + } + + // Create the file + w, err := os.Create(*out) + if err != nil { + log.Fatal(err) + } + defer w.Close() + + // Packet for holding encoded data + pkt := ff.AVCodec_packet_alloc() + if pkt == nil { + log.Fatal("Could not allocate packet") + } + defer ff.AVCodec_packet_free(pkt) + + // Frame containing input raw audio + frame := ff.AVUtil_frame_alloc() + if frame == nil { + log.Fatal("Could not allocate video frame") + } + defer ff.AVUtil_frame_free(frame) + + // Set the frame parameters + frame.SetPixFmt(ctx.PixFmt()) + frame.SetWidth(ctx.Width()) + frame.SetHeight(ctx.Height()) + + // Allocate the data buffers + if err := ff.AVUtil_frame_get_buffer(frame, 0); err != nil { + log.Fatal(err) + } + + // Encode 5 seconds of video + for i := 0; i < 25*5; i++ { + // Make sure the frame data is writable. + // On the first round, the frame is fresh from av_frame_get_buffer() + // and therefore we know it is writable. + // But on the next rounds, encode() will have called + // avcodec_send_frame(), and the codec may have kept a reference to + // the frame in its internal structures, that makes the frame + // unwritable. + // av_frame_make_writable() checks that and allocates a new buffer + // for the frame only if necessary. + if err := ff.AVUtil_frame_make_writable(frame); err != nil { + log.Fatal(err) + } + + // Prepare a dummy image. In real code, this is where you would have your own logic for + // filling the frame. FFmpeg does not care what you put in the frame. + fill_yuv_image(frame, i) + + // Set timestamp in the frame + frame.SetPts(int64(i)) + + // Encode the image + if err := encode(w, ctx, frame, pkt); err != nil { + log.Fatal(err) + } + } + + // Flush the encoder + log.Println("flush") + if err := encode(w, ctx, nil, pkt); err != nil { + log.Fatal(err) + } + + // Add sequence end code to have a real MPEG file. It makes only sense because this tiny examples writes packets + // directly. This is called "elementary stream" and only works for some codecs. To create a valid file, you + // usually need to write packets into a proper file format or protocol; see mux.c. + if codec.ID() == ff.AV_CODEC_ID_MPEG1VIDEO || codec.ID() == ff.AV_CODEC_ID_MPEG2VIDEO { + w.Write([]byte{0, 0, 1, 0xb7}) + } +} + +func encode(w io.Writer, ctx *ff.AVCodecContext, frame *ff.AVFrame, pkt *ff.AVPacket) error { + // Send the frame for encoding, if the frame is nil then flush instead + if frame != nil { + log.Println(" send frame pts", frame.Pts()) + } + if err := ff.AVCodec_send_frame(ctx, frame); err != nil { + log.Println("Error sending frame", err) + return err + } + + // Read all the available output packets (in general there may be any number of them) + for { + log.Println(" receive_packet") + if err := ff.AVCodec_receive_packet(ctx, pkt); errors.Is(err, syscall.EAGAIN) || errors.Is(err, io.EOF) { + return nil + } else if err != nil { + log.Println("AVCodec_receive_packet error", err) + return err + } + // Write the packet to the output file + //log.Println(" write_packet: ", pkt) + if _, err := w.Write(pkt.Bytes()); err != nil { + return err + } + // Release packet data + ff.AVCodec_packet_unref(pkt) + } +} + +func fill_yuv_image(frame *ff.AVFrame, frame_index int) { + width := frame.Width() + height := frame.Height() + + /* Y */ + ydata := frame.Uint8(0) + ystride := frame.Linesize(0) + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + ydata[y*ystride+x] = uint8(x + y + frame_index*3) + } + } + + /* Cb and Cr */ + cbdata := frame.Uint8(1) + cbstride := frame.Linesize(1) + crdata := frame.Uint8(2) + crstride := frame.Linesize(2) + fmt.Println("cbstride", cbstride, "crstride", crstride) + + for y := 0; y < height>>1; y++ { + for x := 0; x < width>>1; x++ { + cbdata[y*cbstride+x] = uint8(128 + y + frame_index*2) + crdata[y*crstride+x] = uint8(64 + x + frame_index*5) + } + } +} + +/* + * Copyright (c) 2001 Fabrice Bellard + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ diff --git a/cmd/ffmpeg/mux/generate.go b/cmd/ffmpeg/mux/generate.go new file mode 100644 index 0000000..9207b6f --- /dev/null +++ b/cmd/ffmpeg/mux/generate.go @@ -0,0 +1,112 @@ +package main + +import ( + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +//////////////////////////////////////////////////////////////////////////////// + + +// Prepare a 16 bit dummy audio frame of 'frame_size' samples and 'nb_channels' channels +func get_audio_frame(stream *Stream) *AVFrame { + AVFrame *frame = stream.tmp_frame + + int j, i, v; + int16_t *q = (int16_t*)frame->data[0]; + + /* check if we want to generate more frames */ + if (av_compare_ts(ost->next_pts, ost->enc->time_base, + STREAM_DURATION, (AVRational){ 1, 1 }) > 0) + return NULL; + + for (j = 0; j nb_samples; j++) { + v = (int)(sin(ost->t) * 10000); + for (i = 0; i < ost->enc->ch_layout.nb_channels; i++) + *q++ = v; + ost->t += ost->tincr; + ost->tincr += ost->tincr2; + } + + frame->pts = ost->next_pts; + ost->next_pts += frame->nb_samples; + + return frame; + } + +/* + * encode one audio frame and send it to the muxer + * return true when encoding is finished + */ +func write_audio_frame(ctx *ff.AVFormatContext, stream *Stream) bool { + frame := get_audio_frame(stream) + if frame != nil { + // convert samples from native format to destination codec format, using the resampler + // compute destination number of samples + delay := ff.SWResample_get_delay(stream.SWRContext(), stream.Encoder().SampleRate()) + frame.NumSamples() + dst_nb_samples := ff.AVUtil_rescale_rnd(delay,ctx.SampleRate(),ctx.SampleRate(),ff.AV_ROUND_UP); + + // When we pass a frame to the encoder, it may keep a reference to it internally; make sure we do not overwrite it here + ff.AVUtil_frame_make_writable(stream.Frame()) + + // Convert to destination format + ff.SWResample_convert_frame(stream.swr_ctx,frame,stream.frame) + + return false + } + frame = ost->frame; + + frame->pts = av_rescale_q(ost->samples_count, (AVRational){1, c->sample_rate}, c->time_base); + ost->samples_count += dst_nb_samples; + } + return write_frame(ctx, stream, frame) +} + +func write_video_frame(ctx *ff.AVFormatContext, stream *Stream) bool { + return true +} + + + + + static int write_audio_frame(AVFormatContext *oc, OutputStream *ost) + { + AVCodecContext *c; + AVFrame *frame; + int ret; + int dst_nb_samples; + + c = ost->enc; + + frame = get_audio_frame(ost); + + if (frame) { + /* 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); + + /* when we pass a frame to the encoder, it may keep a reference to it + * internally; + * make sure we do not overwrite it here + */ + ret = av_frame_make_writable(ost->frame); + if (ret < 0) + exit(1); + + /* convert to destination format */ + ret = swr_convert(ost->swr_ctx, + ost->frame->data, dst_nb_samples, + (const uint8_t **)frame->data, frame->nb_samples); + if (ret < 0) { + fprintf(stderr, "Error while converting\n"); + exit(1); + } + frame = ost->frame; + + frame->pts = av_rescale_q(ost->samples_count, (AVRational){1, c->sample_rate}, c->time_base); + ost->samples_count += dst_nb_samples; + } + + 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 new file mode 100644 index 0000000..8d70f7a --- /dev/null +++ b/cmd/ffmpeg/mux/main.go @@ -0,0 +1,125 @@ +package main + +import ( + "flag" + "fmt" + "log" + + // Packages + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +const ( + STREAM_DURATION = 10.0 +) + +func main() { + out := flag.String("out", "", "output file") + flag.Parse() + + // Check in and out + if *out == "" { + log.Fatal("-out flag must be specified") + } + + // Allocate the output media context + ctx, err := ff.AVFormat_create_file(*out, nil) + if err != nil { + log.Fatal(err) + } + defer ff.AVFormat_close_writer(ctx) + + // Add the audio and video streams using the default format codecs and initialize the codecs. + var video, audio *Stream + if codec := ctx.Output().VideoCodec(); codec != ff.AV_CODEC_ID_NONE { + if stream, err := NewStream(ctx, codec); err != nil { + log.Fatalf("could not add video stream: %v", err) + } else { + video = stream + } + defer video.Close() + } + if codec := ctx.Output().AudioCodec(); codec != ff.AV_CODEC_ID_NONE { + if stream, err := NewStream(ctx, codec); err != nil { + log.Fatalf("could not add audio stream: %v", err) + } else { + audio = stream + } + defer audio.Close() + } + + // Now that all the parameters are set, we can open the audio + // and video codecs and allocate the necessary encode buffers. + if video != nil { + // TODO: AVDictionary of options + if err := video.Open(nil); err != nil { + log.Fatalf("could not open video codec: %v", err) + } + } + if audio != nil { + // TODO: AVDictionary of options + if err := audio.Open(nil); err != nil { + log.Fatalf("could not open audio codec: %v", err) + } + } + + fmt.Println(ctx) + + // Dump the output format + ff.AVFormat_dump_format(ctx, 0, *out) + + // Open the output file, if needed + if !ctx.Flags().Is(ff.AVFMT_NOFILE) { + w, err := ff.AVFormat_avio_open(*out, ff.AVIO_FLAG_WRITE) + if err != nil { + log.Fatalf("could not open output file: %v", err) + } else { + ctx.SetPb(w) + } + defer ff.AVFormat_avio_close(w) + } + + // Write the stream header, if any + // TODO: AVDictionary of options + if err := ff.AVFormat_write_header(ctx, nil); err != nil { + log.Fatalf("could not write header: %v", err) + } + + // TODO Write data + encode_audio, encode_video := true, true + 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) + } else { + encode_audio = !write_audio_frame(ctx, audio) + } + } + + // Write the trailer + if err := ff.AVFormat_write_trailer(ctx); err != nil { + log.Fatalf("could not write trailer: %v", err) + } +} + +/* + * Copyright (c) 2003 Fabrice Bellard + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ diff --git a/cmd/ffmpeg/mux/stream.go b/cmd/ffmpeg/mux/stream.go new file mode 100644 index 0000000..096e702 --- /dev/null +++ b/cmd/ffmpeg/mux/stream.go @@ -0,0 +1,256 @@ +package main + +import ( + "encoding/json" + "errors" + "math" + + // Packages + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +//////////////////////////////////////////////////////////////////////////////// + +// a wrapper around an output AVStream +type Stream struct { + // Main parameters + Codec *ff.AVCodec + Encoder *ff.AVCodecContext + Stream *ff.AVStream + + tmp_packet *ff.AVPacket + next_pts int64 // pts of the next frame that will be generated + samples_count int + frame *ff.AVFrame + tmp_frame *ff.AVFrame + packet *ff.AVPacket + t, tincr, tincr2 float64 + sws_ctx *ff.SWSContext + swr_ctx *ff.SWRContext +} + +func (stream *Stream) String() string { + data, _ := json.MarshalIndent(stream, "", " ") + return string(data) +} + +//////////////////////////////////////////////////////////////////////////////// + +// Create a new output stream, add it to the media context and initialize the codec. +func NewStream(ctx *ff.AVFormatContext, codec_id ff.AVCodecID) (*Stream, error) { + stream := &Stream{} + + // Codec + codec := ff.AVCodec_find_encoder(codec_id) + if codec == nil { + return nil, errors.New("could not find codec") + } else { + stream.Codec = codec + } + + // Packet + if packet := ff.AVCodec_packet_alloc(); packet == nil { + return nil, errors.New("could not allocate packet") + } else { + stream.tmp_packet = packet + } + + // Stream + if str := ff.AVFormat_new_stream(ctx, nil); str == nil { + ff.AVCodec_packet_free(stream.tmp_packet) + return nil, errors.New("could not allocate stream") + } else { + stream_id := int(ctx.NumStreams()) + stream.Stream = str + stream.Stream.SetId(stream_id) + } + + // Codec context + if encoder := ff.AVCodec_alloc_context(codec); encoder == nil { + ff.AVCodec_packet_free(stream.tmp_packet) + return nil, errors.New("could not allocate codec context") + } else { + stream.Encoder = encoder + } + + // Set parameters for the encoder + switch stream.Codec.Type() { + case ff.AVMEDIA_TYPE_AUDIO: + if fmts := stream.Codec.SampleFormats(); len(fmts) > 0 { + stream.Encoder.SetSampleFormat(fmts[0]) + } else { + stream.Encoder.SetSampleFormat(ff.AV_SAMPLE_FMT_FLTP) + } + if rates := stream.Codec.SupportedSamplerates(); len(rates) > 0 { + stream.Encoder.SetSampleRate(rates[0]) + } else { + stream.Encoder.SetSampleRate(44100) + } + stream.Encoder.SetBitRate(64000) + if err := stream.Encoder.SetChannelLayout(ff.AV_CHANNEL_LAYOUT_STEREO); err != nil { + ff.AVCodec_packet_free(stream.tmp_packet) + return nil, err + } + stream.Stream.SetTimeBase(ff.AVUtil_rational(1, stream.Encoder.SampleRate())) + case ff.AVMEDIA_TYPE_VIDEO: + stream.Encoder.SetBitRate(400000) + // Resolution must be a multiple of two. + stream.Encoder.SetWidth(352) + stream.Encoder.SetHeight(288) + /* timebase: This is the fundamental unit of time (in seconds) in terms + * of which frame timestamps are represented. For fixed-fps content, + * timebase should be 1/framerate and timestamp increments should be + * identical to 1. */ + stream.Stream.SetTimeBase(ff.AVUtil_rational(1, 25)) + stream.Encoder.SetTimeBase(stream.Stream.TimeBase()) + stream.Encoder.SetGopSize(12) /* emit one intra frame every twelve frames at most */ + stream.Encoder.SetPixFmt(ff.AV_PIX_FMT_YUV420P) + + if stream.Codec.ID() == ff.AV_CODEC_ID_MPEG2VIDEO { + /* just for testing, we also add B frames */ + stream.Encoder.SetMaxBFrames(2) + } + if stream.Codec.ID() == ff.AV_CODEC_ID_MPEG1VIDEO { + /* Needed to avoid using macroblocks in which some coeffs overflow. + * This does not happen with normal video, it just happens here as + * the motion of the chroma plane does not match the luma plane. */ + stream.Encoder.SetMbDecision(ff.FF_MB_DECISION_SIMPLE) + } + } + + // Some formats want stream headers to be separate + if ctx.Output().Flags().Is(ff.AVFMT_GLOBALHEADER) { + stream.Encoder.SetFlags(stream.Encoder.Flags() | ff.AV_CODEC_FLAG_GLOBAL_HEADER) + } + + // Return success + return stream, nil +} + +func (stream *Stream) Close() { + ff.AVCodec_packet_free(stream.tmp_packet) + ff.AVCodec_free_context(stream.Encoder) + ff.AVUtil_frame_free(stream.frame) + ff.AVUtil_frame_free(stream.tmp_frame) + ff.SWResample_free(stream.swr_ctx) +} + +func (stream *Stream) Open(opts *ff.AVDictionary) error { + // Create a copy of the opts + opt, err := ff.AVUtil_dict_copy(opts, 0) + if err != nil { + return err + } + defer ff.AVUtil_dict_free(opt) + + // Open the codec + if err := ff.AVCodec_open(stream.Encoder, stream.Codec, opt); err != nil { + return err + } + + switch stream.Codec.Type() { + case ff.AVMEDIA_TYPE_AUDIO: + stream.t = 0 + // increment frequency by 110 Hz per second + stream.tincr = 2 * math.Pi * 110.0 / float64(stream.Encoder.SampleRate()) + stream.tincr2 = 2 * math.Pi * 110.0 / float64(stream.Encoder.SampleRate()) / float64(stream.Encoder.SampleRate()) + + // Number of samples in a frame + nb_samples := stream.Encoder.FrameSize() + if stream.Codec.Capabilities().Is(ff.AV_CODEC_CAP_VARIABLE_FRAME_SIZE) { + nb_samples = 10000 + } + + if frame, err := alloc_audio_frame(stream.Encoder.SampleFormat(), stream.Encoder.ChannelLayout(), stream.Encoder.SampleRate(), nb_samples); err != nil { + return err + } else { + stream.frame = frame + } + if frame, err := alloc_audio_frame(ff.AV_SAMPLE_FMT_S16, stream.Encoder.ChannelLayout(), stream.Encoder.SampleRate(), nb_samples); err != nil { + return err + } else { + stream.tmp_frame = frame + } + // create resampler context + if swr_ctx := ff.SWResample_alloc(); swr_ctx == nil { + return errors.New("could not allocate resample context") + } else if err := ff.SWResample_set_opts(swr_ctx, + stream.Encoder.ChannelLayout(), stream.Encoder.SampleFormat(), stream.Encoder.SampleRate(), // out + stream.Encoder.ChannelLayout(), ff.AV_SAMPLE_FMT_S16, stream.Encoder.SampleRate(), // in + ); err != nil { + ff.SWResample_free(swr_ctx) + return err + } else if err := ff.SWResample_init(swr_ctx); err != nil { + ff.SWResample_free(swr_ctx) + return err + } else { + stream.swr_ctx = swr_ctx + } + case ff.AVMEDIA_TYPE_VIDEO: + // Allocate a re-usable frame + if frame, err := alloc_video_frame(stream.Encoder.PixFmt(), stream.Encoder.Width(), stream.Encoder.Height()); err != nil { + return err + } else { + stream.frame = frame + } + // If the output format is not YUV420P, then a temporary YUV420P picture is needed too. It is then converted to the required + // output format. + if stream.Encoder.PixFmt() != ff.AV_PIX_FMT_YUV420P { + if frame, err := alloc_video_frame(ff.AV_PIX_FMT_YUV420P, stream.Encoder.Width(), stream.Encoder.Height()); err != nil { + return err + } else { + stream.tmp_frame = frame + } + } + } + + // copy the stream parameters to the muxer + if err := ff.AVCodec_parameters_from_context(stream.Stream.CodecPar(), stream.Encoder); err != nil { + return err + } + + // Return success + return nil +} + +func alloc_video_frame(pix_fmt ff.AVPixelFormat, width, height int) (*ff.AVFrame, error) { + frame := ff.AVUtil_frame_alloc() + if frame == nil { + return nil, errors.New("could not allocate video frame") + } + frame.SetWidth(width) + frame.SetHeight(height) + frame.SetPixFmt(pix_fmt) + + // allocate the buffers for the frame data + if err := ff.AVUtil_frame_get_buffer(frame, 0); err != nil { + ff.AVUtil_frame_free(frame) + return nil, err + } + + // Return success + return frame, nil +} + +func alloc_audio_frame(sample_fmt ff.AVSampleFormat, channel_layout ff.AVChannelLayout, sample_rate, nb_samples int) (*ff.AVFrame, error) { + frame := ff.AVUtil_frame_alloc() + if frame == nil { + return nil, errors.New("could not allocate audio frame") + } + frame.SetSampleFormat(sample_fmt) + frame.SetSampleRate(sample_rate) + frame.SetNumSamples(nb_samples) + if err := frame.SetChannelLayout(channel_layout); err != nil { + ff.AVUtil_frame_free(frame) + return nil, err + } + + // allocate the buffers for the frame data + if err := ff.AVUtil_frame_get_buffer(frame, 0); err != nil { + ff.AVUtil_frame_free(frame) + return nil, err + } + + // Return success + return frame, nil +} diff --git a/cmd/ffmpeg/remux/main.go b/cmd/ffmpeg/remux/main.go new file mode 100644 index 0000000..6458b7b --- /dev/null +++ b/cmd/ffmpeg/remux/main.go @@ -0,0 +1,153 @@ +package main + +import ( + "flag" + "fmt" + "io" + "log" + + // Packages + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func main() { + in := flag.String("in", "", "input file") + out := flag.String("out", "", "output file") + flag.Parse() + + // Check in and out + if *in == "" || *out == "" { + log.Fatal("-in and -out files must be specified") + } + + // Allocate a packet + pkt := ff.AVCodec_packet_alloc() + if pkt == nil { + log.Fatal("failed to allocate packet") + } + defer ff.AVCodec_packet_free(pkt) + + // Open input file + input, err := ff.AVFormat_open_url(*in, nil, nil) + if err != nil { + log.Fatal(err) + } + defer ff.AVFormat_close_input(input) + + // Find stream information + if err := ff.AVFormat_find_stream_info(input, nil); err != nil { + log.Fatal(err) + } + + // Dump the input format + ff.AVFormat_dump_format(input, 0, *in) + + // Open the output file + output, err := ff.AVFormat_create_file(*out, nil) + if err != nil { + log.Fatal(err) + } + defer ff.AVFormat_close_writer(output) + + // Stream mapping + stream_map := make([]int, input.NumStreams()) + stream_index := 0 + for i := range stream_map { + in_stream := input.Stream(i) + in_codec_par := in_stream.CodecPar() + + // Only copy audio and video streams + if in_codec_par.CodecType() != ff.AVMEDIA_TYPE_AUDIO && in_codec_par.CodecType() != ff.AVMEDIA_TYPE_VIDEO { + stream_map[i] = -1 + continue + } + + // Create a new stream + stream_map[i] = stream_index + out_stream := ff.AVFormat_new_stream(output, nil) + if out_stream == nil { + log.Fatal("failed to create new stream") + } else if err := ff.AVCodec_parameters_copy(out_stream.CodecPar(), in_codec_par); err != nil { + log.Fatal(err) + } else { + out_stream.CodecPar().SetCodecTag(0) + } + + // Increment stream index + stream_index = stream_index + 1 + } + + // Dump the output format + ff.AVFormat_dump_format(output, 0, *out) + + // Write the header + if err := ff.AVFormat_write_header(output, nil); err != nil { + log.Fatal(err) + } + + // Write the frames + for { + // Read a frame from the inout + if err := ff.AVFormat_read_frame(input, pkt); err != nil { + if err == io.EOF { + break + } else if err != nil { + log.Fatal(err) + } + } + + out_stream_index := stream_map[pkt.StreamIndex()] + if out_stream_index < 0 { + continue + } + + // Rescale the time stamp for the packet + in_stream := input.Stream(pkt.StreamIndex()) + out_stream := output.Stream(out_stream_index) + ff.AVCodec_packet_rescale_ts(pkt, in_stream.TimeBase(), out_stream.TimeBase()) + + // Write the packet + pkt.SetPos(-1) + log_packet(output, pkt, "out") + if err := ff.AVFormat_interleaved_write_frame(output, pkt); err != nil { + log.Fatal(err) + } + } + + // Write the trailer + if err := ff.AVFormat_write_trailer(output); err != nil { + log.Fatal(err) + } +} + +func log_packet(ctx *ff.AVFormatContext, pkt *ff.AVPacket, tag string) { + stream_index := pkt.StreamIndex() + tb := ctx.Stream(stream_index).TimeBase() + fmt.Printf("%4s stream %d: pts: %-8s pts_time: %-10s dts: %-8s dts_time: %-10s\n", + tag, stream_index, + ff.AVUtil_ts2str(pkt.Pts()), ff.AVUtil_ts2timestr(pkt.Pts(), &tb), + ff.AVUtil_ts2str(pkt.Dts()), ff.AVUtil_ts2timestr(pkt.Dts(), &tb), + ) +} + +/* + * Copyright (c) 2001 Fabrice Bellard + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ diff --git a/cmd/ffmpeg/resample_audio/main.go b/cmd/ffmpeg/resample_audio/main.go new file mode 100644 index 0000000..198eb73 --- /dev/null +++ b/cmd/ffmpeg/resample_audio/main.go @@ -0,0 +1,204 @@ +package main + +import ( + "encoding/binary" + "errors" + "flag" + "log" + "math" + "os" + "unsafe" + + // Packages + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func main() { + out := flag.String("out", "", "output file") + flag.Parse() + + // Check flags + if *out == "" { + log.Fatal("-out flag must be specified") + } + + // Create a resampler context + ctx := ff.SWResample_alloc() + if ctx == nil { + log.Fatal("could not allocate resampler context") + } + defer ff.SWResample_free(ctx) + + // Source needs to be non-planar + src_ch_layout := ff.AV_CHANNEL_LAYOUT_MONO + src_format := ff.AV_SAMPLE_FMT_S16 + src_nb_samples := 1024 + src_rate := 44100 + + // Destination needs to be non-planar + dest_ch_layout := ff.AV_CHANNEL_LAYOUT_MONO + dest_format := ff.AV_SAMPLE_FMT_U8 + dest_rate := 48000 + + if err := ff.SWResample_set_opts(ctx, + dest_ch_layout, dest_format, dest_rate, + src_ch_layout, src_format, src_rate, + ); err != nil { + log.Fatal(err) + } + + // initialize the resampling context + if err := ff.SWResample_init(ctx); err != nil { + log.Fatal(err) + } + + // Allocate source and destination samples buffers + src, err := ff.AVUtil_samples_alloc(src_nb_samples, src_ch_layout.NumChannels(), src_format, false) + if err != nil { + log.Fatal(err) + } + defer ff.AVUtil_samples_free(src) + + // Open destination file + w, err := os.Create(*out) + if err != nil { + log.Fatal(err) + } + defer w.Close() + + dest_nb_samples := int(float64(src_nb_samples) * float64(dest_rate) / float64(src_rate)) + max_dest_nb_samples := dest_nb_samples + dest, err := ff.AVUtil_samples_alloc(dest_nb_samples, dest_ch_layout.NumChannels(), dest_format, false) + if err != nil { + log.Fatal(err) + } + + t := float64(0) + for t < 10 { + ff.AVUtil_samples_set_silence(src, 0, src_nb_samples) + + // Generate synthetic audio + t = fill_samples(src, src_rate, t) + + // Calculate destination number of samples + dest_nb_samples = int(ff.SWResample_get_delay(ctx, int64(src_rate))) + int(float64(src_nb_samples)*float64(dest_rate)/float64(src_rate)) + if dest_nb_samples > max_dest_nb_samples { + ff.AVUtil_samples_free(dest) + dest, err = ff.AVUtil_samples_alloc(dest_nb_samples, dest_ch_layout.NumChannels(), dest_format, true) + if err != nil { + log.Fatal(err) + } + max_dest_nb_samples = dest_nb_samples + } + + // convert to destination format + n, err := ff.SWResample_convert(ctx, dest, dest_nb_samples, src, src_nb_samples) + if err != nil { + log.Fatal(err) + } + + // Calulate the number of samples converted + _, dest_planesize, err := ff.AVUtil_samples_get_buffer_size(n, dest_ch_layout.NumChannels(), dest_format, true) + if err != nil { + log.Fatal(err) + } + + // We only write the first plane - non-planar format + if _, err := w.Write(dest.Bytes(0)[:dest_planesize]); err != nil { + log.Fatal(err) + } + } + + ff.AVUtil_samples_free(dest) + + if fmt, err := get_format_from_sample_fmt(dest_format); err != nil { + log.Fatal(err) + } else if desc, err := ff.AVUtil_channel_layout_describe(&dest_ch_layout); err != nil { + log.Fatal(err) + } else { + log.Printf("Resampling succeeded. Play the output file with the command:") + log.Printf(" ffplay -f %s -channel_layout %s -ar %d %s\n", fmt, desc, dest_rate, *out) + } +} + +/** + * Fill buffer with nb_samples, generated starting from time t. + */ +func fill_samples(data *ff.AVSamples, sample_rate int, t float64) float64 { + tincr := 1.0 / float64(sample_rate) + buf := data.Int16(0) + c := 2.0 * math.Pi * 440.0 + + // Generate sin tone with 440Hz frequency and duplicated channels + for i := 0; i < data.NumSamples(); i += data.NumChannels() { + sample := math.Sin(c * t) + for j := 0; j < data.NumChannels(); j++ { + buf[i+j] = int16(sample * 0.5 * math.MaxInt16) + } + t = t + tincr + } + return t +} + +// NativeEndian is the ByteOrder of the current system. +var NativeEndian binary.ByteOrder + +func init() { + // Examine the memory layout of an int16 to determine system + // endianness. + var one int16 = 1 + b := (*byte)(unsafe.Pointer(&one)) + if *b == 0 { + NativeEndian = binary.BigEndian + } else { + NativeEndian = binary.LittleEndian + } +} + +func get_format_from_sample_fmt(sample_fmt ff.AVSampleFormat) (string, error) { + type sample_fmt_entry struct { + sample_fmt ff.AVSampleFormat + fmt_be string + fmt_le string + } + sample_fmt_entries := []sample_fmt_entry{ + {ff.AV_SAMPLE_FMT_U8, "u8", "u8"}, + {ff.AV_SAMPLE_FMT_S16, "s16be", "s16le"}, + {ff.AV_SAMPLE_FMT_S32, "s32be", "s32le"}, + {ff.AV_SAMPLE_FMT_FLT, "f32be", "f32le"}, + {ff.AV_SAMPLE_FMT_DBL, "f64be", "f64le"}, + } + + for _, entry := range sample_fmt_entries { + if sample_fmt == entry.sample_fmt { + if NativeEndian == binary.LittleEndian { + return entry.fmt_le, nil + } else { + return entry.fmt_be, nil + } + } + } + return "", errors.New("sample format is not supported as output format") +} + +/* + * Copyright (c) 2012 Stefano Sabatini + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ diff --git a/cmd/ffmpeg/scale_video/main.go b/cmd/ffmpeg/scale_video/main.go new file mode 100644 index 0000000..9ca6c36 --- /dev/null +++ b/cmd/ffmpeg/scale_video/main.go @@ -0,0 +1,114 @@ +package main + +import ( + "flag" + "log" + "os" + + // Packages + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +const ( + SRC_WIDTH = 320 + SRC_HEIGHT = 240 + SRC_PIX_FMT = ff.AV_PIX_FMT_YUV420P + DEST_PIX_FMT = ff.AV_PIX_FMT_RGB24 +) + +func main() { + out := flag.String("out", "", "output file") + size := flag.String("size", "320x240", "output frame size") + flag.Parse() + + // Check out and size + if *out == "" { + log.Fatal("-out argument must be specified") + } + width, height, err := ff.AVUtil_parse_video_size(*size) + if err != nil { + log.Fatal(err) + } + + // Create destination + dest, err := os.Create(*out) + if err != nil { + log.Fatal(err) + } + defer dest.Close() + + // Create scaling context + ctx := ff.SWScale_get_context(SRC_WIDTH, SRC_HEIGHT, SRC_PIX_FMT, width, height, DEST_PIX_FMT, ff.SWS_BILINEAR, nil, nil, nil) + if ctx == nil { + log.Fatal("failed to allocate swscale context") + } + defer ff.SWScale_free_context(ctx) + + // Allocate source and destination image buffers + src_data, src_stride, _, err := ff.AVUtil_image_alloc(SRC_WIDTH, SRC_HEIGHT, SRC_PIX_FMT, 16) + if err != nil { + log.Fatal(err) + } + defer ff.AVUtil_image_free(src_data) + + dest_data, dest_stride, dest_bufsize, err := ff.AVUtil_image_alloc(width, height, DEST_PIX_FMT, 1) + if err != nil { + log.Fatal(err) + } + defer ff.AVUtil_image_free(dest_data) + + for i := 0; i < 1000; i++ { + // Generate synthetic video + fill_yuv_image(src_data, src_stride, SRC_WIDTH, SRC_HEIGHT, i) + + // Convert to destination format + // TODO: Currently getting bad src image pointers here + ff.SWScale_scale(ctx, src_data, src_stride, 0, SRC_HEIGHT, dest_data, dest_stride) + + // Write scaled image to file + if _, err := dest.Write(ff.AVUtil_image_bytes(dest_data, dest_bufsize)); err != nil { + log.Fatal(err) + } + } + + log.Printf("Scaling succeeded. Play the output file with the command:\n ffplay -f rawvideo -pixel_format %s -video_size %dx%d %s\n", ff.AVUtil_get_pix_fmt_name(DEST_PIX_FMT), width, height, *out) +} + +func fill_yuv_image(data [][]byte, stride []int, width, height int, frame_index int) { + /* Y */ + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + data[0][y*stride[0]+x] = byte(x + y + frame_index*3) + } + } + + /* Cb and Cr */ + for y := 0; y < height>>1; y++ { + for x := 0; x < width>>1; x++ { + data[1][y*stride[1]+x] = byte(128 + y + frame_index*2) + data[2][y*stride[2]+x] = byte(64 + x + frame_index*5) + } + } +} + +/* + * Copyright (c) 2001 Fabrice Bellard + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ diff --git a/cmd/ffmpeg/show_metadata/main.go b/cmd/ffmpeg/show_metadata/main.go new file mode 100644 index 0000000..2959ff5 --- /dev/null +++ b/cmd/ffmpeg/show_metadata/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "flag" + "log" + + // Packages + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func main() { + in := flag.String("in", "", "input file") + flag.Parse() + + // Check out and size + if *in == "" { + log.Fatal("-in argument must be specified") + } + + // Open input file + input, err := ff.AVFormat_open_url(*in, nil, nil) + if err != nil { + log.Fatal(err) + } + defer ff.AVFormat_close_input(input) + + if err := ff.AVFormat_find_stream_info(input, nil); err != nil { + log.Fatal(err) + } + + for _, tag := range ff.AVUtil_dict_entries(input.Metadata()) { + log.Println(tag.Key(), "=>", tag.Value()) + } +} + +/* + * Copyright (c) 2001 Fabrice Bellard + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ diff --git a/cmd/server/handle_metadata.go b/cmd/server/handle_metadata.go new file mode 100644 index 0000000..b8bf1e6 --- /dev/null +++ b/cmd/server/handle_metadata.go @@ -0,0 +1,46 @@ +package main + +import ( + "encoding/json" + "net/http" + + // Packages + ffmpeg "github.com/mutablelogic/go-media/pkg/ffmpeg" + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +// POST /metadata +// Returns media file metadata +func handle_metadata(w http.ResponseWriter, r *http.Request) { + // Always close the body + defer r.Body.Close() + + // Check method + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + // Read input stream + reader, err := ffmpeg.NewReader(r.Body, r.Header.Get("Content-Type")) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer reader.Close() + + // Get the metadata + metadata := make(map[string]string) + for _, tag := range ff.AVUtil_dict_entries(reader.Metadata()) { + metadata[tag.Key()] = tag.Value() + } + + // Write the metadata to the response + w.Header().Set("Content-Type", "application/json") + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + if err := enc.Encode(metadata); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..c0ec055 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "flag" + "fmt" + "log" + "net/http" +) + +var ( + port = flag.Int("port", 8080, "port to listen on") +) + +func main() { + mux := http.NewServeMux() + + // Output the metadata of a media file as JSON + mux.HandleFunc("/metadata", handle_metadata) + + // Create the server, and listen + server := http.Server{ + Addr: fmt.Sprintf(":%v", *port), + Handler: mux, + } + if err := server.ListenAndServe(); err != nil { + log.Fatal(err) + } +} diff --git a/decoder.go b/decoder.go new file mode 100644 index 0000000..2199bfe --- /dev/null +++ b/decoder.go @@ -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/endian.go b/endian.go new file mode 100644 index 0000000..eaa030f --- /dev/null +++ b/endian.go @@ -0,0 +1,21 @@ +package media + +import ( + "encoding/binary" + "unsafe" +) + +// NativeEndian is the ByteOrder of the current system. +var NativeEndian binary.ByteOrder + +func init() { + // Examine the memory layout of an int16 to determine system + // endianness. + var one int16 = 1 + b := (*byte)(unsafe.Pointer(&one)) + if *b == 0 { + NativeEndian = binary.BigEndian + } else { + NativeEndian = binary.LittleEndian + } +} diff --git a/etc/docker/Dockerfile b/etc/docker/Dockerfile index 524010e..37721c3 100644 --- a/etc/docker/Dockerfile +++ b/etc/docker/Dockerfile @@ -1,31 +1,45 @@ -# recommended way to build is using: -# docker build \ -# --build-arg PLATFORM=linux --build-arg ARCH=amd64 --build-arg VERSION=focal \ -# -f etc/docker/Dockerfile . -# -# ${VERSION} should be "focal" -ARG PLATFORM +ARG OS ARG ARCH -ARG VERSION -FROM --platform=${PLATFORM}/${ARCH} ubuntu:${VERSION} AS builder -# update the base packages -ENV DEBIAN_FRONTEND="noninteractive" TZ="Europe/Berlin" -RUN apt-get update -y && apt-get upgrade -y - -# install packages -RUN apt-get install -y apt-utils golang make pkg-config ca-certificates lsb-release software-properties-common - -# install other build dependencies -# note we need to install ffmpeg 4 from a different repo for bionic -RUN apt-get install -y \ - libavcodec-dev libavdevice-dev libavfilter-dev \ - libavformat-dev libswresample-dev libavutil-dev libchromaprint-dev - -# Run makefile to build command-line tools +# Run makefile to build all the commands +FROM --platform=${OS}/${ARCH} golang:1.22 AS builder +ARG OS +ARG ARCH WORKDIR /usr/src/app COPY . . -RUN make -FROM --platform=${PLATFORM}/${ARCH} ubuntu:${VERSION} +# Install dependencies +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 \ + && 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 + +# Build all the commands +RUN set -x \ + && OS=${OS} ARCH=${ARCH} make + +# Copy commands to /usr/local/bin +FROM --platform=${OS}/${ARCH} debian:bookworm-slim +ARG OS +ARG ARCH +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 \ + && apt update -y -oAcquire::AllowInsecureRepositories=true \ + && apt install -y --force-yes deb-multimedia-keyring \ + && apt install -y --allow-unauthenticated ffmpeg COPY --from=builder /usr/src/app/build/* /usr/local/bin/ +COPY --chmod=755 etc/docker/entrypoint.sh . + +# Label the image +LABEL org.opencontainers.image.source=${SOURCE} + +# Entrypoint when running the server +ENTRYPOINT [ "/entrypoint.sh" ] +EXPOSE 80 443 +STOPSIGNAL SIGQUIT diff --git a/etc/docker/entrypoint.sh b/etc/docker/entrypoint.sh new file mode 100644 index 0000000..4033ec2 --- /dev/null +++ b/etc/docker/entrypoint.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +if [ -z "$1" ]; then + echo "No command specified" + exit 1 +fi + +# Create the /alloc/logs folder if it doesn't exist +install -d -m 0755 /alloc/logs || exit 1 + +# Create the persistent data folder if it doesn't exist +install -d -m 0755 /data || exit 1 + +# Run the command +set -e +umask 022 +exec "$@" diff --git a/etc/test/sample.mpg b/etc/test/sample.mpg new file mode 100644 index 0000000..6b644cf Binary files /dev/null and b/etc/test/sample.mpg differ diff --git a/go.mod b/go.mod index 672008a..c612a24 100755 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( ) require ( + github.com/alecthomas/kong v0.9.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/media.go b/media.go index bb28af6..fb304fc 100644 --- a/media.go +++ b/media.go @@ -1,367 +1,42 @@ +/* media is a package for reading and writing media files. */ package media -import ( - "context" - "io" - "time" -) +import "io" -//////////////////////////////////////////////////////////////////////////////// -// TYPES - -// MediaFlag is a bitfield of flags for media, including type of media -type MediaFlag uint - -// MediaKey is a string which is used for media metadata -type MediaKey string - -// Demux is a function which is called for each packet in the media, which -// is associated with a single stream. The function should return an error if -// the decode should be terminated. -type DemuxFn func(context.Context, Packet) error - -// DecodeFn is a function which is called for each frame in the media, which -// is associated with a single stream. The function should return an error if -// the decode should be terminated. -type DecodeFn func(context.Context, Frame) error - -//////////////////////////////////////////////////////////////////////////////// -// INTERFACES - -// Manager is an interface to the ffmpeg media library for media manipulation -type Manager interface { - io.Closer - - // Enumerate formats with MEDIA_FLAG_ENCODER, MEDIA_FLAG_DECODER, - // MEDIA_FLAG_FILE and MEDIA_FLAG_DEVICE flags to filter. - // Lookups can be further filtered by name, mimetype and extension - MediaFormats(MediaFlag, ...string) []MediaFormat - - // Open media file for reading and return it. A format can be specified - // to "force" a specific format - OpenFile(string, MediaFormat) (Media, error) - - // Open media URL for reading and return it. A format can be specified - // to "force" a specific format - OpenURL(string, MediaFormat) (Media, error) - - // Open media device with a specific name for reading and return it. - OpenDevice(string) (Media, error) - - // Create file for writing and return it - CreateFile(string) (Media, error) - - // Create an output device with a specific name for writing and return it - CreateDevice(string) (Media, error) - - // Create a map of input media. If MediaFlag is MEDIA_FLAG_NONE, then - // all audio, video and subtitle streams are mapped, or else a - // combination of MEDIA_FLAG_AUDIO, - // MEDIA_FLAG_VIDEO, MEDIA_FLAG_SUBTITLE and MEDIA_FLAG_DATA - // can be used to map specific types of streams. - Map(Media, MediaFlag) (Map, error) - - // Demux a media file, passing packets to a callback function - Demux(context.Context, Map, DemuxFn) error - - // Decode a packet into a series of frames, passing decoded frames to - // a callback function - Decode(context.Context, Map, Packet, DecodeFn) error - - // Log messages from ffmpeg - SetDebug(bool) -} - -// MediaFormat is an input or output format for media items -type MediaFormat interface { - // Return the names of the media format - Name() []string - - // Return a longer description of the media format - Description() string - - // Return MEDIA_FLAG_ENCODER, MEDIA_FLAG_DECODER, MEDIA_FLAG_FILE - // and MEDIA_FLAG_DEVICE flags - Flags() MediaFlag - - // Return mimetypes handled - MimeType() []string - - // Return file extensions handled - Ext() []string - - // Return the default audio codec for the format - DefaultAudioCodec() Codec - - // Return the default video codec for the format - DefaultVideoCodec() Codec - - // Return the default subtitle codec for the format - DefaultSubtitleCodec() Codec -} - -// Map is a mapping of input media, potentially to output media -type Map interface { - // Return input media - Input() Media - - // Return a single stream which is mapped for decoding, filtering by - // stream type. Returns nil if there is no selection of that type - Streams(MediaFlag) []Stream - - // Print a summary of the mapping - PrintMap(w io.Writer) - - // Resample an audio stream - Resample(AudioFormat, Stream) error - - // Encode to output media using default codec from a specific stream - //Encode(Media, Stream) error -} - -// Media is a source or destination of media +// Media represents a media stream, which can +// be input or output. A new media object is created +// using NewReader, Open, NewWriter or Create. type Media interface { io.Closer - // URL for the media - URL() string - - // Return enumeration of streams - Streams() []Stream - - // Return media flags for the media - Flags() MediaFlag - - // Return the format of the media - Format() MediaFormat + // Return the metadata for the media stream. + Metadata() []Metadata - // Return metadata for the media - Metadata() Metadata + // Demultiplex media (when NewReader or Open has + // been used). Pass a packet to a decoder function. + Demux(DecoderFunc) error - // Set metadata value by key, or remove it if the value is nil - Set(MediaKey, any) 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 } -// Stream of data multiplexed in the media -type Stream interface { - // Return index of stream in the media - Index() int +// Decoder represents a decoder for a media stream. +type Decoder interface{} - // Return media flags for the stream - Flags() MediaFlag +// DecoderFunc is a function that decodes a packet +type DecoderFunc func(Decoder, Packet) error - // Return artwork for the stream - if MEDIA_FLAG_ARTWORK is set - Artwork() []byte -} - -// Metadata embedded in the media -type Metadata interface { - // Return enumeration of keys - Keys() []MediaKey - - // Return value for key - Value(MediaKey) any -} - -// Packet is a single unit of data in the media -type Packet interface { - // Flags returns the flags for the packet from the stream - Flags() MediaFlag +// FrameFunc is a function that processes a frame of audio +// or video data. +type FrameFunc func(Frame) error - // Stream returns the stream which the packet belongs to - Stream() Stream +// Packet represents a packet of demultiplexed data. +type Packet interface{} - // IsKeyFrame returns true if the packet contains a key frame - IsKeyFrame() bool - - // Pos returns the byte position of the packet in the media - Pos() int64 - - // Duration returns the duration of the packet - Duration() time.Duration - - // Size of the packet in bytes - Size() int - - // Bytes returns the raw bytes of the packet - Bytes() []byte -} +// Frame represents a frame of audio or video data. +type Frame interface{} -// Frame is a decoded video or audio frame -type Frame interface { - AudioFrame - VideoFrame - - // Returns MEDIA_FLAG_VIDEO, MEDIA_FLAG_AUDIO - Flags() MediaFlag - - // Returns true if planar format - //IsPlanar() bool - - // Returns the samples for a specified channel, as array of bytes. For packed - // audio format, the channel should be 0. - //Bytes(channel int) []byte -} - -type AudioFrame interface { - // Returns the audio format, if MEDIA_FLAG_AUDIO is set - AudioFormat() AudioFormat - - // Number of samples, if MEDIA_FLAG_AUDIO is set - NumSamples() int - - // Audio channels, if MEDIA_FLAG_AUDIO is set - Channels() []AudioChannel - - // Duration of the frame, if MEDIA_FLAG_AUDIO is set - Duration() time.Duration -} - -type VideoFrame interface { - // Returns the audio format, if MEDIA_FLAG_VIDEO is set - PixelFormat() PixelFormat - - // Return frame width and height, if MEDIA_FLAG_VIDEO is set - Size() (int, int) -} - -// Codec is an encoder or decoder for a specific media type -type Codec interface { - // Name returns the unique name for the codec - Name() string - - // Description returns the long description for the codec - Description() string - - // Flags for the codec (Audio, Video, Encoder, Decoder) - Flags() MediaFlag -} - -//////////////////////////////////////////////////////////////////////////////// -// CONSTANTS - -const ( - MEDIA_FLAG_ALBUM MediaFlag = (1 << iota) // Is part of an album - MEDIA_FLAG_ALBUM_TRACK // Is an album track - MEDIA_FLAG_ALBUM_COMPILATION // Album is a compilation - MEDIA_FLAG_TVSHOW // Is part of a TV Show - MEDIA_FLAG_TVSHOW_EPISODE // Is a TV Show episode - MEDIA_FLAG_FILE // Is a file - MEDIA_FLAG_DEVICE // Is a device - MEDIA_FLAG_VIDEO // Contains video - MEDIA_FLAG_AUDIO // Contains audio - MEDIA_FLAG_SUBTITLE // Contains subtitles - MEDIA_FLAG_DATA // Contains data stream - MEDIA_FLAG_ATTACHMENT // Contains attachment - MEDIA_FLAG_ARTWORK // Contains artwork - MEDIA_FLAG_CAPTIONS // Contains captions - MEDIA_FLAG_ENCODER // Is an encoder - MEDIA_FLAG_DECODER // Is an decoder - MEDIA_FLAG_NONE MediaFlag = 0 - MEDIA_FLAG_MAX = MEDIA_FLAG_DECODER -) - -const ( - MEDIA_KEY_BRAND_MAJOR MediaKey = "major_brand" // string - MEDIA_KEY_BRAND_COMPATIBLE MediaKey = "compatible_brands" // string - MEDIA_KEY_CREATED MediaKey = "creation_time" // time.Time - MEDIA_KEY_ENCODER MediaKey = "encoder" // string - MEDIA_KEY_ALBUM MediaKey = "album" // string - MEDIA_KEY_ALBUM_ARTIST MediaKey = "artist" // string - MEDIA_KEY_COMMENT MediaKey = "comment" // string - MEDIA_KEY_COMPOSER MediaKey = "composer" // string - MEDIA_KEY_COPYRIGHT MediaKey = "copyright" // string - MEDIA_KEY_YEAR MediaKey = "date" // uint - MEDIA_KEY_DISC MediaKey = "disc" // uint xx or xx/yy - MEDIA_KEY_ENCODED_BY MediaKey = "encoded_by" // string - MEDIA_KEY_FILENAME MediaKey = "filename" // string - MEDIA_KEY_GENRE MediaKey = "genre" // string - MEDIA_KEY_LANGUAGE MediaKey = "language" // string - MEDIA_KEY_PERFORMER MediaKey = "performer" // string - MEDIA_KEY_PUBLISHER MediaKey = "publisher" // string - MEDIA_KEY_SERVICE_NAME MediaKey = "service_name" // string - MEDIA_KEY_SERVICE_PROVIDER MediaKey = "service_provider" // string - MEDIA_KEY_TITLE MediaKey = "title" // string - MEDIA_KEY_TRACK MediaKey = "track" // uint xx or xx/yy - MEDIA_KEY_VERSION_MAJOR MediaKey = "major_version" // string - MEDIA_KEY_VERSION_MINOR MediaKey = "minor_version" // string - MEDIA_KEY_SHOW MediaKey = "show" // string - MEDIA_KEY_SEASON MediaKey = "season_number" // uint - MEDIA_KEY_EPISODE_SORT MediaKey = "episode_sort" // string - MEDIA_KEY_EPISODE_ID MediaKey = "episode_id" // uint - MEDIA_KEY_COMPILATION MediaKey = "compilation" // bool - MEDIA_KEY_GAPLESS_PLAYBACK MediaKey = "gapless_playback" // bool - MEDIA_KEY_ACCOUNT_ID MediaKey = "account_id" // string - MEDIA_KEY_DESCRIPTION MediaKey = "description" // string - MEDIA_KEY_MEDIA_TYPE MediaKey = "media_type" // string - MEDIA_KEY_PURCHASED MediaKey = "purchase_date" // time.Time - MEDIA_KEY_ALBUM_SORT MediaKey = "sort_album" // string - MEDIA_KEY_ARTIST_SORT MediaKey = "sort_artist" // string - MEDIA_KEY_TITLE_SORT MediaKey = "sort_name" // string - MEDIA_KEY_SYNOPSIS MediaKey = "synopsis" // string - MEDIA_KEY_GROUPING MediaKey = "grouping" // string -) - -//////////////////////////////////////////////////////////////////////////////// -// STRINGIFY - -func (f MediaFlag) String() string { - if f == MEDIA_FLAG_NONE { - return f.FlagString() - } - str := "" - for v := MediaFlag(1); v <= MEDIA_FLAG_MAX; v <<= 1 { - if f&v == v { - str += "|" + v.FlagString() - } - } - return str[1:] -} - -func (f MediaFlag) FlagString() string { - switch f { - case MEDIA_FLAG_NONE: - return "MEDIA_FLAG_NONE" - case MEDIA_FLAG_ALBUM: - return "MEDIA_FLAG_ALBUM" - case MEDIA_FLAG_ALBUM_TRACK: - return "MEDIA_FLAG_ALBUM_TRACK" - case MEDIA_FLAG_ALBUM_COMPILATION: - return "MEDIA_FLAG_ALBUM_COMPILATION" - case MEDIA_FLAG_TVSHOW: - return "MEDIA_FLAG_TVSHOW" - case MEDIA_FLAG_TVSHOW_EPISODE: - return "MEDIA_FLAG_TVSHOW_EPISODE" - case MEDIA_FLAG_FILE: - return "MEDIA_FLAG_FILE" - case MEDIA_FLAG_DEVICE: - return "MEDIA_FLAG_DEVICE" - case MEDIA_FLAG_VIDEO: - return "MEDIA_FLAG_VIDEO" - case MEDIA_FLAG_AUDIO: - return "MEDIA_FLAG_AUDIO" - case MEDIA_FLAG_SUBTITLE: - return "MEDIA_FLAG_SUBTITLE" - case MEDIA_FLAG_DATA: - return "MEDIA_FLAG_DATA" - case MEDIA_FLAG_ATTACHMENT: - return "MEDIA_FLAG_ATTACHMENT" - case MEDIA_FLAG_ARTWORK: - return "MEDIA_FLAG_ARTWORK" - case MEDIA_FLAG_CAPTIONS: - return "MEDIA_FLAG_CAPTIONS" - case MEDIA_FLAG_ENCODER: - return "MEDIA_FLAG_ENCODER" - case MEDIA_FLAG_DECODER: - return "MEDIA_FLAG_DECODER" - default: - return "[?? Invalid MediaFlag]" - } -} - -//////////////////////////////////////////////////////////////////////////////// -// METHODS - -func (f MediaFlag) Is(v MediaFlag) bool { - return f&v == v -} +// Metadata represents a metadata entry for a media stream. +type Metadata interface{} diff --git a/mediatype.go b/mediatype.go new file mode 100644 index 0000000..4bca96c --- /dev/null +++ b/mediatype.go @@ -0,0 +1,19 @@ +package media + +import ( + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +//////////////////////////////////////////////////////////////////////////// +// TYPES + +// Media Types: Audio, Video, Subtitle or Data +type MediaType int + +//////////////////////////////////////////////////////////////////////////// +// GLOBALS + +const ( + AUDIO = MediaType(ff.AVMEDIA_TYPE_AUDIO) // Audio media type + VIDEO = MediaType(ff.AVMEDIA_TYPE_VIDEO) // Video media type +) diff --git a/pkg/version/version.go b/pkg/version/version.go new file mode 100644 index 0000000..0e74549 --- /dev/null +++ b/pkg/version/version.go @@ -0,0 +1,8 @@ +package version + +var ( + GitSource string + GitTag string + GetBranch string + GoBuildTime string +) diff --git a/reader.go b/reader.go new file mode 100644 index 0000000..fa52c12 --- /dev/null +++ b/reader.go @@ -0,0 +1,278 @@ +package media + +import ( + "encoding/json" + "errors" + "io" + "syscall" + + // Packages + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +//////////////////////////////////////////////////////////////////////////////// +// TYPES + +type reader struct { + input *ff.AVFormatContext + avio *ff.AVIOContextEx + decoders map[int]*decoder + frame *ff.AVFrame +} + +type reader_callback struct { + r io.Reader +} + +var _ Media = (*reader)(nil) + +//////////////////////////////////////////////////////////////////////////////// +// GLOBALS + +const ( + bufSize = 4096 +) + +//////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// 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) { + reader := new(reader) + reader.decoders = make(map[int]*decoder) + + // TODO: mimetype input is currently ignored, format is always guessed + + // Open the stream + if ctx, err := ff.AVFormat_open_url(url, nil, nil); err != nil { + return nil, err + } else { + reader.input = ctx + } + + // Find stream information and do rest of the initialization + return reader.open() +} + +// Create a new reader from an io.Reader +func NewReader(r io.Reader, mimetype string) (*reader, error) { + reader := new(reader) + reader.decoders = make(map[int]*decoder) + + // TODO: mimetype input is currently ignored, format is always guessed + + // Allocate the AVIO context + reader.avio = ff.AVFormat_avio_alloc_context(bufSize, false, &reader_callback{r}) + if reader.avio == nil { + return nil, errors.New("failed to allocate avio context") + } + + // Open the stream + if ctx, err := ff.AVFormat_open_reader(reader.avio, nil, nil); err != nil { + ff.AVFormat_avio_context_free(reader.avio) + return nil, err + } else { + reader.input = ctx + } + + // Find stream information and do rest of the initialization + return reader.open() +} + +func (r *reader) open() (*reader, error) { + // Find stream information + if err := ff.AVFormat_find_stream_info(r.input, nil); err != nil { + ff.AVFormat_free_context(r.input) + ff.AVFormat_avio_context_free(r.avio) + 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() + } + ff.AVUtil_frame_free(r.frame) + 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.input = nil + r.avio = nil + + // Return success + return nil +} + +//////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +// Display the reader as a string +func (r *reader) MarshalJSON() ([]byte, error) { + return json.Marshal(r.input) +} + +//////////////////////////////////////////////////////////////////////////////// +// METHODS + +// TODO: Frame should be a struct to access plane data and other properties +// TODO: Frame output may not include pts and time_base + +// 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) + } + + // Flush the decoders + for _, decoder := range r.decoders { + if err := fn(decoder, nil); err != nil { + return err + } + } + + // 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 + } +} + +type jsonMetadata struct { + Key string `json:"key"` + Value string `json:"value"` +} + +// Return the metadata for the media stream +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{ + Key: entry.Key(), + Value: entry.Value(), + }) + } + return result +} + +//////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +func (r *reader_callback) Reader(buf []byte) int { + n, err := r.r.Read(buf) + if err != nil { + return ff.AVERROR_EOF + } + return n +} + +func (r *reader_callback) Seeker(offset int64, whence int) int64 { + whence = whence & ^ff.AVSEEK_FORCE + seeker, ok := r.r.(io.ReadSeeker) + if !ok { + return -1 + } + switch whence { + case io.SeekStart, io.SeekCurrent, io.SeekEnd: + n, err := seeker.Seek(offset, whence) + if err != nil { + return -1 + } + return n + } + return -1 +} + +func (r *reader_callback) Writer([]byte) int { + return ff.AVERROR_EOF +} diff --git a/sys/ffmpeg51/avcodec.go b/sys/ffmpeg51/avcodec.go index 5874857..c7ec55d 100755 --- a/sys/ffmpeg51/avcodec.go +++ b/sys/ffmpeg51/avcodec.go @@ -45,7 +45,7 @@ const ( AV_CODEC_CAP_FRAME_THREADS AVCodecCap = C.AV_CODEC_CAP_FRAME_THREADS // Codec supports frame-level multithreading AV_CODEC_CAP_SLICE_THREADS AVCodecCap = C.AV_CODEC_CAP_SLICE_THREADS // Codec supports slice-based (or partition-based) multithreading AV_CODEC_CAP_PARAM_CHANGE AVCodecCap = C.AV_CODEC_CAP_PARAM_CHANGE // Codec supports changed parameters at any point - AV_CODEC_CAP_AUTO_THREADS AVCodecCap = C.AV_CODEC_CAP_AUTO_THREADS // Codec supports avctx->thread_count == 0 (auto) +// AV_CODEC_CAP_AUTO_THREADS AVCodecCap = C.AV_CODEC_CAP_AUTO_THREADS // Codec supports avctx->thread_count == 0 (auto) AV_CODEC_CAP_VARIABLE_FRAME_SIZE AVCodecCap = C.AV_CODEC_CAP_VARIABLE_FRAME_SIZE // Audio encoder supports receiving a different number of samples in each call AV_CODEC_CAP_AVOID_PROBING AVCodecCap = C.AV_CODEC_CAP_AVOID_PROBING // Decoder is not a preferred choice for probing AV_CODEC_CAP_HARDWARE AVCodecCap = C.AV_CODEC_CAP_HARDWARE // Codec is backed by a hardware implementation @@ -153,8 +153,8 @@ func (v AVCodecCap) FlagString() string { return "AV_CODEC_CAP_SLICE_THREADS" case AV_CODEC_CAP_PARAM_CHANGE: return "AV_CODEC_CAP_PARAM_CHANGE" - case AV_CODEC_CAP_AUTO_THREADS: - return "AV_CODEC_CAP_AUTO_THREADS" +// case AV_CODEC_CAP_AUTO_THREADS: +// return "AV_CODEC_CAP_AUTO_THREADS" case AV_CODEC_CAP_VARIABLE_FRAME_SIZE: return "AV_CODEC_CAP_VARIABLE_FRAME_SIZE" case AV_CODEC_CAP_AVOID_PROBING: diff --git a/sys/ffmpeg51/swresample.go b/sys/ffmpeg51/swresample.go deleted file mode 100755 index 33aa5bb..0000000 --- a/sys/ffmpeg51/swresample.go +++ /dev/null @@ -1,156 +0,0 @@ -package ffmpeg - -import ( - "unsafe" -) - -//////////////////////////////////////////////////////////////////////////////// -// CGO - -/* -#cgo pkg-config: libswresample -#include -*/ -import "C" - -//////////////////////////////////////////////////////////////////////////////// -// TYPES - -type ( - SWRContext C.struct_SwrContext -) - -//////////////////////////////////////////////////////////////////////////////// -// STRINGIFY - -func (ctx *SWRContext) String() string { - str := "" -} - -//////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - VERSION - -// Return the LIBSWRESAMPLE_VERSION_INT constant. -func SWR_version() uint { - return uint(C.swresample_version()) -} - -// Return the swr build-time configuration. -func SWR_configuration() string { - return C.GoString(C.swresample_configuration()) -} - -// Return the swr license. -func SWR_license() string { - return C.GoString(C.swresample_license()) -} - -//////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - INIT - -// Allocate SwrContext. -func SWR_alloc() *SWRContext { - return (*SWRContext)(C.swr_alloc()) -} - -// Free the given SwrContext. -func SWR_free(ctx *SWRContext) { - C.swr_free((**C.struct_SwrContext)(unsafe.Pointer(&ctx))) -} - -// Initialize context after user parameters have been set. -func SWR_init(ctx *SWRContext) error { - if err := AVError(C.swr_init((*C.struct_SwrContext)(ctx))); err == 0 { - return nil - } else { - return err - } -} - -// Closes the context so that swr_is_initialized() returns 0 -func SWR_close(ctx *SWRContext) { - C.swr_close((*C.struct_SwrContext)(ctx)) -} - -// Check whether an swr context has been initialized or not. -func SWR_is_initialized(ctx *SWRContext) bool { - return C.swr_is_initialized((*C.struct_SwrContext)(ctx)) != 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 - } -} - -// Core conversion functions. Returns number of samples output per channel. -// in and in_count can be set to 0 to flush the last few samples out at the end. -func SWR_convert(ctx *SWRContext, out **byte, out_count int, in **byte, in_count int) (int, error) { - n := int(C.swr_convert((*C.struct_SwrContext)(ctx), (**C.uint8_t)(unsafe.Pointer(out)), C.int(out_count), (**C.uint8_t)(unsafe.Pointer(in)), C.int(in_count))) - if n < 0 { - return n, AVError(AVERROR_INVALIDDATA) - } else { - return n, nil - } -} - -// Convert the next timestamp from input to output timestamps are in 1/(in_sample_rate * out_sample_rate) units. -func SWR_next_pts(ctx *SWRContext, pts int64) int64 { - return int64(C.swr_next_pts((*C.struct_SwrContext)(ctx), C.int64_t(pts))) -} - -// Drops the specified number of output samples. -func SWR_drop_output(ctx *SWRContext, count int) error { - if err := AVError(C.swr_drop_output((*C.struct_SwrContext)(ctx), C.int(count))); err != 0 { - return err - } else { - return nil - } -} - -// Inject the specified number of silence samples. -func SWR_inject_silence(ctx *SWRContext, count int) error { - if err := AVError(C.swr_inject_silence((*C.struct_SwrContext)(ctx), C.int(count))); err != 0 { - return err - } else { - return nil - } -} - -// Gets the delay the next input sample will experience relative to the next output sample. -func SWR_get_delay(ctx *SWRContext, base int64) int64 { - return int64(C.swr_get_delay((*C.struct_SwrContext)(ctx), C.int64_t(base))) -} - -// Find an upper bound on the number of samples that the next swr_convert -func SWR_get_out_samples(ctx *SWRContext, in_samples int) (int, error) { - n := int(C.swr_get_out_samples((*C.struct_SwrContext)(ctx), C.int(in_samples))) - if n < 0 { - return n, AVError(n) - } else { - return n, nil - } -} - -// Convert the samples in the input AVFrame and write them to the output AVFrame. -func SWR_convert_frame(ctx *SWRContext, src, dest *AVFrame) error { - if err := AVError(C.swr_convert_frame((*C.struct_SwrContext)(ctx), (*C.struct_AVFrame)(dest), (*C.struct_AVFrame)(src))); err != 0 { - return err - } else { - return nil - } -} - -// Configure or reconfigure the SwrContext using the information provided by the AVFrames. -func SWR_config_frame(ctx *SWRContext, src, dest *AVFrame) error { - if err := AVError(C.swr_config_frame((*C.struct_SwrContext)(ctx), (*C.struct_AVFrame)(dest), (*C.struct_AVFrame)(src))); err != 0 { - return err - } else { - return nil - } -} diff --git a/sys/ffmpeg61/avcodec.go b/sys/ffmpeg61/avcodec.go new file mode 100644 index 0000000..0792d62 --- /dev/null +++ b/sys/ffmpeg61/avcodec.go @@ -0,0 +1,700 @@ +package ffmpeg + +import ( + "encoding/json" + "fmt" + "unsafe" +) + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavcodec libavutil +#include +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// TYPES + +type ( + AVPacket C.AVPacket + AVCodec C.AVCodec + AVCodecCap C.uint32_t + AVCodecContext C.AVCodecContext + AVCodecFlag C.uint32_t + AVCodecFlag2 C.uint32_t + AVCodecMacroblockDecisionMode C.int + AVCodecParameters C.AVCodecParameters + AVCodecParser C.AVCodecParser + AVCodecParserContext C.AVCodecParserContext + AVProfile C.AVProfile + AVCodecID C.enum_AVCodecID +) + +type jsonAVPacket struct { + Pts int64 `json:"pts,omitempty"` + Dts int64 `json:"dts,omitempty"` + Size int `json:"size,omitempty"` + StreamIndex int `json:"stream_index"` // Stream index starts at 0 + Flags int `json:"flags,omitempty"` + SideDataElems int `json:"side_data_elems,omitempty"` + Duration int64 `json:"duration,omitempty"` + Pos int64 `json:"pos,omitempty"` +} + +type jsonAVCodec struct { + Type AVMediaType `json:"type"` + Name string `json:"name,omitempty"` + LongName string `json:"long_name,omitempty"` + ID AVCodecID `json:"id,omitempty"` + Capabilities AVCodecCap `json:"capabilities,omitempty"` + Framerates []AVRational `json:"supported_framerates,omitempty"` + SampleFormats []AVSampleFormat `json:"sample_formats,omitempty"` + PixelFormats []AVPixelFormat `json:"pixel_formats,omitempty"` + Samplerates []int `json:"samplerates,omitempty"` + Profiles []AVProfile `json:"profiles,omitempty"` + ChannelLayouts []AVChannelLayout `json:"channel_layouts,omitempty"` +} + +type jsonAVCodecContext struct { + CodecType AVMediaType `json:"codec_type,omitempty"` + Codec *AVCodec `json:"codec,omitempty"` + BitRate int64 `json:"bit_rate,omitempty"` + BitRateTolerance int `json:"bit_rate_tolerance,omitempty"` + PixelFormat AVPixelFormat `json:"pix_fmt,omitempty"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` + SampleFormat AVSampleFormat `json:"sample_fmt,omitempty"` + SampleRate int `json:"sample_rate,omitempty"` + ChannelLayout AVChannelLayout `json:"channel_layout,omitempty"` + TimeBase AVRational `json:"time_base,omitempty"` +} + +//////////////////////////////////////////////////////////////////////////////// +// CONSTANTS + +const ( + AV_CODEC_ID_NONE AVCodecID = C.AV_CODEC_ID_NONE + AV_CODEC_ID_MP2 AVCodecID = C.AV_CODEC_ID_MP2 + AV_CODEC_ID_H264 AVCodecID = C.AV_CODEC_ID_H264 + AV_CODEC_ID_MPEG1VIDEO AVCodecID = C.AV_CODEC_ID_MPEG1VIDEO + AV_CODEC_ID_MPEG2VIDEO AVCodecID = C.AV_CODEC_ID_MPEG2VIDEO +) + +/** + * Required number of additionally allocated bytes at the end of the input bitstream for decoding. + * This is mainly needed because some optimized bitstream readers read + * 32 or 64 bit at once and could read over the end. + * Note: If the first 23 bits of the additional bytes are not 0, then damaged + * MPEG bitstreams could cause overread and segfault. + */ +const ( + AV_INPUT_BUFFER_PADDING_SIZE int = C.AV_INPUT_BUFFER_PADDING_SIZE +) + +/** + * macroblock decision mode + * - encoding: Set by user. + * - decoding: unused + */ +const ( + FF_MB_DECISION_SIMPLE AVCodecMacroblockDecisionMode = C.FF_MB_DECISION_SIMPLE ///< uses mb_cmp + FF_MB_DECISION_BITS AVCodecMacroblockDecisionMode = C.FF_MB_DECISION_BITS ///< chooses the one which needs the fewest bits + FF_MB_DECISION_RD AVCodecMacroblockDecisionMode = C.FF_MB_DECISION_RD ///< rate distortion +) + +const ( + AV_CODEC_FLAG_UNALIGNED AVCodecFlag = C.AV_CODEC_FLAG_UNALIGNED // Allow decoders to produce frames with data planes that are not aligned to CPU requirements + AV_CODEC_FLAG_QSCALE AVCodecFlag = C.AV_CODEC_FLAG_QSCALE // Use fixed qscale + AV_CODEC_FLAG_4MV AVCodecFlag = C.AV_CODEC_FLAG_4MV // 4 MV per MB allowed / advanced prediction for H.263. + AV_CODEC_FLAG_OUTPUT_CORRUPT AVCodecFlag = C.AV_CODEC_FLAG_OUTPUT_CORRUPT // Output even those frames that might be corrupted. + AV_CODEC_FLAG_QPEL AVCodecFlag = C.AV_CODEC_FLAG_QPEL // Use qpel MC. + AV_CODEC_FLAG_RECON_FRAME AVCodecFlag = C.AV_CODEC_FLAG_RECON_FRAME // Request the encoder to output reconstructed frames + AV_CODEC_FLAG_COPY_OPAQUE AVCodecFlag = C.AV_CODEC_FLAG_COPY_OPAQUE // Request the decoder to propagate each packet's AVPacket.opaque and AVPacket.opaque_ref to its corresponding output AVFrame. + AV_CODEC_FLAG_FRAME_DURATION AVCodecFlag = C.AV_CODEC_FLAG_FRAME_DURATION // Signal to the encoder that the values of AVFrame.duration are valid and should be used + AV_CODEC_FLAG_PASS1 AVCodecFlag = C.AV_CODEC_FLAG_PASS1 // Use internal 2pass ratecontrol in first pass mode. + AV_CODEC_FLAG_PASS2 AVCodecFlag = C.AV_CODEC_FLAG_PASS2 // Use internal 2pass ratecontrol in second pass mode. + AV_CODEC_FLAG_LOOP_FILTER AVCodecFlag = C.AV_CODEC_FLAG_LOOP_FILTER // loop filter. + AV_CODEC_FLAG_GRAY AVCodecFlag = C.AV_CODEC_FLAG_GRAY // Only decode/encode grayscale. + AV_CODEC_FLAG_PSNR AVCodecFlag = C.AV_CODEC_FLAG_PSNR // error[?] variables will be set during encoding. + AV_CODEC_FLAG_INTERLACED_DCT AVCodecFlag = C.AV_CODEC_FLAG_INTERLACED_DCT // Use interlaced DCT. + AV_CODEC_FLAG_LOW_DELAY AVCodecFlag = C.AV_CODEC_FLAG_LOW_DELAY // Force low delay. + AV_CODEC_FLAG_GLOBAL_HEADER AVCodecFlag = C.AV_CODEC_FLAG_GLOBAL_HEADER // Place global headers in extradata instead of every keyframe. + AV_CODEC_FLAG_BITEXACT AVCodecFlag = C.AV_CODEC_FLAG_BITEXACT // Use only bitexact stuff (except (I)DCT). + AV_CODEC_FLAG_AC_PRED AVCodecFlag = C.AV_CODEC_FLAG_AC_PRED // H.263 advanced intra coding / MPEG-4 AC prediction + AV_CODEC_FLAG_INTERLACED_ME AVCodecFlag = C.AV_CODEC_FLAG_INTERLACED_ME // interlaced motion estimation + AV_CODEC_FLAG_CLOSED_GOP AVCodecFlag = C.AV_CODEC_FLAG_CLOSED_GOP + AV_CODEC_FLAG2_FAST AVCodecFlag2 = C.AV_CODEC_FLAG2_FAST // Allow non spec compliant speedup tricks. + AV_CODEC_FLAG2_NO_OUTPUT AVCodecFlag2 = C.AV_CODEC_FLAG2_NO_OUTPUT // Skip bitstream encoding. + AV_CODEC_FLAG2_LOCAL_HEADER AVCodecFlag2 = C.AV_CODEC_FLAG2_LOCAL_HEADER // Place global headers at every keyframe instead of in extradata. + AV_CODEC_FLAG2_CHUNKS AVCodecFlag2 = C.AV_CODEC_FLAG2_CHUNKS // Input bitstream might be truncated at a packet boundaries instead of only at frame boundaries. + AV_CODEC_FLAG2_IGNORE_CROP AVCodecFlag2 = C.AV_CODEC_FLAG2_IGNORE_CROP // Discard cropping information from SPS. + AV_CODEC_FLAG2_SHOW_ALL AVCodecFlag2 = C.AV_CODEC_FLAG2_SHOW_ALL // Show all frames before the first keyframe + AV_CODEC_FLAG2_EXPORT_MVS AVCodecFlag2 = C.AV_CODEC_FLAG2_EXPORT_MVS // Export motion vectors through frame side data + AV_CODEC_FLAG2_SKIP_MANUAL AVCodecFlag2 = C.AV_CODEC_FLAG2_SKIP_MANUAL // Do not skip samples and export skip information as frame side data + AV_CODEC_FLAG2_RO_FLUSH_NOOP AVCodecFlag2 = C.AV_CODEC_FLAG2_RO_FLUSH_NOOP // Do not reset ASS ReadOrder field on flush (subtitles decoding) + AV_CODEC_FLAG2_ICC_PROFILES AVCodecFlag2 = C.AV_CODEC_FLAG2_ICC_PROFILES // Generate/parse ICC profiles on encode/decode, as appropriate for the type of file +) + +const ( + AV_CODEC_CAP_NONE AVCodecCap = 0 + AV_CODEC_CAP_DRAW_HORIZ_BAND AVCodecCap = C.AV_CODEC_CAP_DRAW_HORIZ_BAND // Decoder can use draw_horiz_band callback + AV_CODEC_CAP_DR1 AVCodecCap = C.AV_CODEC_CAP_DR1 // Codec uses get_buffer() for allocating buffers and supports custom allocators + AV_CODEC_CAP_DELAY AVCodecCap = C.AV_CODEC_CAP_DELAY // Encoder or decoder requires flushing with NULL input at the end in order to give the complete and correct output + AV_CODEC_CAP_SMALL_LAST_FRAME AVCodecCap = C.AV_CODEC_CAP_SMALL_LAST_FRAME // Codec can be fed a final frame with a smaller size + AV_CODEC_CAP_SUBFRAMES AVCodecCap = C.AV_CODEC_CAP_SUBFRAMES // Codec can output multiple frames per AVPacket Normally demuxers return one frame at a time, demuxers which do not do are connected to a parser to split what they return into proper frames + AV_CODEC_CAP_EXPERIMENTAL AVCodecCap = C.AV_CODEC_CAP_EXPERIMENTAL // Codec is experimental and is thus avoided in favor of non experimental encoders + AV_CODEC_CAP_CHANNEL_CONF AVCodecCap = C.AV_CODEC_CAP_CHANNEL_CONF // Codec should fill in channel configuration and samplerate instead of container + AV_CODEC_CAP_FRAME_THREADS AVCodecCap = C.AV_CODEC_CAP_FRAME_THREADS // Codec supports frame-level multithreading + AV_CODEC_CAP_SLICE_THREADS AVCodecCap = C.AV_CODEC_CAP_SLICE_THREADS // Codec supports slice-based (or partition-based) multithreading + AV_CODEC_CAP_PARAM_CHANGE AVCodecCap = C.AV_CODEC_CAP_PARAM_CHANGE // Codec supports changed parameters at any point + AV_CODEC_CAP_OTHER_THREADS AVCodecCap = C.AV_CODEC_CAP_OTHER_THREADS // Codec supports multithreading through a method other than slice + AV_CODEC_CAP_VARIABLE_FRAME_SIZE AVCodecCap = C.AV_CODEC_CAP_VARIABLE_FRAME_SIZE // Audio encoder supports receiving a different number of samples in each call + AV_CODEC_CAP_AVOID_PROBING AVCodecCap = C.AV_CODEC_CAP_AVOID_PROBING // Decoder is not a preferred choice for probing + AV_CODEC_CAP_HARDWARE AVCodecCap = C.AV_CODEC_CAP_HARDWARE // Codec is backed by a hardware implementation + AV_CODEC_CAP_HYBRID AVCodecCap = C.AV_CODEC_CAP_HYBRID // Codec is potentially backed by a hardware implementation, but not necessarily + AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE AVCodecCap = C.AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE // This encoder can reorder user opaque values from input AVFrames and return them with corresponding output packets. + AV_CODEC_CAP_ENCODER_FLUSH AVCodecCap = C.AV_CODEC_CAP_ENCODER_FLUSH // This encoder can be flushed using avcodec_flush_buffers() + AV_CODEC_CAP_ENCODER_RECON_FRAME AVCodecCap = C.AV_CODEC_CAP_ENCODER_RECON_FRAME // The encoder is able to output reconstructed frame data + AV_CODEC_CAP_MAX = AV_CODEC_CAP_ENCODER_RECON_FRAME +) + +//////////////////////////////////////////////////////////////////////////////// +// JSON OUTPUT + +func (ctx *AVPacket) MarshalJSON() ([]byte, error) { + return json.Marshal(jsonAVPacket{ + Pts: int64(ctx.pts), + Dts: int64(ctx.dts), + Size: int(ctx.size), + StreamIndex: int(ctx.stream_index), + Flags: int(ctx.flags), + SideDataElems: int(ctx.side_data_elems), + Duration: int64(ctx.duration), + Pos: int64(ctx.pos), + }) +} + +func (ctx *AVCodec) MarshalJSON() ([]byte, error) { + return json.Marshal(jsonAVCodec{ + Name: C.GoString(ctx.name), + LongName: C.GoString(ctx.long_name), + Type: AVMediaType(ctx._type), + ID: AVCodecID(ctx.id), + Capabilities: AVCodecCap(ctx.capabilities), + Framerates: ctx.SupportedFramerates(), + SampleFormats: ctx.SampleFormats(), + PixelFormats: ctx.PixelFormats(), + Samplerates: ctx.SupportedSamplerates(), + Profiles: ctx.Profiles(), + ChannelLayouts: ctx.ChannelLayouts(), + }) +} + +func (ctx *AVCodecContext) MarshalJSON() ([]byte, error) { + switch ctx.codec_type { + case C.AVMEDIA_TYPE_VIDEO: + return json.Marshal(jsonAVCodecContext{ + CodecType: AVMediaType(ctx.codec_type), + Codec: (*AVCodec)(ctx.codec), + BitRate: int64(ctx.bit_rate), + BitRateTolerance: int(ctx.bit_rate_tolerance), + PixelFormat: AVPixelFormat(ctx.pix_fmt), + Width: int(ctx.width), + Height: int(ctx.height), + }) + case C.AVMEDIA_TYPE_AUDIO: + return json.Marshal(jsonAVCodecContext{ + CodecType: AVMediaType(ctx.codec_type), + Codec: (*AVCodec)(ctx.codec), + BitRate: int64(ctx.bit_rate), + BitRateTolerance: int(ctx.bit_rate_tolerance), + TimeBase: (AVRational)(ctx.time_base), + SampleFormat: AVSampleFormat(ctx.sample_fmt), + SampleRate: int(ctx.sample_rate), + ChannelLayout: AVChannelLayout(ctx.ch_layout), + }) + default: + return json.Marshal(jsonAVCodecContext{ + CodecType: AVMediaType(ctx.codec_type), + Codec: (*AVCodec)(ctx.codec), + BitRate: int64(ctx.bit_rate), + BitRateTolerance: int(ctx.bit_rate_tolerance), + TimeBase: (AVRational)(ctx.time_base), + }) + } +} + +func (ctx AVProfile) MarshalJSON() ([]byte, error) { + return json.Marshal(ctx.Name()) +} + +func (ctx AVMediaType) MarshalJSON() ([]byte, error) { + return json.Marshal(ctx.String()) +} + +func (v AVCodecCap) MarshalJSON() ([]byte, error) { + return json.Marshal(v.String()) +} + +//////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (ctx *AVPacket) String() string { + if str, err := json.MarshalIndent(ctx, "", " "); err != nil { + return err.Error() + } else { + return string(str) + } +} + +func (ctx *AVCodec) String() string { + if str, err := json.MarshalIndent(ctx, "", " "); err != nil { + return err.Error() + } else { + return string(str) + } +} + +func (ctx *AVCodecParameters) String() string { + if str, err := json.MarshalIndent(ctx, "", " "); err != nil { + return err.Error() + } else { + return string(str) + } +} + +func (ctx *AVCodecContext) String() string { + if str, err := json.MarshalIndent(ctx, "", " "); err != nil { + return err.Error() + } else { + return string(str) + } +} + +func (ctx AVProfile) String() string { + if str, err := json.MarshalIndent(ctx, "", " "); err != nil { + return err.Error() + } else { + return string(str) + } +} + +//////////////////////////////////////////////////////////////////////////////// +// AVCodecParameters + +type jsonAVCodecParameters struct { + CodecType AVMediaType `json:"codec_type"` + CodecID AVCodecID `json:"codec_id,omitempty"` + CodecTag uint32 `json:"codec_tag,omitempty"` + Format int `json:"format,omitempty"` + BitRate int64 `json:"bit_rate,omitempty"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` + 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) { + return json.Marshal(jsonAVCodecParameters{ + CodecType: AVMediaType(ctx.codec_type), + CodecID: AVCodecID(ctx.codec_id), + CodecTag: uint32(ctx.codec_tag), + Format: int(ctx.format), + BitRate: int64(ctx.bit_rate), + Width: int(ctx.width), + Height: int(ctx.height), + 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) +} + +func (ctx *AVCodecParameters) CodecID() AVCodecID { + return AVCodecID(ctx.codec_id) +} + +func (ctx *AVCodecParameters) CodecTag() uint32 { + return uint32(ctx.codec_tag) +} + +func (ctx *AVCodecParameters) SetCodecTag(tag uint32) { + ctx.codec_tag = C.uint32_t(tag) +} + +//////////////////////////////////////////////////////////////////////////////// +// AVCodec + +func (c *AVCodec) Name() string { + return C.GoString(c.name) +} + +func (c *AVCodec) LongName() string { + return C.GoString(c.long_name) +} + +func (c *AVCodec) Type() AVMediaType { + return AVMediaType(c._type) +} + +func (c *AVCodec) ID() AVCodecID { + return AVCodecID(c.id) +} + +func (c *AVCodec) Capabilities() AVCodecCap { + return AVCodecCap(c.capabilities) +} + +func (c *AVCodec) SupportedFramerates() []AVRational { + var result []AVRational + ptr := uintptr(unsafe.Pointer(c.supported_framerates)) + if ptr == 0 { + return nil + } + for { + v := AVRational(*(*C.struct_AVRational)(unsafe.Pointer(ptr))) + if v.IsZero() { + break + } + result = append(result, v) + ptr += unsafe.Sizeof(AVRational{}) + } + return result +} + +func (c *AVCodec) SampleFormats() []AVSampleFormat { + var result []AVSampleFormat + ptr := uintptr(unsafe.Pointer(c.sample_fmts)) + if ptr == 0 { + return nil + } + for { + v := AVSampleFormat(*(*C.enum_AVSampleFormat)(unsafe.Pointer(ptr))) + if v == AV_SAMPLE_FMT_NONE { + break + } + result = append(result, v) + ptr += unsafe.Sizeof(AV_SAMPLE_FMT_NONE) + } + return result +} + +func (c *AVCodec) PixelFormats() []AVPixelFormat { + var result []AVPixelFormat + ptr := uintptr(unsafe.Pointer(c.pix_fmts)) + if ptr == 0 { + return nil + } + for { + v := AVPixelFormat(*(*C.enum_AVPixelFormat)(unsafe.Pointer(ptr))) + if v == AV_PIX_FMT_NONE { + break + } + result = append(result, v) + ptr += unsafe.Sizeof(AV_PIX_FMT_NONE) + } + return result +} + +func (c *AVCodec) SupportedSamplerates() []int { + var result []int + ptr := uintptr(unsafe.Pointer(c.supported_samplerates)) + if ptr == 0 { + return nil + } + for { + v := int(*(*C.int)(unsafe.Pointer(ptr))) + if v == 0 { + break + } + result = append(result, v) + ptr += unsafe.Sizeof(C.int(0)) + } + return result +} + +func (c *AVCodec) Profiles() []AVProfile { + var result []AVProfile + ptr := uintptr(unsafe.Pointer(c.profiles)) + if ptr == 0 { + return nil + } + for { + v := (AVProfile)(*(*C.struct_AVProfile)(unsafe.Pointer(ptr))) + if v.profile == C.FF_PROFILE_UNKNOWN { + break + } + result = append(result, v) + ptr += unsafe.Sizeof(AVProfile{}) + } + return result +} + +func (c *AVCodec) ChannelLayouts() []AVChannelLayout { + var result []AVChannelLayout + ptr := uintptr(unsafe.Pointer(c.ch_layouts)) + if ptr == 0 { + return nil + } + for { + v := (AVChannelLayout)(*(*C.struct_AVChannelLayout)(unsafe.Pointer(ptr))) + if v.nb_channels == 0 { + break + } + result = append(result, v) + ptr += unsafe.Sizeof(AVChannelLayout{}) + } + return result +} + +//////////////////////////////////////////////////////////////////////////////// +// AVCodecContext + +func (ctx *AVCodecContext) Codec() *AVCodec { + return (*AVCodec)(ctx.codec) +} + +func (ctx *AVCodecContext) BitRate() int64 { + return int64(ctx.bit_rate) +} + +func (ctx *AVCodecContext) SetBitRate(bit_rate int64) { + ctx.bit_rate = C.int64_t(bit_rate) +} + +func (ctx *AVCodecContext) Width() int { + return int(ctx.width) +} + +func (ctx *AVCodecContext) SetWidth(width int) { + ctx.width = C.int(width) +} + +func (ctx *AVCodecContext) Height() int { + return int(ctx.height) +} + +func (ctx *AVCodecContext) SetHeight(height int) { + ctx.height = C.int(height) +} + +func (ctx *AVCodecContext) TimeBase() AVRational { + return (AVRational)(ctx.time_base) +} + +func (ctx *AVCodecContext) SetTimeBase(time_base AVRational) { + ctx.time_base = C.struct_AVRational(time_base) +} + +func (ctx *AVCodecContext) Framerate() AVRational { + return (AVRational)(ctx.framerate) +} + +func (ctx *AVCodecContext) SetFramerate(framerate AVRational) { + ctx.framerate = C.struct_AVRational(framerate) +} + +// Audio sample format. +func (ctx *AVCodecContext) SampleFormat() AVSampleFormat { + return AVSampleFormat(ctx.sample_fmt) +} + +// Audio sample format. +func (ctx *AVCodecContext) SetSampleFormat(sample_fmt AVSampleFormat) { + ctx.sample_fmt = C.enum_AVSampleFormat(sample_fmt) +} + +// Frame number. +func (ctx *AVCodecContext) FrameNum() int { + return int(ctx.frame_num) +} + +// Audio sample rate. +func (ctx *AVCodecContext) SampleRate() int { + return int(ctx.sample_rate) +} + +// Audio sample rate. +func (ctx *AVCodecContext) SetSampleRate(sample_rate int) { + ctx.sample_rate = C.int(sample_rate) +} + +// Number of samples per channel in an audio frame. +func (ctx *AVCodecContext) FrameSize() int { + return int(ctx.frame_size) +} + +// Audio channel layout. +func (ctx *AVCodecContext) ChannelLayout() AVChannelLayout { + return AVChannelLayout(ctx.ch_layout) +} + +// Audio channel layout. +func (ctx *AVCodecContext) SetChannelLayout(src AVChannelLayout) error { + if ret := AVError(C.av_channel_layout_copy((*C.struct_AVChannelLayout)(&ctx.ch_layout), (*C.struct_AVChannelLayout)(&src))); ret != 0 { + return ret + } + return nil +} + +// Group-of-pictures (GOP) size. +func (ctx *AVCodecContext) GopSize() int { + return int(ctx.gop_size) +} + +// Group-of-pictures (GOP) size. +func (ctx *AVCodecContext) SetGopSize(gop_size int) { + ctx.gop_size = C.int(gop_size) +} + +// Maximum number of B-frames between non-B-frames. +func (ctx *AVCodecContext) MaxBFrames() int { + return int(ctx.max_b_frames) +} + +// Maximum number of B-frames between non-B-frames. +func (ctx *AVCodecContext) SetMaxBFrames(max_b_frames int) { + ctx.max_b_frames = C.int(max_b_frames) +} + +// Pixel format. +func (ctx *AVCodecContext) PixFmt() AVPixelFormat { + return AVPixelFormat(ctx.pix_fmt) +} + +// Pixel format. +func (ctx *AVCodecContext) SetPixFmt(pix_fmt AVPixelFormat) { + ctx.pix_fmt = C.enum_AVPixelFormat(pix_fmt) +} + +// Private data, set key/value pair +func (ctx *AVCodecContext) SetPrivDataKV(name, value string) error { + cName, cValue := C.CString(name), C.CString(value) + defer C.free(unsafe.Pointer(cName)) + defer C.free(unsafe.Pointer(cValue)) + if ret := AVError(C.av_opt_set(ctx.priv_data, cName, cValue, 0)); ret != 0 { + return ret + } + return nil +} + +// Set Macroblock decision mode. +func (ctx *AVCodecContext) SetMbDecision(mode AVCodecMacroblockDecisionMode) { + ctx.mb_decision = C.int(mode) +} + +// Get Macroblock decision mode. +func (ctx *AVCodecContext) MbDecision() AVCodecMacroblockDecisionMode { + return AVCodecMacroblockDecisionMode(ctx.mb_decision) +} + +// Get flags +func (ctx *AVCodecContext) Flags() AVCodecFlag { + return AVCodecFlag(ctx.flags) + +} + +// Set flags +func (ctx *AVCodecContext) SetFlags(flags AVCodecFlag) { + ctx.flags = C.int(flags) +} + +// Get flags2 +func (ctx *AVCodecContext) Flags2() AVCodecFlag2 { + return AVCodecFlag2(ctx.flags2) +} + +// Set flags2 +func (ctx *AVCodecContext) SetFlags2(flags2 AVCodecFlag2) { + ctx.flags2 = C.int(flags2) +} + +//////////////////////////////////////////////////////////////////////////////// +// AVProfile + +func (c *AVProfile) ID() int { + return int(c.profile) +} + +func (c *AVProfile) Name() string { + return C.GoString(c.name) +} + +//////////////////////////////////////////////////////////////////////////////// +// AVCodecCap + +func (v AVCodecCap) Is(cap AVCodecCap) bool { + return v&cap == cap +} + +func (v AVCodecCap) String() string { + if v == AV_CODEC_CAP_NONE { + return v.FlagString() + } + str := "" + for i := AVCodecCap(C.int(1)); i <= AV_CODEC_CAP_MAX; i <<= 1 { + if v&i == i { + str += "|" + i.FlagString() + } + } + return str[1:] +} + +func (v AVCodecCap) FlagString() string { + switch v { + case AV_CODEC_CAP_NONE: + return "AV_CODEC_CAP_NONE" + case AV_CODEC_CAP_DRAW_HORIZ_BAND: + return "AV_CODEC_CAP_DRAW_HORIZ_BAND" + case AV_CODEC_CAP_DR1: + return "AV_CODEC_CAP_DR1" + case AV_CODEC_CAP_DELAY: + return "AV_CODEC_CAP_DELAY" + case AV_CODEC_CAP_SMALL_LAST_FRAME: + return "AV_CODEC_CAP_SMALL_LAST_FRAME" + case AV_CODEC_CAP_SUBFRAMES: + return "AV_CODEC_CAP_SUBFRAMES" + case AV_CODEC_CAP_EXPERIMENTAL: + return "AV_CODEC_CAP_EXPERIMENTAL" + case AV_CODEC_CAP_CHANNEL_CONF: + return "AV_CODEC_CAP_CHANNEL_CONF" + case AV_CODEC_CAP_FRAME_THREADS: + return "AV_CODEC_CAP_FRAME_THREADS" + case AV_CODEC_CAP_SLICE_THREADS: + return "AV_CODEC_CAP_SLICE_THREADS" + case AV_CODEC_CAP_PARAM_CHANGE: + return "AV_CODEC_CAP_PARAM_CHANGE" + case AV_CODEC_CAP_OTHER_THREADS: + return "AV_CODEC_CAP_OTHER_THREADS" + case AV_CODEC_CAP_VARIABLE_FRAME_SIZE: + return "AV_CODEC_CAP_VARIABLE_FRAME_SIZE" + case AV_CODEC_CAP_AVOID_PROBING: + return "AV_CODEC_CAP_AVOID_PROBING" + case AV_CODEC_CAP_HARDWARE: + return "AV_CODEC_CAP_HARDWARE" + case AV_CODEC_CAP_HYBRID: + return "AV_CODEC_CAP_HYBRID" + case AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE: + return "AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE" + case AV_CODEC_CAP_ENCODER_FLUSH: + return "AV_CODEC_CAP_ENCODER_FLUSH" + case AV_CODEC_CAP_ENCODER_RECON_FRAME: + return "AV_CODEC_CAP_ENCODER_RECON_FRAME" + default: + return fmt.Sprintf("AVCodecCap(0x%08X)", uint32(v)) + } +} diff --git a/sys/ffmpeg61/avcodec_core.go b/sys/ffmpeg61/avcodec_core.go new file mode 100644 index 0000000..d42bac2 --- /dev/null +++ b/sys/ffmpeg61/avcodec_core.go @@ -0,0 +1,101 @@ +package ffmpeg + +import "unsafe" + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavcodec +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC FUNCTIONS + +// Allocate an AVCodecContext and set its fields to default values. +func AVCodec_alloc_context(codec *AVCodec) *AVCodecContext { + return (*AVCodecContext)(C.avcodec_alloc_context3((*C.struct_AVCodec)(codec))) +} + +// Free the codec context and everything associated with it. +func AVCodec_free_context(ctx *AVCodecContext) { + C.avcodec_free_context((**C.struct_AVCodecContext)(unsafe.Pointer(&ctx))) +} + +// From fill the parameters based on the values from the supplied codec parameters +func AVCodec_parameters_copy(ctx *AVCodecParameters, codecpar *AVCodecParameters) error { + if err := AVError(C.avcodec_parameters_copy((*C.AVCodecParameters)(ctx), (*C.AVCodecParameters)(codecpar))); err != 0 { + return err + } else { + return nil + } +} + +// Fill the parameters struct based on the values from the supplied codec context (encoding) +func AVCodec_parameters_from_context(codecpar *AVCodecParameters, ctx *AVCodecContext) error { + if err := AVError(C.avcodec_parameters_from_context((*C.AVCodecParameters)(codecpar), (*C.struct_AVCodecContext)(ctx))); err < 0 { + return err + } + return nil +} + +// Fill the codec context based on the values from the supplied codec parameters (decoding) +func AVCodec_parameters_to_context(ctx *AVCodecContext, codecpar *AVCodecParameters) error { + if err := AVError(C.avcodec_parameters_to_context((*C.struct_AVCodecContext)(ctx), (*C.AVCodecParameters)(codecpar))); err < 0 { + return err + } + return nil +} + +// Initialize the AVCodecContext to use the given AVCodec. +func AVCodec_open(ctx *AVCodecContext, codec *AVCodec, options *AVDictionary) error { + var opts **C.struct_AVDictionary + if options != nil { + opts = &options.ctx + } + if err := AVError(C.avcodec_open2((*C.struct_AVCodecContext)(ctx), (*C.struct_AVCodec)(codec), opts)); err != 0 { + return err + } + return nil +} + +// Iterate over all registered codecs. +func AVCodec_iterate(opaque *uintptr) *AVCodec { + return (*AVCodec)(C.av_codec_iterate((*unsafe.Pointer)(unsafe.Pointer(opaque)))) +} + +// Find a registered decoder with a matching codec ID. +func AVCodec_find_decoder(id AVCodecID) *AVCodec { + return (*AVCodec)(C.avcodec_find_decoder((C.enum_AVCodecID)(id))) +} + +// Find a registered decoder with the specified name. +func AVCodec_find_decoder_by_name(name string) *AVCodec { + cStr := C.CString(name) + defer C.free(unsafe.Pointer(cStr)) + return (*AVCodec)(C.avcodec_find_decoder_by_name(cStr)) +} + +// Find a registered encoder with a matching codec ID. +func AVCodec_find_encoder(id AVCodecID) *AVCodec { + return (*AVCodec)(C.avcodec_find_encoder((C.enum_AVCodecID)(id))) +} + +// Find a registered encoder with the specified name. +func AVCodec_find_encoder_by_name(name string) *AVCodec { + cStr := C.CString(name) + defer C.free(unsafe.Pointer(cStr)) + return (*AVCodec)(C.avcodec_find_encoder_by_name(cStr)) +} + +// Return true if codec is an encoder, false otherwise. +func AVCodec_is_encoder(codec *AVCodec) bool { + return C.av_codec_is_encoder((*C.struct_AVCodec)(codec)) != 0 +} + +// Return true if codec is a decoder, false otherwise. +func AVCodec_is_decoder(codec *AVCodec) bool { + return C.av_codec_is_decoder((*C.struct_AVCodec)(codec)) != 0 +} diff --git a/sys/ffmpeg61/avcodec_core_test.go b/sys/ffmpeg61/avcodec_core_test.go new file mode 100644 index 0000000..6774044 --- /dev/null +++ b/sys/ffmpeg61/avcodec_core_test.go @@ -0,0 +1,46 @@ +package ffmpeg_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func Test_avcodec_core_000(t *testing.T) { + assert := assert.New(t) + + // Iterate over all codecs + var opaque uintptr + for { + codec := AVCodec_iterate(&opaque) + if codec == nil { + break + } + + t.Log("codec.name=", codec.Name()) + t.Log(" .longname=", codec.LongName()) + t.Log(" .type=", codec.Type()) + t.Log(" .id=", codec.ID()) + t.Log(" .encoder=", AVCodec_is_encoder(codec)) + t.Log(" .decoder=", AVCodec_is_decoder(codec)) + if AVCodec_is_encoder(codec) { + codec_ := AVCodec_find_encoder(codec.ID()) + assert.NotNil(codec_) + } else if AVCodec_is_decoder(codec) { + codec_ := AVCodec_find_decoder(codec.ID()) + assert.NotNil(codec_) + } + if codec.Type().Is(AVMEDIA_TYPE_VIDEO) { + t.Log(" .framerates=", codec.SupportedFramerates()) + t.Log(" .pixel_formats=", codec.PixelFormats()) + } + if codec.Type().Is(AVMEDIA_TYPE_AUDIO) { + t.Log(" .samplerates=", codec.SupportedSamplerates()) + t.Log(" .sample_formats=", codec.SampleFormats()) + } + t.Log(" .profile=", codec.Profiles()) + } +} diff --git a/sys/ffmpeg61/avcodec_decoding.go b/sys/ffmpeg61/avcodec_decoding.go new file mode 100644 index 0000000..0251964 --- /dev/null +++ b/sys/ffmpeg61/avcodec_decoding.go @@ -0,0 +1,55 @@ +package ffmpeg + +import ( + "io" + "syscall" +) + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavcodec +#include +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Return decoded output data from a decoder or encoder. Error return of +// EAGAIN means that more input is needed to produce output, while EINVAL +// means that the decoder has been flushed and no more output is available. +func AVCodec_receive_frame(ctx *AVCodecContext, frame *AVFrame) error { + if err := AVError(C.avcodec_receive_frame((*C.AVCodecContext)(ctx), (*C.AVFrame)(frame))); err != 0 { + if err == AVERROR_EOF { + return io.EOF + } else if err.IsErrno(syscall.EAGAIN) { + return syscall.EAGAIN + } else if err.IsErrno(syscall.EINVAL) { + return syscall.EINVAL + } else { + return err + } + } + return nil +} + +// Send a packet with a compressed frame to a decoder. Error return of +// EAGAIN means that more input is needed to produce output, while EINVAL +// means that the decoder has been flushed and no more output is available. +func AVCodec_send_packet(ctx *AVCodecContext, pkt *AVPacket) error { + if err := AVError(C.avcodec_send_packet((*C.AVCodecContext)(ctx), (*C.AVPacket)(pkt))); err != 0 { + if err == AVERROR_EOF { + return io.EOF + } else if err.IsErrno(syscall.EAGAIN) { + return syscall.EAGAIN + } else if err.IsErrno(syscall.EINVAL) { + return syscall.EINVAL + } else { + return err + } + } + return nil +} diff --git a/sys/ffmpeg61/avcodec_encoding.go b/sys/ffmpeg61/avcodec_encoding.go new file mode 100644 index 0000000..1ecf7c1 --- /dev/null +++ b/sys/ffmpeg61/avcodec_encoding.go @@ -0,0 +1,52 @@ +package ffmpeg + +import ( + "io" + "syscall" +) + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavcodec +#include +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Copy codec parameters from input stream to output codec context. +// Supply a raw video or audio frame to the encoder. +func AVCodec_send_frame(ctx *AVCodecContext, frame *AVFrame) error { + if err := AVError(C.avcodec_send_frame((*C.AVCodecContext)(ctx), (*C.AVFrame)(frame))); err != 0 { + if err == AVERROR_EOF { + return io.EOF + } else if err.IsErrno(syscall.EAGAIN) { + return syscall.EAGAIN + } else if err.IsErrno(syscall.EINVAL) { + return syscall.EINVAL + } else { + return err + } + } + return nil +} + +// Read encoded data from the encoder. +func AVCodec_receive_packet(ctx *AVCodecContext, pkt *AVPacket) error { + if err := AVError(C.avcodec_receive_packet((*C.AVCodecContext)(ctx), (*C.AVPacket)(pkt))); err != 0 { + if err == AVERROR_EOF { + return io.EOF + } else if err.IsErrno(syscall.EAGAIN) { + return syscall.EAGAIN + } else if err.IsErrno(syscall.EINVAL) { + return syscall.EINVAL + } else { + return err + } + } + return nil +} diff --git a/sys/ffmpeg61/avcodec_packet.go b/sys/ffmpeg61/avcodec_packet.go new file mode 100644 index 0000000..eef89bf --- /dev/null +++ b/sys/ffmpeg61/avcodec_packet.go @@ -0,0 +1,98 @@ +package ffmpeg + +import "unsafe" + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavcodec +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC FUNCTIONS + +// Allocate an AVPacket and set its fields to default values. +func AVCodec_packet_alloc() *AVPacket { + return (*AVPacket)(C.av_packet_alloc()) +} + +// Free the packet, if the packet is reference counted, it will be unreferenced first. +func AVCodec_packet_free(pkt *AVPacket) { + C.av_packet_free((**C.struct_AVPacket)(unsafe.Pointer(&pkt))) +} + +// Create a new packet that references the same data as src. +func AVCodec_packet_clone(src *AVPacket) *AVPacket { + return (*AVPacket)(C.av_packet_clone((*C.struct_AVPacket)(src))) +} + +// Allocate the payload of a packet and initialize its fields with default values. +func AVCodec_new_packet(pkt *AVPacket, size int) error { + if err := AVError(C.av_new_packet((*C.struct_AVPacket)(pkt), C.int(size))); err != 0 { + return err + } else { + return nil + } +} + +// Reduce packet size, correctly zeroing padding. +func AVCodec_shrink_packet(pkt *AVPacket, size int) { + C.av_shrink_packet((*C.struct_AVPacket)(pkt), C.int(size)) +} + +// Increase packet size, correctly zeroing padding. +func AVCodec_grow_packet(pkt *AVPacket, size int) error { + if err := AVError(C.av_grow_packet((*C.struct_AVPacket)(pkt), C.int(size))); err != 0 { + return err + } else { + return nil + } +} + +// Convert valid timing fields (timestamps / durations) in a packet from one timebase to another. +func AVCodec_packet_rescale_ts(pkt *AVPacket, tb_src, tb_dst AVRational) { + C.av_packet_rescale_ts((*C.AVPacket)(pkt), (C.AVRational)(tb_src), (C.AVRational)(tb_dst)) +} + +// Unreference the packet to release the data +func AVCodec_packet_unref(pkt *AVPacket) { + C.av_packet_unref((*C.struct_AVPacket)(pkt)) +} + +//////////////////////////////////////////////////////////////////////////////// +// AVPacket + +func (ctx *AVPacket) StreamIndex() int { + return int(ctx.stream_index) +} + +func (ctx *AVPacket) Pts() int64 { + return int64(ctx.pts) +} + +func (ctx *AVPacket) Dts() int64 { + return int64(ctx.dts) +} + +func (ctx *AVPacket) Duration() int64 { + return int64(ctx.duration) +} + +func (ctx *AVPacket) Pos() int64 { + return int64(ctx.pos) +} + +func (ctx *AVPacket) SetPos(pos int64) { + ctx.pos = C.int64_t(pos) +} + +func (ctx *AVPacket) Bytes() []byte { + return C.GoBytes(unsafe.Pointer(ctx.data), C.int(ctx.size)) +} + +func (ctx *AVPacket) Size() int { + return int(ctx.size) +} diff --git a/sys/ffmpeg61/avcodec_packet_test.go b/sys/ffmpeg61/avcodec_packet_test.go new file mode 100644 index 0000000..920dc11 --- /dev/null +++ b/sys/ffmpeg61/avcodec_packet_test.go @@ -0,0 +1,27 @@ +package ffmpeg_test + +import ( + "testing" + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" + "github.com/stretchr/testify/assert" +) + +func Test_avcodec_packet_000(t *testing.T) { + assert := assert.New(t) + packet := AVCodec_packet_alloc() + if !assert.NotNil(packet) { + t.SkipNow() + } + + if !assert.NoError(AVCodec_new_packet(packet, 1024)) { + t.SkipNow() + } + if !assert.NoError(AVCodec_grow_packet(packet, 2048)) { + t.SkipNow() + } + AVCodec_shrink_packet(packet, 1024) + AVCodec_packet_unref(packet) + AVCodec_packet_free(packet) +} diff --git a/sys/ffmpeg61/avcodec_parser.go b/sys/ffmpeg61/avcodec_parser.go new file mode 100644 index 0000000..28ec3b3 --- /dev/null +++ b/sys/ffmpeg61/avcodec_parser.go @@ -0,0 +1,32 @@ +package ffmpeg + +import "unsafe" + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavcodec +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC FUNCTIONS + +// Iterate over all registered codec parsers. +func AVCodec_parser_iterate(opaque *uintptr) *AVCodecParser { + return (*AVCodecParser)(C.av_parser_iterate((*unsafe.Pointer)(unsafe.Pointer(opaque)))) +} + +func AVCodec_parser_init(codec_id AVCodecID) *AVCodecParserContext { + return (*AVCodecParserContext)(C.av_parser_init(C.int(codec_id))) +} + +func AVCodec_parser_close(parser *AVCodecParserContext) { + C.av_parser_close((*C.AVCodecParserContext)(parser)) +} + +func AVCodec_parser_parse(parser *AVCodecParserContext, ctx *AVCodecContext, packet *AVPacket, buf []byte, pts int64, dts int64, pos int64) int { + return int(C.av_parser_parse2((*C.AVCodecParserContext)(parser), (*C.AVCodecContext)(ctx), &packet.data, &packet.size, (*C.uint8_t)(unsafe.Pointer(&buf[0])), C.int(len(buf)), C.int64_t(pts), C.int64_t(dts), C.int64_t(pos))) +} diff --git a/sys/ffmpeg61/avcodec_parser_test.go b/sys/ffmpeg61/avcodec_parser_test.go new file mode 100644 index 0000000..088a546 --- /dev/null +++ b/sys/ffmpeg61/avcodec_parser_test.go @@ -0,0 +1,23 @@ +package ffmpeg_test + +import ( + "testing" + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func Test_avcodec_parser_000(t *testing.T) { + //assert := assert.New(t) + + // Iterate over all codecs + var opaque uintptr + for { + parser := AVCodec_parser_iterate(&opaque) + if parser == nil { + break + } + + t.Log("codec_parser=", parser) + } +} diff --git a/sys/ffmpeg61/avcodec_version.go b/sys/ffmpeg61/avcodec_version.go new file mode 100644 index 0000000..94805a2 --- /dev/null +++ b/sys/ffmpeg61/avcodec_version.go @@ -0,0 +1,28 @@ +package ffmpeg + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavcodec +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Return the LIBAVCODEC_VERSION_INT constant. +func AVCodec_version() uint { + return uint(C.avcodec_version()) +} + +// Return the libavcodec build-time configuration. +func AVCodec_configuration() string { + return C.GoString(C.avcodec_configuration()) +} + +// Return the libavcodec license. +func AVCodec_license() string { + return C.GoString(C.avcodec_license()) +} diff --git a/sys/ffmpeg61/avcodec_version_test.go b/sys/ffmpeg61/avcodec_version_test.go new file mode 100644 index 0000000..e2d4e64 --- /dev/null +++ b/sys/ffmpeg61/avcodec_version_test.go @@ -0,0 +1,20 @@ +package ffmpeg_test + +import ( + "testing" + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func Test_avcodec_version_000(t *testing.T) { + t.Log("avcodec_version=", AVCodec_version()) +} + +func Test_avcodec_version_001(t *testing.T) { + t.Log("avcodec_configuration=", AVCodec_configuration()) +} + +func Test_avcodec_version_002(t *testing.T) { + t.Log("avcodec_license=", AVCodec_license()) +} diff --git a/sys/ffmpeg61/avformat.go b/sys/ffmpeg61/avformat.go new file mode 100644 index 0000000..605dcb6 --- /dev/null +++ b/sys/ffmpeg61/avformat.go @@ -0,0 +1,292 @@ +package ffmpeg + +import ( + "encoding/json" + "unsafe" +) + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavformat +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// TYPES + +type ( + AVInputFormat C.struct_AVInputFormat + AVOutputFormat C.struct_AVOutputFormat + AVFormat C.int + AVFormatFlag C.int + AVFormatContext C.struct_AVFormatContext + AVIOContext C.struct_AVIOContext + AVStream C.struct_AVStream + AVIOFlag C.int + AVTimestamp C.int64_t +) + +type jsonAVIOContext struct { + IsEOF bool `json:"is_eof,omitempty"` + IsWriteable bool `json:"is_writeable,omitempty"` + IsSeekable bool `json:"is_seekable,omitempty"` + IsDirect bool `json:"is_direct,omitempty"` + Pos int64 `json:"pos,omitempty"` + BufferSize int `json:"buffer_size,omitempty"` + BytesRead int64 `json:"bytes_read,omitempty"` + BytesWritten int64 `json:"bytes_written,omitempty"` + Error string `json:"error,omitempty"` +} + +//////////////////////////////////////////////////////////////////////////////// +// CONSTANTS + +const ( + /** + * ORing this as the "whence" parameter to a seek function causes it to + * return the filesize without seeking anywhere. Supporting this is optional. + * If it is not supported then the seek function will return <0. + */ + AVSEEK_SIZE = C.AVSEEK_SIZE + + /** + * Passing this flag as the "whence" parameter to a seek function causes it to + * seek by any means (like reopening and linear reading) or other normally unreasonable + * means that can be extremely slow. + * This may be ignored by the seek code. + */ + 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 + AVIO_FLAG_WRITE AVIOFlag = C.AVIO_FLAG_WRITE + AVIO_FLAG_READ_WRITE AVIOFlag = C.AVIO_FLAG_READ_WRITE +) + +//////////////////////////////////////////////////////////////////////////////// +// JSON OUTPUT + +func (ctx *AVIOContext) MarshalJSON() ([]byte, error) { + return json.Marshal(jsonAVIOContext{ + IsEOF: ctx.eof_reached != 0, + IsWriteable: ctx.write_flag != 0, + IsSeekable: ctx.seekable != 0, + IsDirect: ctx.direct != 0, + Pos: int64(ctx.pos), + BufferSize: int(ctx.buffer_size), + BytesRead: int64(ctx.bytes_read), + BytesWritten: int64(ctx.bytes_written), + Error: AVError(ctx.error).Error(), + }) +} + +type jsonAVFormatContext struct { + Pb *AVIOContext `json:"pb,omitempty"` + Input *AVInputFormat `json:"input_format,omitempty"` + Output *AVOutputFormat `json:"output_format,omitempty"` + Url string `json:"url,omitempty"` + NumStreams uint `json:"num_streams,omitempty"` + Streams []*AVStream `json:"streams,omitempty"` + StartTime int64 `json:"start_time,omitempty"` + Duration int64 `json:"duration,omitempty"` + BitRate int64 `json:"bit_rate,omitempty"` + Flags AVFormatFlag `json:"flags,omitempty"` +} + +func (ctx *AVFormatContext) MarshalJSON() ([]byte, error) { + return json.Marshal(jsonAVFormatContext{ + Pb: (*AVIOContext)(ctx.pb), + Input: (*AVInputFormat)(ctx.iformat), + Output: (*AVOutputFormat)(ctx.oformat), + Url: C.GoString(ctx.url), + NumStreams: uint(ctx.nb_streams), + Streams: ctx.Streams(), + StartTime: int64(ctx.start_time), + Duration: int64(ctx.duration), + BitRate: int64(ctx.bit_rate), + Flags: AVFormatFlag(ctx.flags), + }) +} + +//////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (ctx *AVIOContext) String() string { + if str, err := json.MarshalIndent(ctx, "", " "); err != nil { + return err.Error() + } else { + return string(str) + } +} + +func (ctx *AVFormatContext) String() string { + data, _ := json.MarshalIndent(ctx, "", " ") + return string(data) +} + +//////////////////////////////////////////////////////////////////////////////// +// AVTimestamp + +func (v AVTimestamp) MarshalJSON() ([]byte, error) { + if v == AV_NOPTS_VALUE { + return json.Marshal(nil) + } else { + return json.Marshal(int64(v)) + } +} + +//////////////////////////////////////////////////////////////////////////////// +// AVFormatContext functions + +func (ctx *AVFormatContext) Input() *AVInputFormat { + return (*AVInputFormat)(ctx.iformat) +} + +func (ctx *AVFormatContext) Output() *AVOutputFormat { + return (*AVOutputFormat)(ctx.oformat) +} + +func (ctx *AVFormatContext) Metadata() *AVDictionary { + return &AVDictionary{ctx.metadata} +} + +func (ctx *AVFormatContext) SetPb(pb *AVIOContextEx) { + if pb == nil { + ctx.pb = nil + } else { + ctx.pb = (*C.struct_AVIOContext)(pb.AVIOContext) + } +} + +func (ctx *AVFormatContext) NumStreams() uint { + return uint(ctx.nb_streams) +} + +func (ctx *AVFormatContext) Streams() []*AVStream { + return cAVStreamSlice(unsafe.Pointer(ctx.streams), C.int(ctx.nb_streams)) +} + +func (ctx *AVFormatContext) Stream(stream int) *AVStream { + streams := ctx.Streams() + if stream < 0 || stream >= len(streams) { + return nil + } else { + return streams[stream] + } +} + +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 + +func (f AVFormatFlag) Is(flag AVFormatFlag) bool { + return f&flag != 0 +} diff --git a/sys/ffmpeg61/avformat_avio.go b/sys/ffmpeg61/avformat_avio.go new file mode 100644 index 0000000..d2dbe42 --- /dev/null +++ b/sys/ffmpeg61/avformat_avio.go @@ -0,0 +1,162 @@ +package ffmpeg + +import ( + "runtime" + "unsafe" +) + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavformat libavutil +#include + +extern int avio_read_callback(void* userInfo, uint8_t* buf, int buf_size); +extern int avio_write_callback(void* userInfo, uint8_t* buf, int buf_size); +extern int64_t avio_seek_callback(void* userInfo, int64_t offset, int whence); + +static AVIOContext* avio_alloc_context_(int sz, int writeable, void* userInfo) { + uint8_t* buf = av_malloc(sz); + if (!buf) { + return NULL; + } + return avio_alloc_context(buf, sz, writeable, userInfo,avio_read_callback,avio_write_callback,avio_seek_callback); +} +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// TYPES + +// Wrapper around AVIOContext with callbacks +type AVIOContextEx struct { + *AVIOContext + cb AVIOContextCallback + pin *runtime.Pinner +} + +// Callbacks for AVIOContextEx +type AVIOContextCallback interface { + Reader(buf []byte) int + Writer(buf []byte) int + Seeker(offset int64, whence int) int64 +} + +//////////////////////////////////////////////////////////////////////////////// +// FUNCTIONS + +// avio_alloc_context +func AVFormat_avio_alloc_context(sz int, writeable bool, callback AVIOContextCallback) *AVIOContextEx { + // Create a context + ctx := new(AVIOContextEx) + ctx.cb = callback + ctx.pin = new(runtime.Pinner) + ctx.pin.Pin(ctx.cb) + ctx.pin.Pin(ctx.pin) + + // Allocate the context + ctx.AVIOContext = (*AVIOContext)(C.avio_alloc_context_( + C.int(sz), + boolToInt(writeable), + unsafe.Pointer(ctx), + )) + if ctx.AVIOContext == nil { + return nil + } + + return ctx +} + +// Create and initialize a AVIOContext for accessing the resource indicated by url. +func AVFormat_avio_open(url string, flags AVIOFlag) (*AVIOContextEx, error) { + ctx := new(AVIOContextEx) + ctx.pin = new(runtime.Pinner) + ctx.pin.Pin(ctx.pin) + cUrl := C.CString(url) + defer C.free(unsafe.Pointer(cUrl)) + if err := AVError(C.avio_open((**C.struct_AVIOContext)(unsafe.Pointer(&ctx.AVIOContext)), cUrl, C.int(flags))); err != 0 { + return nil, err + } + + // Return success + return ctx, nil +} + +// Close the resource and free it. +// This function can only be used if it was opened by avio_open(). +func AVFormat_avio_close(ctx *AVIOContextEx) error { + ctx_ := (*C.struct_AVIOContext)(ctx.AVIOContext) + if err := AVError(C.avio_closep(&ctx_)); err != 0 { + return err + } + + // Return success + return nil +} + +// avio_context_free +func AVFormat_avio_context_free(ctx *AVIOContextEx) { + C.av_free(unsafe.Pointer(ctx.buffer)) + C.avio_context_free((**C.struct_AVIOContext)(unsafe.Pointer(&ctx.AVIOContext))) + ctx.pin.Unpin() +} + +// avio_w8 +func AVFormat_avio_w8(ctx *AVIOContextEx, b int) { + C.avio_w8((*C.struct_AVIOContext)(ctx.AVIOContext), C.int(b)) +} + +// avio_write +func AVFormat_avio_write(ctx *AVIOContextEx, buf []byte) { + C.avio_write((*C.struct_AVIOContext)(ctx.AVIOContext), (*C.uint8_t)(&buf[0]), C.int(len(buf))) +} + +// avio_wl64 +func AVFormat_avio_wl64(ctx *AVIOContextEx, b uint64) { + C.avio_wl64((*C.struct_AVIOContext)(ctx.AVIOContext), C.uint64_t(b)) +} + +// avio_put_str +func AVFormat_avio_put_str(ctx *AVIOContextEx, str string) int { + cStr := C.CString(str) + defer C.free(unsafe.Pointer(cStr)) + return int(C.avio_put_str((*C.struct_AVIOContext)(ctx.AVIOContext), cStr)) +} + +// avio_seek +// whence: SEEK_SET, SEEK_CUR, SEEK_END (like fseek) and AVSEEK_SIZE +func AVFormat_avio_seek(ctx *AVIOContextEx, offset int64, whence int) int64 { + return int64(C.avio_seek((*C.struct_AVIOContext)(ctx.AVIOContext), C.int64_t(offset), C.int(whence))) +} + +// avio_flush +func AVFormat_avio_flush(ctx *AVIOContextEx) { + C.avio_flush((*C.struct_AVIOContext)(ctx.AVIOContext)) +} + +// avio_read +func AVFormat_avio_read(ctx *AVIOContextEx, buf []byte) int { + return int(C.avio_read((*C.struct_AVIOContext)(ctx.AVIOContext), (*C.uint8_t)(&buf[0]), C.int(len(buf)))) +} + +//////////////////////////////////////////////////////////////////////////////// +// CALLBACKS + +//export avio_read_callback +func avio_read_callback(userInfo unsafe.Pointer, buf *C.uint8_t, size C.int) C.int { + ctx := (*AVIOContextEx)(userInfo) + return C.int(ctx.cb.Reader(cByteSlice(unsafe.Pointer(buf), size))) +} + +//export avio_write_callback +func avio_write_callback(userInfo unsafe.Pointer, buf *C.uint8_t, size C.int) C.int { + ctx := (*AVIOContextEx)(userInfo) + return C.int(ctx.cb.Writer(cByteSlice(unsafe.Pointer(buf), size))) +} + +//export avio_seek_callback +func avio_seek_callback(userInfo unsafe.Pointer, offset C.int64_t, whence C.int) C.int64_t { + ctx := (*AVIOContextEx)(userInfo) + return C.int64_t(ctx.cb.Seeker(int64(offset), int(whence))) +} diff --git a/sys/ffmpeg61/avformat_avio_test.go b/sys/ffmpeg61/avformat_avio_test.go new file mode 100644 index 0000000..02fd8c5 --- /dev/null +++ b/sys/ffmpeg61/avformat_avio_test.go @@ -0,0 +1,63 @@ +package ffmpeg_test + +import ( + "bytes" + "fmt" + "testing" + + // Packages + "github.com/stretchr/testify/assert" + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +var ( + // Create some data to read from + data = new(bytes.Buffer) +) + +type reader struct{} + +func (r *reader) Reader(buf []byte) int { + n, err := data.Read(buf) + if err != nil { + return AVERROR_EOF + } else { + return n + } +} + +func (r *reader) Writer([]byte) int { + return AVERROR_EOF +} + +func (r *reader) Seeker(int64, int) int64 { + return -1 +} + +func Test_avio_001(t *testing.T) { + assert := assert.New(t) + + // Populate the data + for i := 0; i < 100; i++ { + data.WriteString(fmt.Sprintf("%v: hello, world\n", i)) + } + + // Create the context + ctx := AVFormat_avio_alloc_context(20, false, new(reader)) + assert.NotNil(ctx) + + // Read the data + var buf [100]byte + for { + n := AVFormat_avio_read(ctx, buf[:]) + if n == AVERROR_EOF { + break + } + fmt.Println("N=", n, string(buf[:n])) + } + + // Free the context + AVFormat_avio_context_free(ctx) +} diff --git a/sys/ffmpeg61/avformat_core.go b/sys/ffmpeg61/avformat_core.go new file mode 100644 index 0000000..0b955f7 --- /dev/null +++ b/sys/ffmpeg61/avformat_core.go @@ -0,0 +1,22 @@ +package ffmpeg + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavformat +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// BINDINGS + +// Allocate an AVFormatContext. +func AVFormat_alloc_context() *AVFormatContext { + return (*AVFormatContext)(C.avformat_alloc_context()) +} + +func AVFormat_free_context(ctx *AVFormatContext) { + C.avformat_free_context((*C.struct_AVFormatContext)(ctx)) +} diff --git a/sys/ffmpeg61/avformat_demux.go b/sys/ffmpeg61/avformat_demux.go new file mode 100644 index 0000000..623afd6 --- /dev/null +++ b/sys/ffmpeg61/avformat_demux.go @@ -0,0 +1,140 @@ +package ffmpeg + +import ( + "io" + "syscall" + "unsafe" +) + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavformat +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Open an input stream and read the header. +func AVFormat_open_reader(reader *AVIOContextEx, format *AVInputFormat, options *AVDictionary) (*AVFormatContext, error) { + 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) + } else { + ctx.pb = (*C.struct_AVIOContext)(unsafe.Pointer(reader.AVIOContext)) + } + + // Open the stream + if err := AVError(C.avformat_open_input((**C.struct_AVFormatContext)(unsafe.Pointer(&ctx)), nil, (*C.struct_AVInputFormat)(format), opts)); err != 0 { + return nil, err + } else { + return ctx, nil + } +} + +// Open an input stream from a URL and read the header. +func AVFormat_open_url(url string, format *AVInputFormat, options *AVDictionary) (*AVFormatContext, error) { + var opts **C.struct_AVDictionary + if options != nil { + opts = &options.ctx + } + + // Create a C string for the URL + cUrl := C.CString(url) + defer C.free(unsafe.Pointer(cUrl)) + + // Allocate a context + ctx := AVFormat_alloc_context() + if ctx == nil { + return nil, AVError(syscall.ENOMEM) + } + + // Open the URL + if err := AVError(C.avformat_open_input((**C.struct_AVFormatContext)(unsafe.Pointer(&ctx)), cUrl, (*C.struct_AVInputFormat)(format), opts)); err != 0 { + return nil, err + } + + // Return success + return ctx, nil +} + +// Open an input stream from a device. +func AVFormat_open_device(format *AVInputFormat, options *AVDictionary) (*AVFormatContext, error) { + return AVFormat_open_url("", format, options) +} + +// Close an opened input AVFormatContext, free it and all its contents. +func AVFormat_close_input(ctx *AVFormatContext) { + C.avformat_close_input((**C.struct_AVFormatContext)(unsafe.Pointer(&ctx))) +} + +// Read packets of a media file to get stream information. +func AVFormat_find_stream_info(ctx *AVFormatContext, options *AVDictionary) error { + var opts **C.struct_AVDictionary + if options != nil { + opts = &options.ctx + } + if err := AVError(C.avformat_find_stream_info((*C.struct_AVFormatContext)(ctx), opts)); err != 0 { + return err + } + // Return success + return nil +} + +// Read a frame from the input stream. Return io.EOF if the end of the stream is reached. +func AVFormat_read_frame(ctx *AVFormatContext, packet *AVPacket) error { + if err := AVError(C.av_read_frame((*C.struct_AVFormatContext)(ctx), (*C.struct_AVPacket)(packet))); err < 0 { + if err == AVERROR_EOF { + return io.EOF + } else { + return err + } + } + // Return success + return nil +} + +// Return the next frame of a stream. +func AVFormat_seek_frame(ctx *AVFormatContext, stream_index int, timestamp int64, flags int) error { + if err := AVError(C.av_seek_frame((*C.struct_AVFormatContext)(ctx), C.int(stream_index), C.int64_t(timestamp), C.int(flags))); err != 0 { + return err + } + // Return success + return nil +} + +// Discard all internally buffered data. +func AVFormat_flush(ctx *AVFormatContext) error { + if err := AVError(C.avformat_flush((*C.struct_AVFormatContext)(ctx))); err != 0 { + return err + } + // Return success + return nil +} + +// Start playing a network-based stream (e.g. RTSP stream) at the current position. +func AVFormat_read_play(ctx *AVFormatContext) error { + if err := AVError(C.av_read_play((*C.struct_AVFormatContext)(ctx))); err != 0 { + return err + } + // Return success + return nil +} + +// Pause a network-based stream (e.g. RTSP stream). +func AVFormat_read_pause(ctx *AVFormatContext) error { + if err := AVError(C.av_read_pause((*C.struct_AVFormatContext)(ctx))); err != 0 { + return err + } + // Return success + return nil +} diff --git a/sys/ffmpeg61/avformat_demux_test.go b/sys/ffmpeg61/avformat_demux_test.go new file mode 100644 index 0000000..93b4011 --- /dev/null +++ b/sys/ffmpeg61/avformat_demux_test.go @@ -0,0 +1,133 @@ +package ffmpeg_test + +import ( + "io" + "os" + "syscall" + "testing" + + // Packages + "github.com/stretchr/testify/assert" + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +const ( + TEST_MP4_FILE = "../../etc/test/sample.mp4" +) + +func Test_avformat_demux_001(t *testing.T) { + assert := assert.New(t) + + // Open the file + filereader, err := NewFileReader(TEST_MP4_FILE) + if !assert.NoError(err) { + t.FailNow() + } + defer filereader.Close() + + // Create the context + ctx := AVFormat_avio_alloc_context(20, false, filereader) + assert.NotNil(ctx) + defer AVFormat_avio_context_free(ctx) + + // Open for demuxing + input, err := AVFormat_open_reader(ctx, nil, nil) + if !assert.NoError(err) { + t.FailNow() + } + defer AVFormat_free_context(input) + + t.Log(input) +} + +func Test_avformat_demux_002(t *testing.T) { + assert := assert.New(t) + + // Open for demuxing + input, err := AVFormat_open_url(TEST_MP4_FILE, nil, nil) + if !assert.NoError(err) { + t.FailNow() + } + defer AVFormat_free_context(input) + + packet := AVCodec_packet_alloc() + if !assert.NotNil(packet) { + t.SkipNow() + } + defer AVCodec_packet_free(packet) + + for { + if err := AVFormat_read_frame(input, packet); err != nil { + if err == io.EOF { + break + } + if !assert.NoError(err) { + t.FailNow() + } + } + + // Output the packet + t.Logf("Packet: %v", packet) + + // Mark the packet as consumed + AVCodec_packet_unref(packet) + } + +} + +//////////////////////////////////////////////////////////////////////////////// +// filereader implements the AVIOContext interface for reading from a file + +type filereader struct { + r io.ReadSeekCloser +} + +func NewFileReader(filename string) (*filereader, error) { + r, err := os.Open(filename) + if err != nil { + return nil, err + } + return &filereader{r}, nil +} + +func (r *filereader) Reader(buf []byte) int { + if n, err := r.r.Read(buf); err == io.EOF { + return AVERROR_EOF + } else if err != nil { + if errno, ok := err.(syscall.Errno); ok { + return int(errno) + } else { + return AVERROR_UNKNOWN + } + } else { + return n + } +} + +func (r *filereader) Writer([]byte) int { + // Reader does not implement the writer + return AVERROR_EOF +} + +func (r *filereader) Seeker(offset int64, whence int) int64 { + whence = whence & ^AVSEEK_FORCE + switch whence { + case AVSEEK_SIZE: + // TODO: Not sure what to put here yet + return -1 + case io.SeekStart, io.SeekCurrent, io.SeekEnd: + n, err := r.r.Seek(offset, whence) + if err != nil { + return -1 + } + return n + default: + return -1 + } +} + +func (r *filereader) Close() error { + return r.r.Close() +} diff --git a/sys/ffmpeg61/avformat_dump.go b/sys/ffmpeg61/avformat_dump.go new file mode 100644 index 0000000..9cf6d15 --- /dev/null +++ b/sys/ffmpeg61/avformat_dump.go @@ -0,0 +1,21 @@ +package ffmpeg + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavformat +#include +*/ +import "C" +import "unsafe" + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +func AVFormat_dump_format(ctx *AVFormatContext, stream_index int, filename string) { + ctx_ := (*C.struct_AVFormatContext)(ctx) + cFilename := C.CString(filename) + defer C.free(unsafe.Pointer(cFilename)) + C.av_dump_format((*C.AVFormatContext)(ctx), C.int(stream_index), cFilename, boolToInt(ctx_.oformat != nil)) +} diff --git a/sys/ffmpeg61/avformat_dump_test.go b/sys/ffmpeg61/avformat_dump_test.go new file mode 100644 index 0000000..ee58595 --- /dev/null +++ b/sys/ffmpeg61/avformat_dump_test.go @@ -0,0 +1,28 @@ +package ffmpeg_test + +import ( + "testing" + + // Packages + "github.com/stretchr/testify/assert" + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func Test_avformat_dump_001(t *testing.T) { + assert := assert.New(t) + + // Open input file + input, err := AVFormat_open_url(TEST_MP4_FILE, nil, nil) + if !assert.NoError(err) { + t.SkipNow() + } + defer AVFormat_close_input(input) + + // Fine stream information + assert.NoError(AVFormat_find_stream_info(input, nil)) + + // Dump the input format + AVFormat_dump_format(input, 0, TEST_MP4_FILE) +} diff --git a/sys/ffmpeg61/avformat_input.go b/sys/ffmpeg61/avformat_input.go new file mode 100644 index 0000000..445cb8c --- /dev/null +++ b/sys/ffmpeg61/avformat_input.go @@ -0,0 +1,52 @@ +package ffmpeg + +import ( + "encoding/json" + "unsafe" +) + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavformat +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +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"` +} + +func (ctx *AVInputFormat) MarshalJSON() ([]byte, error) { + return json.Marshal(jsonAVInputFormat{ + Name: C.GoString(ctx.name), + LongName: C.GoString(ctx.long_name), + MimeTypes: C.GoString(ctx.mime_type), + Extensions: C.GoString(ctx.extensions), + Flags: AVFormat(ctx.flags), + }) +} + +func (ctx *AVInputFormat) String() string { + if str, err := json.MarshalIndent(ctx, "", " "); err != nil { + return err.Error() + } else { + return string(str) + } +} + +//////////////////////////////////////////////////////////////////////////////// +// BINDINGS + +// Iterate over all AVInputFormats +func AVFormat_demuxer_iterate(opaque *uintptr) *AVInputFormat { + return (*AVInputFormat)(C.av_demuxer_iterate((*unsafe.Pointer)(unsafe.Pointer(opaque)))) +} diff --git a/sys/ffmpeg61/avformat_input_test.go b/sys/ffmpeg61/avformat_input_test.go new file mode 100644 index 0000000..a51a484 --- /dev/null +++ b/sys/ffmpeg61/avformat_input_test.go @@ -0,0 +1,23 @@ +package ffmpeg_test + +import ( + "testing" + + // Packages + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func Test_avformat_input_001(t *testing.T) { + // Iterate over all input formats + var opaque uintptr + for { + demuxer := AVFormat_demuxer_iterate(&opaque) + if demuxer == nil { + break + } + + t.Log(demuxer) + } +} diff --git a/sys/ffmpeg61/avformat_mux.go b/sys/ffmpeg61/avformat_mux.go new file mode 100644 index 0000000..25bf5f9 --- /dev/null +++ b/sys/ffmpeg61/avformat_mux.go @@ -0,0 +1,131 @@ +package ffmpeg + +import ( + "errors" + "unsafe" +) + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavformat +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Open an output stream. +func AVFormat_open_writer(writer *AVIOContextEx, format *AVOutputFormat, filename string) (*AVFormatContext, error) { + // TODO + return nil, errors.New("not implemented") +} + +// Open an output file. +func AVFormat_create_file(filename string, format *AVOutputFormat) (*AVFormatContext, error) { + var ctx *AVFormatContext + if err := AVError(C.avformat_alloc_output_context2((**C.struct_AVFormatContext)(unsafe.Pointer(&ctx)), (*C.struct_AVOutputFormat)(format), nil, C.CString(filename))); err != 0 { + return nil, err + } else if !ctx.Flags().Is(AVFMT_NOFILE) { + if ioctx, err := AVFormat_avio_open(filename, AVIO_FLAG_WRITE); err != nil { + return nil, err + } else { + ctx.SetPb(ioctx) + } + } + + // Return success + return ctx, nil +} + +func AVFormat_close_writer(ctx *AVFormatContext) error { + var result error + + octx := (*C.struct_AVFormatContext)(ctx) + if octx.oformat.flags&C.int(AVFMT_NOFILE) == 0 && octx.pb != nil { + if err := AVError(C.avio_closep(&octx.pb)); err != 0 { + result = errors.Join(result, err) + } + } + C.avformat_free_context(octx) + + // Return any errors + return result +} + +// Allocate an AVFormatContext for an output format. +func AVFormat_alloc_output_context2(ctx **AVFormatContext, format *AVOutputFormat, filename string) error { + var cFilename *C.char + if filename != "" { + cFilename = C.CString(filename) + } + defer C.free(unsafe.Pointer(cFilename)) + if err := AVError(C.avformat_alloc_output_context2((**C.struct_AVFormatContext)(unsafe.Pointer(&ctx)), (*C.struct_AVOutputFormat)(format), nil, cFilename)); err != 0 { + return err + } + + // Return success + return nil +} + +// Allocate the stream private data and initialize the codec, but do not write the header. +// May optionally be used before avformat_write_header() to initialize stream parameters before actually writing the header. +func AVFormat_init_output(ctx *AVFormatContext, options *AVDictionary) error { + var opts **C.struct_AVDictionary + if options != nil { + opts = &options.ctx + } + if err := AVError(C.avformat_init_output((*C.struct_AVFormatContext)(ctx), opts)); err != 0 { + return err + } else { + return nil + } +} + +// Allocate the stream private data and write the stream header to an output media file. +func AVFormat_write_header(ctx *AVFormatContext, options *AVDictionary) error { + var opts **C.struct_AVDictionary + if options != nil { + opts = &options.ctx + } + if err := AVError(C.avformat_write_header((*C.struct_AVFormatContext)(ctx), opts)); err != 0 { + return err + } else { + return nil + } + // TODO: + // AVSTREAM_INIT_IN_WRITE_HEADER + // AVSTREAM_INIT_IN_INIT_OUTPUT +} + +// Write a packet to an output media file. Returns true if flushed and there is +// no more data to flush. +func AVFormat_write_frame(ctx *AVFormatContext, pkt *AVPacket) (bool, error) { + if err := AVError(C.av_write_frame((*C.struct_AVFormatContext)(ctx), (*C.struct_AVPacket)(pkt))); err < 0 { + return false, err + } else if err == 0 { + return false, nil + } else { + return true, nil + } +} + +// Write a packet to an output media file ensuring correct interleaving. +func AVFormat_interleaved_write_frame(ctx *AVFormatContext, pkt *AVPacket) error { + if err := AVError(C.av_interleaved_write_frame((*C.struct_AVFormatContext)(ctx), (*C.struct_AVPacket)(pkt))); err != 0 { + return err + } else { + return nil + } +} + +// Write the stream trailer to an output media file and free the file private data. +func AVFormat_write_trailer(ctx *AVFormatContext) error { + if err := AVError(C.av_write_trailer((*C.struct_AVFormatContext)(ctx))); err != 0 { + return err + } else { + return nil + } +} diff --git a/sys/ffmpeg61/avformat_mux_test.go b/sys/ffmpeg61/avformat_mux_test.go new file mode 100644 index 0000000..0e66d0d --- /dev/null +++ b/sys/ffmpeg61/avformat_mux_test.go @@ -0,0 +1,133 @@ +package ffmpeg_test + +import ( + "io" + "os" + "path/filepath" + "testing" + + // Packages + "github.com/stretchr/testify/assert" + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func Test_avformat_mux_001(t *testing.T) { + assert := assert.New(t) + + // Create the file + filename := filepath.Join(os.TempDir(), "test.mp4") + output, err := AVFormat_create_file(filename, nil) + if !assert.NoError(err) { + t.FailNow() + } + + t.Log(output) + + // Close the file + assert.NoError(AVFormat_close_writer(output)) + +} + +func Test_avformat_mux_002(t *testing.T) { + assert := assert.New(t) + + // Allocate a packet + pkt := AVCodec_packet_alloc() + if !assert.NotNil(pkt) { + t.SkipNow() + } + defer AVCodec_packet_free(pkt) + + // Open input file + input, err := AVFormat_open_url(TEST_MP4_FILE, nil, nil) + if !assert.NoError(err) { + t.SkipNow() + } + defer AVFormat_close_input(input) + + // Fine stream information + assert.NoError(AVFormat_find_stream_info(input, nil)) + + // Dump the input format + AVFormat_dump_format(input, 0, TEST_MP4_FILE) + + // Open the output file + outfile := filepath.Join(os.TempDir(), "test.mp4") + output, err := AVFormat_create_file(outfile, nil) + if !assert.NoError(err) { + t.SkipNow() + } + defer AVFormat_close_writer(output) + + // Stream mapping + stream_map := make([]int, input.NumStreams()) + stream_index := 0 + for i := range stream_map { + in_stream := input.Stream(i) + in_codec_par := in_stream.CodecPar() + + // Ignore if not audio, video or subtitle + if in_codec_par.CodecType() != AVMEDIA_TYPE_AUDIO && in_codec_par.CodecType() != AVMEDIA_TYPE_VIDEO && in_codec_par.CodecType() != AVMEDIA_TYPE_SUBTITLE { + stream_map[i] = -1 + continue + } + + // Create a new stream + stream_map[i] = stream_index + stream_index = stream_index + 1 + + // Create a new output stream + out_stream := AVFormat_new_stream(output, nil) + if !assert.NotNil(out_stream) { + t.FailNow() + } + + // Copy the codec parameters + if err := AVCodec_parameters_copy(out_stream.CodecPar(), in_codec_par); !assert.NoError(err) { + t.FailNow() + } + + out_stream.CodecPar().SetCodecTag(0) + } + + // Dump the output format + AVFormat_dump_format(output, 0, outfile) + + // Write the header + if err := AVFormat_write_header(output, nil); !assert.NoError(err) { + t.FailNow() + } + + // Write the frames + for { + if err := AVFormat_read_frame(input, pkt); err != nil { + if err == io.EOF { + break + } + if !assert.NoError(err) { + t.FailNow() + } + } + in_stream := input.Stream(pkt.StreamIndex()) + if out_stream_index := stream_map[pkt.StreamIndex()]; out_stream_index < 0 { + continue + } else { + out_stream := output.Stream(out_stream_index) + + /* copy packet */ + AVCodec_packet_rescale_ts(pkt, in_stream.TimeBase(), out_stream.TimeBase()) + pkt.SetPos(-1) + + if err := AVFormat_interleaved_write_frame(output, pkt); !assert.NoError(err) { + t.FailNow() + } + } + } + + // Write the trailer + if err := AVFormat_write_trailer(output); !assert.NoError(err) { + t.FailNow() + } +} diff --git a/sys/ffmpeg61/avformat_output.go b/sys/ffmpeg61/avformat_output.go new file mode 100644 index 0000000..b97558f --- /dev/null +++ b/sys/ffmpeg61/avformat_output.go @@ -0,0 +1,95 @@ +package ffmpeg + +import ( + "encoding/json" + "unsafe" +) + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavformat +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +type jsonAVOutputFormat struct { + Name string `json:"name,omitempty"` + LongName string `json:"long_name,omitempty"` + MimeTypes string `json:"mime_types,omitempty"` + Flags AVFormat `json:"flags,omitempty"` + Extensions string `json:"extensions,omitempty"` + VideoCodec AVCodecID `json:"video_codec,omitempty"` + AudioCodec AVCodecID `json:"audio_codec,omitempty"` + SubtitleCodec AVCodecID `json:"subtitle_codec,omitempty"` +} + +func (ctx *AVOutputFormat) MarshalJSON() ([]byte, error) { + return json.Marshal(jsonAVOutputFormat{ + Name: C.GoString(ctx.name), + LongName: C.GoString(ctx.long_name), + MimeTypes: C.GoString(ctx.mime_type), + Flags: AVFormat(ctx.flags), + Extensions: C.GoString(ctx.extensions), + VideoCodec: AVCodecID(ctx.video_codec), + AudioCodec: AVCodecID(ctx.audio_codec), + SubtitleCodec: AVCodecID(ctx.subtitle_codec), + }) +} + +func (ctx *AVOutputFormat) String() string { + if str, err := json.MarshalIndent(ctx, "", " "); err != nil { + return err.Error() + } else { + return string(str) + } +} + +//////////////////////////////////////////////////////////////////////////////// +// PROPERTIES + +func (ctx *AVOutputFormat) VideoCodec() AVCodecID { + return AVCodecID(ctx.video_codec) +} + +func (ctx *AVOutputFormat) AudioCodec() AVCodecID { + return AVCodecID(ctx.audio_codec) +} + +func (ctx *AVOutputFormat) SubtitleCodec() AVCodecID { + return AVCodecID(ctx.subtitle_codec) +} + +func (ctx *AVOutputFormat) Flags() AVFormat { + return AVFormat(ctx.flags) +} + +//////////////////////////////////////////////////////////////////////////////// +// BINDINGS + +// Iterate over all AVOutputFormats +func AVFormat_muxer_iterate(opaque *uintptr) *AVOutputFormat { + return (*AVOutputFormat)(C.av_muxer_iterate((*unsafe.Pointer)(unsafe.Pointer(opaque)))) +} + +// Return the output format in the list of registered output formats which best matches the provided parameters, or return NULL if there is no match. +func AVFormat_guess_format(format, filename, mimetype string) *AVOutputFormat { + var cFilename, cFormat, cMimeType *C.char + if format != "" { + cFormat = C.CString(format) + } + if filename != "" { + cFilename = C.CString(filename) + } + if mimetype != "" { + cMimeType = C.CString(mimetype) + } + defer C.free(unsafe.Pointer(cFormat)) + defer C.free(unsafe.Pointer(cFilename)) + defer C.free(unsafe.Pointer(cMimeType)) + return (*AVOutputFormat)(C.av_guess_format(cFormat, cFilename, cMimeType)) +} diff --git a/sys/ffmpeg61/avformat_output_test.go b/sys/ffmpeg61/avformat_output_test.go new file mode 100644 index 0000000..55e4bdd --- /dev/null +++ b/sys/ffmpeg61/avformat_output_test.go @@ -0,0 +1,23 @@ +package ffmpeg_test + +import ( + "testing" + + // Packages + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func Test_avformat_output_001(t *testing.T) { + // Iterate over all output formats + var opaque uintptr + for { + muxer := AVFormat_muxer_iterate(&opaque) + if muxer == nil { + break + } + + t.Log(muxer) + } +} diff --git a/sys/ffmpeg61/avformat_stream.go b/sys/ffmpeg61/avformat_stream.go new file mode 100644 index 0000000..22c7587 --- /dev/null +++ b/sys/ffmpeg61/avformat_stream.go @@ -0,0 +1,90 @@ +package ffmpeg + +import "encoding/json" + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavformat +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// TYPES + +type jsonAVStream struct { + Index int `json:"index"` + Id int `json:"id"` + CodecPar *AVCodecParameters `json:"codec_par,omitempty"` + StartTime AVTimestamp `json:"start_time"` + Duration AVTimestamp `json:"duration"` + NumFrames int64 `json:"num_frames,omitempty"` + TimeBase AVRational `json:"time_base,omitempty"` +} + +//////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (ctx *AVStream) MarshalJSON() ([]byte, error) { + return json.Marshal(jsonAVStream{ + Index: int(ctx.index), + Id: int(ctx.id), + CodecPar: (*AVCodecParameters)(ctx.codecpar), + StartTime: AVTimestamp(ctx.start_time), + Duration: AVTimestamp(ctx.duration), + NumFrames: int64(ctx.nb_frames), + TimeBase: AVRational(ctx.time_base), + }) +} + +func (ctx *AVStream) String() string { + data, _ := json.MarshalIndent(ctx, "", " ") + return string(data) +} + +//////////////////////////////////////////////////////////////////////////////// +// PROPERTIES + +func (ctx *AVStream) Index() int { + return int(ctx.index) +} + +func (ctx *AVStream) Id() int { + return int(ctx.id) +} + +func (ctx *AVStream) SetId(id int) { + ctx.id = C.int(id) +} + +func (ctx *AVStream) CodecPar() *AVCodecParameters { + return (*AVCodecParameters)(ctx.codecpar) +} + +func (ctx *AVStream) TimeBase() AVRational { + return AVRational(ctx.time_base) +} + +func (ctx *AVStream) SetTimeBase(time_base AVRational) { + ctx.time_base = C.AVRational(time_base) +} + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +func AVFormat_new_stream(ctx *AVFormatContext, c *AVCodec) *AVStream { + return (*AVStream)(C.avformat_new_stream((*C.struct_AVFormatContext)(ctx), (*C.struct_AVCodec)(c))) +} + +// Find the best stream given the media type, wanted stream number, and related stream number. +func AVFormat_find_best_stream(ctx *AVFormatContext, t AVMediaType, wanted int, related int) (int, *AVCodec, error) { + var codec *C.struct_AVCodec + ret := int(C.av_find_best_stream((*C.struct_AVFormatContext)(ctx), (C.enum_AVMediaType)(t), C.int(wanted), C.int(related), (**C.struct_AVCodec)(&codec), 0)) + if ret < 0 { + return 0, nil, AVError(ret) + } else { + return ret, (*AVCodec)(codec), nil + } +} diff --git a/sys/ffmpeg61/avformat_version.go b/sys/ffmpeg61/avformat_version.go new file mode 100644 index 0000000..6ceb81f --- /dev/null +++ b/sys/ffmpeg61/avformat_version.go @@ -0,0 +1,28 @@ +package ffmpeg + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavformat +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - VERSION + +// Return the LIBAVFORMAT_VERSION_INT constant. +func AVFormat_version() uint { + return uint(C.avformat_version()) +} + +// Return the libavformat build-time configuration. +func AVFormat_configuration() string { + return C.GoString(C.avformat_configuration()) +} + +// Return the libavformat license. +func AVFormat_license() string { + return C.GoString(C.avformat_license()) +} diff --git a/sys/ffmpeg61/avformat_version_test.go b/sys/ffmpeg61/avformat_version_test.go new file mode 100644 index 0000000..7bdd980 --- /dev/null +++ b/sys/ffmpeg61/avformat_version_test.go @@ -0,0 +1,20 @@ +package ffmpeg_test + +import ( + "testing" + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func Test_avformat_version_000(t *testing.T) { + t.Log("avformat_version=", AVFormat_version()) +} + +func Test_avformat_version_001(t *testing.T) { + t.Log("avformat_configuration=", AVFormat_configuration()) +} + +func Test_avformat_version_002(t *testing.T) { + t.Log("avformat_license=", AVFormat_license()) +} diff --git a/sys/ffmpeg61/avutil.go b/sys/ffmpeg61/avutil.go new file mode 100644 index 0000000..2928a61 --- /dev/null +++ b/sys/ffmpeg61/avutil.go @@ -0,0 +1,199 @@ +package ffmpeg + +import ( + "encoding/json" +) + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavutil +#include +#include +#include +#include +#include + +AVChannelLayout _AV_CHANNEL_LAYOUT_MONO = AV_CHANNEL_LAYOUT_MONO; +AVChannelLayout _AV_CHANNEL_LAYOUT_STEREO = AV_CHANNEL_LAYOUT_STEREO; +AVChannelLayout _AV_CHANNEL_LAYOUT_2POINT1 = AV_CHANNEL_LAYOUT_2POINT1; +AVChannelLayout _AV_CHANNEL_LAYOUT_2_1 = AV_CHANNEL_LAYOUT_2_1; +AVChannelLayout _AV_CHANNEL_LAYOUT_SURROUND = AV_CHANNEL_LAYOUT_SURROUND; +AVChannelLayout _AV_CHANNEL_LAYOUT_3POINT1 = AV_CHANNEL_LAYOUT_3POINT1; +AVChannelLayout _AV_CHANNEL_LAYOUT_4POINT0 = AV_CHANNEL_LAYOUT_4POINT0; +AVChannelLayout _AV_CHANNEL_LAYOUT_4POINT1 = AV_CHANNEL_LAYOUT_4POINT1; +AVChannelLayout _AV_CHANNEL_LAYOUT_2_2 = AV_CHANNEL_LAYOUT_2_2; +AVChannelLayout _AV_CHANNEL_LAYOUT_QUAD = AV_CHANNEL_LAYOUT_QUAD; +AVChannelLayout _AV_CHANNEL_LAYOUT_5POINT0 = AV_CHANNEL_LAYOUT_5POINT0; +AVChannelLayout _AV_CHANNEL_LAYOUT_5POINT1 = AV_CHANNEL_LAYOUT_5POINT1; +AVChannelLayout _AV_CHANNEL_LAYOUT_5POINT0_BACK = AV_CHANNEL_LAYOUT_5POINT0_BACK; +AVChannelLayout _AV_CHANNEL_LAYOUT_5POINT1_BACK = AV_CHANNEL_LAYOUT_5POINT1_BACK; +AVChannelLayout _AV_CHANNEL_LAYOUT_6POINT0 = AV_CHANNEL_LAYOUT_6POINT0; +AVChannelLayout _AV_CHANNEL_LAYOUT_6POINT0_FRONT = AV_CHANNEL_LAYOUT_6POINT0_FRONT; +AVChannelLayout _AV_CHANNEL_LAYOUT_HEXAGONAL = AV_CHANNEL_LAYOUT_HEXAGONAL; +AVChannelLayout _AV_CHANNEL_LAYOUT_6POINT1 = AV_CHANNEL_LAYOUT_6POINT1; +AVChannelLayout _AV_CHANNEL_LAYOUT_6POINT1_BACK = AV_CHANNEL_LAYOUT_6POINT1_BACK; +AVChannelLayout _AV_CHANNEL_LAYOUT_6POINT1_FRONT = AV_CHANNEL_LAYOUT_6POINT1_FRONT; +AVChannelLayout _AV_CHANNEL_LAYOUT_7POINT0 = AV_CHANNEL_LAYOUT_7POINT0; +AVChannelLayout _AV_CHANNEL_LAYOUT_7POINT0_FRONT = AV_CHANNEL_LAYOUT_7POINT0_FRONT; +AVChannelLayout _AV_CHANNEL_LAYOUT_7POINT1 = AV_CHANNEL_LAYOUT_7POINT1; +AVChannelLayout _AV_CHANNEL_LAYOUT_7POINT1_WIDE = AV_CHANNEL_LAYOUT_7POINT1_WIDE; +AVChannelLayout _AV_CHANNEL_LAYOUT_7POINT1_WIDE_BACK = AV_CHANNEL_LAYOUT_7POINT1_WIDE_BACK; +AVChannelLayout _AV_CHANNEL_LAYOUT_OCTAGONAL = AV_CHANNEL_LAYOUT_OCTAGONAL; +AVChannelLayout _AV_CHANNEL_LAYOUT_HEXADECAGONAL = AV_CHANNEL_LAYOUT_HEXADECAGONAL; +AVChannelLayout _AV_CHANNEL_LAYOUT_STEREO_DOWNMIX = AV_CHANNEL_LAYOUT_STEREO_DOWNMIX; +AVChannelLayout _AV_CHANNEL_LAYOUT_22POINT2 = AV_CHANNEL_LAYOUT_22POINT2; +AVChannelLayout _AV_CHANNEL_LAYOUT_AMBISONIC_FIRST_ORDER = AV_CHANNEL_LAYOUT_AMBISONIC_FIRST_ORDER; +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// TYPES + +type ( + AVBufferRef C.struct_AVBufferRef + AVChannel C.enum_AVChannel + AVChannelLayout C.AVChannelLayout + AVClass C.AVClass + AVDictionary struct{ ctx *C.struct_AVDictionary } // Wrapper + AVDictionaryEntry C.struct_AVDictionaryEntry + AVDictionaryFlag C.int + AVError C.int + AVFrame C.struct_AVFrame + AVMediaType C.enum_AVMediaType + AVRational C.AVRational + AVPictureType C.enum_AVPictureType + AVPixelFormat C.enum_AVPixelFormat + AVPixFmtDescriptor C.AVPixFmtDescriptor + AVRounding C.enum_AVRounding + AVSampleFormat C.enum_AVSampleFormat +) + +type jsonAVClass struct { + ClassName string `json:"class_name"` +} + +type jsonAVDictionary struct { + Count int `json:"count"` + Elems []*AVDictionaryEntry `json:"elems,omitempty"` +} + +type jsonAVDictionaryEntry struct { + Key string `json:"key"` + Value string `json:"value,omitempty"` +} + +//////////////////////////////////////////////////////////////////////////////// +// CONSTANTS + +const ( + // Only get an entry with exact-case key match. + AV_DICT_MATCH_CASE AVDictionaryFlag = C.AV_DICT_MATCH_CASE + + // Return first entry in a dictionary whose first part corresponds to the search key, ignoring the suffix of the found key string. + AV_DICT_IGNORE_SUFFIX AVDictionaryFlag = C.AV_DICT_IGNORE_SUFFIX + + // Take ownership of key that has been allocated with av_malloc() + AV_DICT_DONT_STRDUP_KEY AVDictionaryFlag = C.AV_DICT_DONT_STRDUP_KEY + + // Take ownership of value that has been allocated with av_malloc() + AV_DICT_DONT_STRDUP_VAL AVDictionaryFlag = C.AV_DICT_DONT_STRDUP_VAL + + // Don't overwrite existing entries. + AV_DICT_DONT_OVERWRITE AVDictionaryFlag = C.AV_DICT_DONT_OVERWRITE + + // Append to existing key. + AV_DICT_APPEND AVDictionaryFlag = C.AV_DICT_APPEND + + // Allow to store several equal keys in the dictionary. + AV_DICT_MULTIKEY AVDictionaryFlag = C.AV_DICT_MULTIKEY +) + +var ( + AV_CHANNEL_LAYOUT_MONO = AVChannelLayout(C._AV_CHANNEL_LAYOUT_MONO) + AV_CHANNEL_LAYOUT_STEREO = AVChannelLayout(C._AV_CHANNEL_LAYOUT_STEREO) + AV_CHANNEL_LAYOUT_2POINT1 = AVChannelLayout(C._AV_CHANNEL_LAYOUT_2POINT1) + AV_CHANNEL_LAYOUT_2_1 = AVChannelLayout(C._AV_CHANNEL_LAYOUT_2_1) + AV_CHANNEL_LAYOUT_SURROUND = AVChannelLayout(C._AV_CHANNEL_LAYOUT_SURROUND) + AV_CHANNEL_LAYOUT_3POINT1 = AVChannelLayout(C._AV_CHANNEL_LAYOUT_3POINT1) + AV_CHANNEL_LAYOUT_4POINT0 = AVChannelLayout(C._AV_CHANNEL_LAYOUT_4POINT0) + AV_CHANNEL_LAYOUT_4POINT1 = AVChannelLayout(C._AV_CHANNEL_LAYOUT_4POINT1) + AV_CHANNEL_LAYOUT_2_2 = AVChannelLayout(C._AV_CHANNEL_LAYOUT_2_2) + AV_CHANNEL_LAYOUT_QUAD = AVChannelLayout(C._AV_CHANNEL_LAYOUT_QUAD) + AV_CHANNEL_LAYOUT_5POINT0 = AVChannelLayout(C._AV_CHANNEL_LAYOUT_5POINT0) + AV_CHANNEL_LAYOUT_5POINT1 = AVChannelLayout(C._AV_CHANNEL_LAYOUT_5POINT1) + AV_CHANNEL_LAYOUT_5POINT0_BACK = AVChannelLayout(C._AV_CHANNEL_LAYOUT_5POINT0_BACK) + AV_CHANNEL_LAYOUT_5POINT1_BACK = AVChannelLayout(C._AV_CHANNEL_LAYOUT_5POINT1_BACK) + AV_CHANNEL_LAYOUT_6POINT0 = AVChannelLayout(C._AV_CHANNEL_LAYOUT_6POINT0) + AV_CHANNEL_LAYOUT_6POINT0_FRONT = AVChannelLayout(C._AV_CHANNEL_LAYOUT_6POINT0_FRONT) + AV_CHANNEL_LAYOUT_HEXAGONAL = AVChannelLayout(C._AV_CHANNEL_LAYOUT_HEXAGONAL) + AV_CHANNEL_LAYOUT_6POINT1 = AVChannelLayout(C._AV_CHANNEL_LAYOUT_6POINT1) + AV_CHANNEL_LAYOUT_6POINT1_BACK = AVChannelLayout(C._AV_CHANNEL_LAYOUT_6POINT1_BACK) + AV_CHANNEL_LAYOUT_6POINT1_FRONT = AVChannelLayout(C._AV_CHANNEL_LAYOUT_6POINT1_FRONT) + AV_CHANNEL_LAYOUT_7POINT0 = AVChannelLayout(C._AV_CHANNEL_LAYOUT_7POINT0) + AV_CHANNEL_LAYOUT_7POINT0_FRONT = AVChannelLayout(C._AV_CHANNEL_LAYOUT_7POINT0_FRONT) + AV_CHANNEL_LAYOUT_7POINT1 = AVChannelLayout(C._AV_CHANNEL_LAYOUT_7POINT1) + AV_CHANNEL_LAYOUT_7POINT1_WIDE = AVChannelLayout(C._AV_CHANNEL_LAYOUT_7POINT1_WIDE) + AV_CHANNEL_LAYOUT_7POINT1_WIDE_BACK = AVChannelLayout(C._AV_CHANNEL_LAYOUT_7POINT1_WIDE_BACK) + AV_CHANNEL_LAYOUT_OCTAGONAL = AVChannelLayout(C._AV_CHANNEL_LAYOUT_OCTAGONAL) + AV_CHANNEL_LAYOUT_HEXADECAGONAL = AVChannelLayout(C._AV_CHANNEL_LAYOUT_HEXADECAGONAL) + AV_CHANNEL_LAYOUT_STEREO_DOWNMIX = AVChannelLayout(C._AV_CHANNEL_LAYOUT_STEREO_DOWNMIX) + AV_CHANNEL_LAYOUT_22POINT2 = AVChannelLayout(C._AV_CHANNEL_LAYOUT_22POINT2) + AV_CHANNEL_LAYOUT_AMBISONIC_FIRST_ORDER = AVChannelLayout(C._AV_CHANNEL_LAYOUT_AMBISONIC_FIRST_ORDER) +) + +const ( + AV_NOPTS_VALUE = C.AV_NOPTS_VALUE ///< Undefined timestamp value +) + +const ( + AV_ROUND_ZERO AVRounding = C.AV_ROUND_ZERO // Round toward zero. + AV_ROUND_INF AVRounding = C.AV_ROUND_INF // Round away from zero. + AV_ROUND_DOWN AVRounding = C.AV_ROUND_DOWN // Round toward -infinity. + AV_ROUND_UP AVRounding = C.AV_ROUND_UP // Round toward +infinity. + AV_ROUND_NEAR_INF AVRounding = C.AV_ROUND_NEAR_INF // Round to nearest and halfway cases away from zero. + AV_ROUND_PASS_MINMAX AVRounding = C.AV_ROUND_PASS_MINMAX // Flag to pass INT64_MIN/MAX through instead of rescaling, this avoids special cases for AV_NOPTS_VALUE +) + +//////////////////////////////////////////////////////////////////////////////// +// JSON OUTPUT + +func (ctx *AVClass) MarshalJSON() ([]byte, error) { + return json.Marshal(jsonAVClass{ + ClassName: C.GoString(ctx.class_name), + }) +} + +func (ctx *AVDictionary) MarshalJSON() ([]byte, error) { + return json.Marshal(jsonAVDictionary{ + Count: AVUtil_dict_count(ctx), + Elems: AVUtil_dict_entries(ctx), + }) +} + +func (ctx *AVDictionaryEntry) MarshalJSON() ([]byte, error) { + return json.Marshal(jsonAVDictionaryEntry{ + Key: ctx.Key(), + Value: ctx.Value(), + }) +} + +//////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (ctx *AVClass) String() string { + if str, err := json.MarshalIndent(ctx, "", " "); err != nil { + return err.Error() + } else { + return string(str) + } +} + +func (ctx *AVDictionary) String() string { + if str, err := json.MarshalIndent(ctx, "", " "); err != nil { + return err.Error() + } else { + return string(str) + } +} diff --git a/sys/ffmpeg61/avutil_channel_layout.go b/sys/ffmpeg61/avutil_channel_layout.go new file mode 100644 index 0000000..e97d0e6 --- /dev/null +++ b/sys/ffmpeg61/avutil_channel_layout.go @@ -0,0 +1,130 @@ +package ffmpeg + +import ( + "encoding/json" + "unsafe" +) + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavutil +#include +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// GLOBALS + +const ( + cBufSize = 32 +) + +var ( + cBuf [cBufSize]C.char +) + +//////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (ch AVChannelLayout) MarshalJSON() ([]byte, error) { + if ch.NumChannels() == 0 { + return json.Marshal(nil) + } else if str, err := AVUtil_channel_layout_describe(&ch); err != nil { + return nil, err + } else { + return json.Marshal(str) + } +} + +//////////////////////////////////////////////////////////////////////////////// +// BINDINGS + +// Get the name of a given channel. +func AVUtil_channel_name(channel AVChannel) (string, error) { + if n := C.av_channel_name(&cBuf[0], cBufSize, C.enum_AVChannel(channel)); n < 0 { + return "", AVError(n) + } else { + return C.GoString(&cBuf[0]), nil + } +} + +// Get a human readable string describing a given channel. +func AVUtil_channel_description(channel AVChannel) (string, error) { + if n := C.av_channel_description(&cBuf[0], cBufSize, C.enum_AVChannel(channel)); n < 0 { + return "", AVError(n) + } else { + return C.GoString(&cBuf[0]), nil + } +} + +// This is the inverse function of av_channel_name. +func AVUtil_channel_from_string(name string) AVChannel { + cName := C.CString(name) + defer C.free(unsafe.Pointer(cName)) + return AVChannel(C.av_channel_from_string(cName)) +} + +// Iterate over all standard channel layouts. +func AVUtil_channel_layout_standard(iterator *uintptr) *AVChannelLayout { + return (*AVChannelLayout)(C.av_channel_layout_standard((*unsafe.Pointer)(unsafe.Pointer(iterator)))) +} + +// Iterate over all standard channel layouts. +func AVUtil_channel_layout_describe(channel_layout *AVChannelLayout) (string, error) { + if n := C.av_channel_layout_describe((*C.struct_AVChannelLayout)(channel_layout), &cBuf[0], cBufSize); n < 0 { + return "", AVError(n) + } else { + return C.GoString(&cBuf[0]), nil + } +} + +// Get the default channel layout for a given number of channels. +func AVUtil_channel_layout_default(ch_layout *AVChannelLayout, nb_channels int) { + C.av_channel_layout_default((*C.struct_AVChannelLayout)(ch_layout), C.int(nb_channels)) +} + +// Return channel layout from a description +func AVUtil_channel_layout_from_string(ch_layout *AVChannelLayout, str string) error { + cStr := C.CString(str) + defer C.free(unsafe.Pointer(cStr)) + if err := AVError(C.av_channel_layout_from_string((*C.struct_AVChannelLayout)(ch_layout), cStr)); err < 0 { + return err + } else { + return nil + } +} + +// Free any allocated data in the channel layout and reset the channel count to 0. +func AVUtil_channel_layout_uninit(ch_layout *AVChannelLayout) { + C.av_channel_layout_uninit((*C.struct_AVChannelLayout)(ch_layout)) +} + +// Get the channel with the given index in a channel layout. +func AVUtil_channel_layout_channel_from_index(ch_layout *AVChannelLayout, index int) AVChannel { + return AVChannel(C.av_channel_layout_channel_from_index((*C.struct_AVChannelLayout)(ch_layout), C.uint(index))) +} + +// Get the index of a given channel in a channel layout. +func AVUtil_channel_layout_index_from_channel(ch_layout *AVChannelLayout, channel AVChannel) int { + return int(C.av_channel_layout_index_from_channel((*C.struct_AVChannelLayout)(ch_layout), C.enum_AVChannel(channel))) +} + +// Return number of channels +func AVUtil_get_channel_layout_nb_channels(ch_layout *AVChannelLayout) int { + return int((*C.struct_AVChannelLayout)(ch_layout).nb_channels) +} + +// Check whether a channel layout is valid +func AVUtil_channel_layout_check(ch_layout *AVChannelLayout) bool { + return C.av_channel_layout_check((*C.struct_AVChannelLayout)(ch_layout)) != 0 +} + +//////////////////////////////////////////////////////////////////////////////// +// PROPERTIES + +func (ctx AVChannelLayout) NumChannels() int { + return int(ctx.nb_channels) +} diff --git a/sys/ffmpeg61/avutil_channel_layout_test.go b/sys/ffmpeg61/avutil_channel_layout_test.go new file mode 100644 index 0000000..16ed928 --- /dev/null +++ b/sys/ffmpeg61/avutil_channel_layout_test.go @@ -0,0 +1,25 @@ +package ffmpeg_test + +import ( + "testing" + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" + "github.com/stretchr/testify/assert" +) + +func Test_avutil_channel_layout_001(t *testing.T) { + assert := assert.New(t) + var iter uintptr + for { + layout := AVUtil_channel_layout_standard(&iter) + if layout == nil { + break + } + description, err := AVUtil_channel_layout_describe(layout) + assert.NoError(err) + + t.Logf("AVChannelLayout: %q", description) + t.Log(" .channels: ", AVUtil_get_channel_layout_nb_channels(layout)) + } +} diff --git a/sys/ffmpeg61/avutil_dict.go b/sys/ffmpeg61/avutil_dict.go new file mode 100644 index 0000000..7524081 --- /dev/null +++ b/sys/ffmpeg61/avutil_dict.go @@ -0,0 +1,124 @@ +package ffmpeg + +import ( + "unsafe" +) + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavutil +#include +#include +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Allocate a dictionary +func AVUtil_dict_alloc() *AVDictionary { + return new(AVDictionary) +} + +// Free a dictionary and all entries in the dictionary. +func AVUtil_dict_free(dict *AVDictionary) { + if dict == nil { + return + } + C.av_dict_free(&dict.ctx) +} + +// Copy entries from one dictionary into another. +func AVUtil_dict_copy(dict *AVDictionary, flags AVDictionaryFlag) (*AVDictionary, error) { + if dict == nil { + return nil, nil + } + dest := new(AVDictionary) + if err := AVError(C.av_dict_copy(&dest.ctx, dict.ctx, C.int(flags))); err != 0 { + return nil, err + } + + // Return success + return dest, nil +} + +// Get the number of entries in the dictionary. +func AVUtil_dict_count(dict *AVDictionary) int { + if dict == nil { + return 0 + } + return int(C.av_dict_count(dict.ctx)) +} + +// Set the given entry, overwriting an existing entry. +func AVUtil_dict_set(dict *AVDictionary, key, value string, flags AVDictionaryFlag) error { + cKey, cValue := C.CString(key), C.CString(value) + defer C.free(unsafe.Pointer(cKey)) + defer C.free(unsafe.Pointer(cValue)) + if err := AVError(C.av_dict_set(&dict.ctx, cKey, cValue, C.int(flags))); err != 0 { + return err + } + return nil +} + +// Delete the given entry. If dictionary becomes empty, the return value is nil +func AVUtil_dict_delete(dict *AVDictionary, key string) (*AVDictionary, error) { + if dict == nil { + return dict, nil + } + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + if err := AVError(C.av_dict_set(&dict.ctx, cKey, nil, 0)); err != 0 { + return dict, err + } else { + return dict, nil + } +} + +// Get a dictionary entry with matching key. +func AVUtil_dict_get(dict *AVDictionary, key string, prev *AVDictionaryEntry, flags AVDictionaryFlag) *AVDictionaryEntry { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + return (*AVDictionaryEntry)(C.av_dict_get(dict.ctx, cKey, (*C.struct_AVDictionaryEntry)(prev), C.int(flags))) +} + +// Get the keys for the dictionary. +func AVUtil_dict_keys(dict *AVDictionary) []string { + keys := make([]string, 0, AVUtil_dict_count(dict)) + entry := AVUtil_dict_get(dict, "", nil, AV_DICT_IGNORE_SUFFIX) + for entry != nil { + keys = append(keys, entry.Key()) + entry = AVUtil_dict_get(dict, "", entry, AV_DICT_IGNORE_SUFFIX) + } + return keys +} + +// Get the entries for the dictionary. +func AVUtil_dict_entries(dict *AVDictionary) []*AVDictionaryEntry { + if dict == nil { + return nil + } + result := make([]*AVDictionaryEntry, 0, AVUtil_dict_count(dict)) + entry := AVUtil_dict_get(dict, "", nil, AV_DICT_IGNORE_SUFFIX) + for entry != nil { + result = append(result, entry) + entry = AVUtil_dict_get(dict, "", entry, AV_DICT_IGNORE_SUFFIX) + } + return result +} + +//////////////////////////////////////////////////////////////////////////////// +// DICTIONARY ENTRY + +// Return dictionary entry key +func (e *AVDictionaryEntry) Key() string { + return C.GoString(e.key) +} + +// Return dictionary entry value +func (e *AVDictionaryEntry) Value() string { + return C.GoString(e.value) +} diff --git a/sys/ffmpeg61/avutil_dict_test.go b/sys/ffmpeg61/avutil_dict_test.go new file mode 100644 index 0000000..89b389f --- /dev/null +++ b/sys/ffmpeg61/avutil_dict_test.go @@ -0,0 +1,52 @@ +package ffmpeg_test + +import ( + "fmt" + "testing" + + // Package imports + "github.com/stretchr/testify/assert" + + // Namespace imports + + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func Test_avutil_dict_001(t *testing.T) { + assert := assert.New(t) + + dict := AVUtil_dict_alloc() + if !assert.NotNil(dict) { + t.SkipNow() + } + assert.NoError(AVUtil_dict_set(dict, "a", "b", 0)) + assert.NoError(AVUtil_dict_set(dict, "b", "b", 0)) + + t.Log(dict) + + keys := AVUtil_dict_keys(dict) + assert.Equal(2, len(keys)) + + entries := AVUtil_dict_entries(dict) + assert.Equal(2, len(entries)) + + AVUtil_dict_free(dict) +} + +func Test_avutil_dict_002(t *testing.T) { + assert := assert.New(t) + + dict := AVUtil_dict_alloc() + if !assert.NotNil(dict) { + t.SkipNow() + } + defer AVUtil_dict_free(dict) + + for i := 0; i < 10000; i++ { + key := fmt.Sprintf("key_%d", i) + value := fmt.Sprintf("value_%d", i) + assert.NoError(AVUtil_dict_set(dict, key, value, 0)) + } + + t.Log(dict) +} diff --git a/sys/ffmpeg61/avutil_error.go b/sys/ffmpeg61/avutil_error.go new file mode 100644 index 0000000..1e806b9 --- /dev/null +++ b/sys/ffmpeg61/avutil_error.go @@ -0,0 +1,82 @@ +package ffmpeg + +import ( + "bytes" + "fmt" + "syscall" + "unsafe" +) + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavutil +#include + +static int av_error_matches(int av,int en) { + return av == AVERROR(en); +} +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// CONSTANTS + +const ( + errBufferSize = C.AV_ERROR_MAX_STRING_SIZE +) + +const ( + AVERROR_BSF_NOT_FOUND = C.AVERROR_BSF_NOT_FOUND ///< Bitstream filter not found + AVERROR_BUG = C.AVERROR_BUG ///< Internal bug, also see AVERROR_BUG2 + AVERROR_BUFFER_TOO_SMALL = C.AVERROR_BUFFER_TOO_SMALL ///< Buffer too small + AVERROR_DECODER_NOT_FOUND = C.AVERROR_DECODER_NOT_FOUND ///< Decoder not found + AVERROR_DEMUXER_NOT_FOUND = C.AVERROR_DEMUXER_NOT_FOUND ///< Demuxer not found + AVERROR_ENCODER_NOT_FOUND = C.AVERROR_ENCODER_NOT_FOUND ///< Encoder not found + AVERROR_EOF = C.AVERROR_EOF ///< End of file + AVERROR_EXIT = C.AVERROR_EXIT ///< Immediate exit was requested; the called function should not be restarted + AVERROR_EXTERNAL = C.AVERROR_EXTERNAL ///< Generic error in an external library + AVERROR_FILTER_NOT_FOUND = C.AVERROR_FILTER_NOT_FOUND ///< Filter not found + AVERROR_INVALIDDATA = C.AVERROR_INVALIDDATA ///< Invalid data found when processing input + AVERROR_MUXER_NOT_FOUND = C.AVERROR_MUXER_NOT_FOUND ///< Muxer not found + AVERROR_OPTION_NOT_FOUND = C.AVERROR_OPTION_NOT_FOUND ///< Option not found + AVERROR_PATCHWELCOME = C.AVERROR_PATCHWELCOME ///< Not yet implemented in FFmpeg, patches welcome + AVERROR_PROTOCOL_NOT_FOUND = C.AVERROR_PROTOCOL_NOT_FOUND ///< Protocol not found + AVERROR_STREAM_NOT_FOUND = C.AVERROR_STREAM_NOT_FOUND ///< Stream not found + AVERROR_BUG2 = C.AVERROR_BUG2 // This is semantically identical to AVERROR_BUG, it has been introduced in Libav after our AVERROR_BUG and with a modified value + AVERROR_UNKNOWN = C.AVERROR_UNKNOWN ///< Unknown error, typically from an external library + AVERROR_EXPERIMENTAL = C.AVERROR_EXPERIMENTAL ///< Requested feature is flagged experimental. Set strict_std_compliance if you really want to use it. + AVERROR_INPUT_CHANGED = C.AVERROR_INPUT_CHANGED ///< Input changed between calls. Reconfiguration is required. (can be OR-ed with AVERROR_OUTPUT_CHANGED) + AVERROR_OUTPUT_CHANGED = C.AVERROR_OUTPUT_CHANGED ///< Output changed between calls. Reconfiguration is required. (can be OR-ed with AVERROR_INPUT_CHANGED) + AVERROR_HTTP_BAD_REQUEST = C.AVERROR_HTTP_BAD_REQUEST // HTTP & RTSP errors + AVERROR_HTTP_UNAUTHORIZED = C.AVERROR_HTTP_UNAUTHORIZED // HTTP & RTSP errors + AVERROR_HTTP_FORBIDDEN = C.AVERROR_HTTP_FORBIDDEN // HTTP & RTSP errors + AVERROR_HTTP_NOT_FOUND = C.AVERROR_HTTP_NOT_FOUND // HTTP & RTSP errors + AVERROR_HTTP_OTHER_4XX = C.AVERROR_HTTP_OTHER_4XX // HTTP & RTSP errors + AVERROR_HTTP_SERVER_ERROR = C.AVERROR_HTTP_SERVER_ERROR // HTTP & RTSP errors +) + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +func (err AVError) Error() string { + if err == 0 { + return "" + } + cBuffer := make([]byte, errBufferSize) + if err := C.av_strerror(C.int(err), (*C.char)(unsafe.Pointer(&cBuffer[0])), errBufferSize); err == 0 { + if n := bytes.IndexByte(cBuffer, 0); n >= 0 { + return string(cBuffer[:n]) + } else { + return string(cBuffer) + } + } else { + return fmt.Sprintf("Error code: %v", int(err)) + } +} + +func (err AVError) IsErrno(v syscall.Errno) bool { + c := int(C.av_error_matches(C.int(err), C.int(v))) + return c == 1 +} diff --git a/sys/ffmpeg61/avutil_frame.go b/sys/ffmpeg61/avutil_frame.go new file mode 100644 index 0000000..185a45d --- /dev/null +++ b/sys/ffmpeg61/avutil_frame.go @@ -0,0 +1,241 @@ +package ffmpeg + +import ( + "encoding/json" + "unsafe" +) + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavutil +#include +#include +#include +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +type jsonAVFrame struct { + SampleFormat AVSampleFormat `json:"sample_format,omitempty"` + NumSamples int `json:"num_samples,omitempty"` + SampleRate int `json:"sample_rate,omitempty"` + ChannelLayout AVChannelLayout `json:"channel_layout,omitempty"` + PixelFormat AVPixelFormat `json:"pixel_format"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` + PictureType AVPictureType `json:"picture_type,omitempty"` + Pts AVTimestamp `json:"pts,omitempty"` + BestEffortTs AVTimestamp `json:"best_effort_timestamp,omitempty"` + TimeBase AVRational `json:"time_base,omitempty"` + NumPlanes int `json:"num_planes,omitempty"` +} + +func (ctx *AVFrame) MarshalJSON() ([]byte, error) { + if ctx.nb_samples > 0 { + // Audio + return json.Marshal(jsonAVFrame{ + NumSamples: int(ctx.nb_samples), + SampleFormat: AVSampleFormat(ctx.format), + SampleRate: int(ctx.sample_rate), + ChannelLayout: AVChannelLayout(ctx.ch_layout), + Pts: AVTimestamp(ctx.pts), + BestEffortTs: AVTimestamp(ctx.best_effort_timestamp), + TimeBase: AVRational(ctx.time_base), + NumPlanes: AVUtil_frame_get_num_planes(ctx), + }) + } else { + // Video + return json.Marshal(jsonAVFrame{ + PixelFormat: AVPixelFormat(ctx.format), + Width: int(ctx.width), + Height: int(ctx.height), + Pts: AVTimestamp(ctx.pts), + TimeBase: AVRational(ctx.time_base), + NumPlanes: AVUtil_frame_get_num_planes(ctx), + }) + } +} + +func (ctx *AVFrame) String() string { + data, _ := json.MarshalIndent(ctx, "", " ") + return string(data) +} + +//////////////////////////////////////////////////////////////////////////////// +// BINDINGS + +// Allocate an AVFrame and set its fields to default values. +func AVUtil_frame_alloc() *AVFrame { + return (*AVFrame)(C.av_frame_alloc()) +} + +// Free the frame and any dynamically allocated objects in it +func AVUtil_frame_free(frame *AVFrame) { + C.av_frame_free((**C.AVFrame)(unsafe.Pointer(&frame))) +} + +// Unreference all the buffers referenced by frame and reset the frame fields. +func AVUtil_frame_unref(frame *AVFrame) { + C.av_frame_unref((*C.AVFrame)(frame)) +} + +// Allocate new buffer(s) for audio or video data. +// The following fields must be set on frame before calling this function: +// format, width and height for video, +// format, nb_samples and ch_layout for audio +func AVUtil_frame_get_buffer(frame *AVFrame, align bool) error { + if ret := AVError(C.av_frame_get_buffer((*C.struct_AVFrame)(frame), boolToInt(align))); ret != 0 { + return ret + } + return nil +} + +// Ensure that the frame data is writable, avoiding data copy if possible. +// Do nothing if the frame is writable, allocate new buffers and copy the data if it is not. +// Non-refcounted frames behave as non-writable, i.e. a copy is always made. +func AVUtil_frame_make_writable(frame *AVFrame) error { + if ret := AVError(C.av_frame_make_writable((*C.struct_AVFrame)(frame))); ret != 0 { + return ret + } + return nil +} + +// Return the number of planes in the frame data. +func AVUtil_frame_get_num_planes(frame *AVFrame) int { + if frame.nb_samples > 0 { + // Audio + if AVUtil_sample_fmt_is_planar(AVSampleFormat(frame.format)) { + return int(frame.ch_layout.nb_channels) + } else { + return 1 + } + } else { + // Video + desc := AVUtil_get_pix_fmt_desc(AVPixelFormat(frame.format)) + return int(desc.nb_components) + } +} + +// Copy only "metadata" fields from src to dst, those fields that do not affect the data layout in the buffers. +// E.g. pts, sample rate (for audio) or sample aspect ratio (for video), but not width/height or channel layout. +// Side data is also copied. +func AVUtil_frame_copy_props(dst, src *AVFrame) error { + if ret := AVError(C.av_frame_copy_props((*C.struct_AVFrame)(dst), (*C.struct_AVFrame)(src))); ret != 0 { + return ret + } + return nil +} + +//////////////////////////////////////////////////////////////////////////////// +// PROPERTIES + +func (ctx *AVFrame) NumSamples() int { + return int(ctx.nb_samples) +} + +func (ctx *AVFrame) SetNumSamples(nb_samples int) { + ctx.nb_samples = C.int(nb_samples) +} + +func (ctx *AVFrame) SampleFormat() AVSampleFormat { + return AVSampleFormat(ctx.format) +} + +func (ctx *AVFrame) SetSampleFormat(format AVSampleFormat) { + ctx.format = C.int(format) +} + +func (ctx *AVFrame) SampleRate() int { + return int(ctx.sample_rate) +} + +func (ctx *AVFrame) SetSampleRate(sample_rate int) { + ctx.sample_rate = C.int(sample_rate) +} + +func (ctx *AVFrame) ChannelLayout() AVChannelLayout { + return AVChannelLayout(ctx.ch_layout) +} + +func (ctx *AVFrame) SetChannelLayout(src AVChannelLayout) error { + if ret := AVError(C.av_channel_layout_copy((*C.struct_AVChannelLayout)(&ctx.ch_layout), (*C.struct_AVChannelLayout)(&src))); ret != 0 { + return ret + } + return nil +} + +func (ctx *AVFrame) Width() int { + return int(ctx.width) +} + +func (ctx *AVFrame) SetWidth(width int) { + ctx.width = C.int(width) +} + +func (ctx *AVFrame) Height() int { + return int(ctx.height) +} + +func (ctx *AVFrame) SetHeight(height int) { + ctx.height = C.int(height) +} + +func (ctx *AVFrame) PixFmt() AVPixelFormat { + return AVPixelFormat(ctx.format) +} + +func (ctx *AVFrame) SetPixFmt(format AVPixelFormat) { + ctx.format = C.int(format) +} + +func (ctx *AVFrame) Pts() int64 { + return int64(ctx.pts) +} + +func (ctx *AVFrame) BestEffortTs() int64 { + return int64(ctx.best_effort_timestamp) +} + +func (ctx *AVFrame) SetPts(pts int64) { + ctx.pts = C.int64_t(pts) +} + +func (ctx *AVFrame) TimeBase() AVRational { + return AVRational(ctx.time_base) +} + +// Return stride of a plane for images, or plane size for audio. +func (ctx *AVFrame) Linesize(plane int) int { + if plane < 0 || plane >= int(C.AV_NUM_DATA_POINTERS) { + return 0 + } + return int(ctx.linesize[plane]) +} + +// Return a buffer reference to the data for a plane. +func (ctx *AVFrame) bufferRef(plane int) *AVBufferRef { + return (*AVBufferRef)(C.av_frame_get_plane_buffer((*C.AVFrame)(ctx), C.int(plane))) +} + +// Returns a plane as a uint8 array. +func (ctx *AVFrame) Uint8(plane int) []uint8 { + if buf := ctx.bufferRef(plane); buf == nil { + return nil + } else { + return cUint8Slice(unsafe.Pointer(buf.data), C.int(buf.size)) + } +} + +// Returns a plane as a int16 array. +func (ctx *AVFrame) Int16(plane int) []int16 { + if buf := ctx.bufferRef(plane); buf == nil { + return nil + } else { + return cInt16Slice(unsafe.Pointer(buf.data), C.int(buf.size)>>1) + } +} diff --git a/sys/ffmpeg61/avutil_frame_test.go b/sys/ffmpeg61/avutil_frame_test.go new file mode 100644 index 0000000..d4f7048 --- /dev/null +++ b/sys/ffmpeg61/avutil_frame_test.go @@ -0,0 +1,21 @@ +package ffmpeg_test + +import ( + "testing" + + // Packages + "github.com/stretchr/testify/assert" + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func Test_avutil_frame_000(t *testing.T) { + assert := assert.New(t) + + frame := AVUtil_frame_alloc() + if !assert.NotNil(frame) { + t.SkipNow() + } + AVUtil_frame_free(frame) +} diff --git a/sys/ffmpeg61/avutil_image.go b/sys/ffmpeg61/avutil_image.go new file mode 100644 index 0000000..bf68fd4 --- /dev/null +++ b/sys/ffmpeg61/avutil_image.go @@ -0,0 +1,102 @@ +package ffmpeg + +import ( + "unsafe" +) + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavutil +#include +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +func AVUtil_image_linesizes(pixfmt AVPixelFormat, width int) [4]C.int { + var strides [4]C.int + C.av_image_fill_linesizes(&strides[0], C.enum_AVPixelFormat(pixfmt), C.int(width)) + return strides +} + +// Fill plane sizes for an image with pixel format pix_fmt and height height. +func AVUtil_image_plane_sizes(pixfmt AVPixelFormat, height int, strides [4]C.int) ([4]C.size_t, error) { + var planes [4]C.size_t + var strides_ [4]C.ptrdiff_t + for i := 0; i < 4; i++ { + strides_[i] = C.ptrdiff_t(strides[i]) + } + if ret := C.av_image_fill_plane_sizes(&planes[0], C.enum_AVPixelFormat(pixfmt), C.int(height), &strides_[0]); ret < 0 { + return [4]C.size_t{}, AVError(ret) + } else { + return planes, nil + } +} + +// Fill plane sizes for an image with pixel format pix_fmt and height height. +func AVUtil_image_plane_sizes_ex(width, height int, pixfmt AVPixelFormat) ([4]C.size_t, error) { + return AVUtil_image_plane_sizes(pixfmt, height, AVUtil_image_linesizes(pixfmt, width)) +} + +// Allocate an image buffer with size, pixel format and alignment suitable for the image +// The allocated image buffer has to be freed by using AVUtil_image_free +// The return values are the allocated data pointers, the strides and the size of the allocated data +func AVUtil_image_alloc(width, height int, pixfmt AVPixelFormat, align int) ([][]byte, []int, int, error) { + var data [4]*C.uint8_t + var stride [4]C.int + if ret := C.av_image_alloc(&data[0], &stride[0], C.int(width), C.int(height), C.enum_AVPixelFormat(pixfmt), C.int(align)); ret < 0 { + return nil, nil, 0, AVError(ret) + } else if planeSizes, err := AVUtil_image_plane_sizes_ex(width, height, pixfmt); err != nil { + return nil, nil, 0, err + } else { + dataSlice := make([][]byte, 4) + strideSlice := make([]int, 4) + for i := 0; i < 4; i++ { + if data[i] != nil { + dataSlice[i] = cByteSlice(unsafe.Pointer(data[i]), C.int(planeSizes[i])) + } + strideSlice[i] = int(stride[i]) + } + return dataSlice, strideSlice, int(ret), nil + } +} + +// Free an image buffer allocated by AVUtil_image_alloc +func AVUtil_image_free(data [][]byte) { + ptrs, _ := avutil_image_ptr(data, nil) + C.av_free(unsafe.Pointer(ptrs[0])) +} + +// Copy image in src into dst +func AVUtil_image_copy(dst [][]byte, dst_stride []int, src [][]byte, src_stride []int, pixfmt AVPixelFormat, width, height int) { + dst_ptrs, dst_strides := avutil_image_ptr(dst, dst_stride) + src_ptrs, src_strides := avutil_image_ptr(src, src_stride) + C.av_image_copy(&dst_ptrs[0], &dst_strides[0], &src_ptrs[0], &src_strides[0], C.enum_AVPixelFormat(pixfmt), C.int(width), C.int(height)) +} + +// Return the image as a byte buffer +func AVUtil_image_bytes(data [][]byte, size int) []byte { + ptrs, _ := avutil_image_ptr(data, nil) + return cByteSlice(unsafe.Pointer(ptrs[0]), C.int(size)) +} + +// Convert [][]byte to a [4]*C.uint8_t +func avutil_image_ptr(data [][]byte, stride []int) ([4]*C.uint8_t, [4]C.int) { + var ptrs [4]*C.uint8_t + var strides [4]C.int + for i := 0; i < 4; i++ { + if len(data[i]) == 0 { + ptrs[i] = nil + } else { + ptrs[i] = (*C.uint8_t)(unsafe.Pointer(&data[i][0])) + } + if len(stride) > 0 { + strides[i] = C.int(stride[i]) + } + } + return ptrs, strides +} diff --git a/sys/ffmpeg61/avutil_image_test.go b/sys/ffmpeg61/avutil_image_test.go new file mode 100644 index 0000000..a06d9f0 --- /dev/null +++ b/sys/ffmpeg61/avutil_image_test.go @@ -0,0 +1,39 @@ +package ffmpeg_test + +import ( + "testing" + + // Package imports + "github.com/stretchr/testify/assert" + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func Test_avutil_image_000(t *testing.T) { + assert := assert.New(t) + + data, linesize, bufsize, err := AVUtil_image_alloc(320, 240, AV_PIX_FMT_YUV420P, 16) + if !assert.NoError(err) { + t.Fatal(err) + } + assert.NotNil(data) + assert.NotNil(linesize) + assert.NotZero(bufsize) + + t.Log("data=", data) + t.Log("linesize=", linesize) + t.Log("bufsize=", bufsize) + + AVUtil_image_free(data) +} + +func Test_avutil_image_001(t *testing.T) { + assert := assert.New(t) + + sizes, err := AVUtil_image_plane_sizes_ex(320, 240, AV_PIX_FMT_YUV420P) + if !assert.NoError(err) { + t.Fatal(err) + } + t.Log(sizes) +} diff --git a/sys/ffmpeg61/avutil_math.go b/sys/ffmpeg61/avutil_math.go new file mode 100644 index 0000000..cfae419 --- /dev/null +++ b/sys/ffmpeg61/avutil_math.go @@ -0,0 +1,22 @@ +package ffmpeg + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavutil +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// + +// Compare two timestamps each in its own time base. Returns -1 if a is before b, 1 if a is after b, or 0 if they are equal. +func AVUtil_compare_ts(a int64, a_tb AVRational, b int64, b_tb AVRational) int { + return int(C.av_compare_ts(C.int64_t(a), C.AVRational(a_tb), C.int64_t(b), C.AVRational(b_tb))) +} + +// Rescale a value from one range to another. +func AVUtil_rescale_rnd(a, b, c int64, rnd AVRounding) int64 { + return int64(C.av_rescale_rnd(C.int64_t(a), C.int64_t(b), C.int64_t(c), C.enum_AVRounding(rnd))) +} diff --git a/sys/ffmpeg61/avutil_mediatype.go b/sys/ffmpeg61/avutil_mediatype.go new file mode 100644 index 0000000..63c7ea4 --- /dev/null +++ b/sys/ffmpeg61/avutil_mediatype.go @@ -0,0 +1,50 @@ +package ffmpeg + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavutil +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// CONSTANTS + +const ( + AVMEDIA_TYPE_UNKNOWN AVMediaType = C.AVMEDIA_TYPE_UNKNOWN ///< Usually treated as AVMEDIA_TYPE_DATA + AVMEDIA_TYPE_VIDEO AVMediaType = C.AVMEDIA_TYPE_VIDEO + AVMEDIA_TYPE_AUDIO AVMediaType = C.AVMEDIA_TYPE_AUDIO + AVMEDIA_TYPE_DATA AVMediaType = C.AVMEDIA_TYPE_DATA ///< Opaque data information usually continuous + AVMEDIA_TYPE_SUBTITLE AVMediaType = C.AVMEDIA_TYPE_SUBTITLE + AVMEDIA_TYPE_ATTACHMENT AVMediaType = C.AVMEDIA_TYPE_ATTACHMENT ///< Opaque data information usually sparse +) + +//////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (v AVMediaType) String() string { + switch v { + case AVMEDIA_TYPE_UNKNOWN: + return "AVMEDIA_TYPE_UNKNOWN" + case AVMEDIA_TYPE_VIDEO: + return "AVMEDIA_TYPE_VIDEO" + case AVMEDIA_TYPE_AUDIO: + return "AVMEDIA_TYPE_AUDIO" + case AVMEDIA_TYPE_DATA: + return "AVMEDIA_TYPE_DATA" + case AVMEDIA_TYPE_SUBTITLE: + return "AVMEDIA_TYPE_SUBTITLE" + case AVMEDIA_TYPE_ATTACHMENT: + return "AVMEDIA_TYPE_ATTACHMENT" + } + return "[AVMediaType]" +} + +//////////////////////////////////////////////////////////////////////////////// +// PROPERTIES + +func (m AVMediaType) Is(v AVMediaType) bool { + return v == m +} diff --git a/sys/ffmpeg61/avutil_parse.go b/sys/ffmpeg61/avutil_parse.go new file mode 100644 index 0000000..a088889 --- /dev/null +++ b/sys/ffmpeg61/avutil_parse.go @@ -0,0 +1,27 @@ +package ffmpeg + +import "unsafe" + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavutil +#include +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Parse size and return the width and height of the detected values. +func AVUtil_parse_video_size(size string) (int, int, error) { + var width, height C.int + var cStr = C.CString(size) + defer C.free(unsafe.Pointer(cStr)) + if ret := AVError(C.av_parse_video_size(&width, &height, cStr)); ret < 0 { + return 0, 0, ret + } + return int(width), int(height), nil +} diff --git a/sys/ffmpeg61/avutil_parse_test.go b/sys/ffmpeg61/avutil_parse_test.go new file mode 100644 index 0000000..accd86f --- /dev/null +++ b/sys/ffmpeg61/avutil_parse_test.go @@ -0,0 +1,21 @@ +package ffmpeg_test + +import ( + "testing" + + // Package imports + "github.com/stretchr/testify/assert" + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func Test_avutil_parse_000(t *testing.T) { + assert := assert.New(t) + x, y, err := AVUtil_parse_video_size("1920x1080") + if !assert.NoError(err) { + t.Fatal(err) + } + assert.Equal(1920, x) + assert.Equal(1080, y) +} diff --git a/sys/ffmpeg61/avutil_picturetype.go b/sys/ffmpeg61/avutil_picturetype.go new file mode 100644 index 0000000..040e761 --- /dev/null +++ b/sys/ffmpeg61/avutil_picturetype.go @@ -0,0 +1,50 @@ +package ffmpeg + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavutil +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// CONSTANTS + +const ( + AV_PICTURE_TYPE_NONE AVPictureType = C.AV_PICTURE_TYPE_NONE ///< Undefined + AV_PICTURE_TYPE_I AVPictureType = C.AV_PICTURE_TYPE_I ///< Intra + AV_PICTURE_TYPE_P AVPictureType = C.AV_PICTURE_TYPE_P ///< Predicted + AV_PICTURE_TYPE_B AVPictureType = C.AV_PICTURE_TYPE_B ///< Bi-dir predicted + AV_PICTURE_TYPE_S AVPictureType = C.AV_PICTURE_TYPE_S ///< S(GMC)-VOP MPEG-4 + AV_PICTURE_TYPE_SI AVPictureType = C.AV_PICTURE_TYPE_SI ///< Switching Intra + AV_PICTURE_TYPE_SP AVPictureType = C.AV_PICTURE_TYPE_SP ///< Switching Predicted + AV_PICTURE_TYPE_BI AVPictureType = C.AV_PICTURE_TYPE_BI ///< BI type +) + +//////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (v AVPictureType) String() string { + switch v { + case AV_PICTURE_TYPE_NONE: + return "AV_PICTURE_TYPE_NONE" + case AV_PICTURE_TYPE_I: + return "AV_PICTURE_TYPE_I" + case AV_PICTURE_TYPE_P: + return "AV_PICTURE_TYPE_P" + case AV_PICTURE_TYPE_B: + return "AV_PICTURE_TYPE_B" + case AV_PICTURE_TYPE_S: + return "AV_PICTURE_TYPE_S" + case AV_PICTURE_TYPE_SI: + return "AV_PICTURE_TYPE_SI" + case AV_PICTURE_TYPE_SP: + return "AV_PICTURE_TYPE_SP" + case AV_PICTURE_TYPE_BI: + return "AV_PICTURE_TYPE_BI" + default: + return "[?? Invalid AVPictureType value]" + } +} diff --git a/sys/ffmpeg61/avutil_pixfmt.go b/sys/ffmpeg61/avutil_pixfmt.go new file mode 100644 index 0000000..30853a9 --- /dev/null +++ b/sys/ffmpeg61/avutil_pixfmt.go @@ -0,0 +1,272 @@ +package ffmpeg + +import ( + "encoding/json" + "fmt" +) + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavutil +#include +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// CONSTANTS + +const ( + AV_PIX_FMT_NONE AVPixelFormat = C.AV_PIX_FMT_NONE + AV_PIX_FMT_YUV420P AVPixelFormat = C.AV_PIX_FMT_YUV420P ///< planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples) + AV_PIX_FMT_YUYV422 AVPixelFormat = C.AV_PIX_FMT_YUYV422 ///< packed YUV 4:2:2, 16bpp, Y0 Cb Y1 Cr + AV_PIX_FMT_RGB24 AVPixelFormat = C.AV_PIX_FMT_RGB24 ///< packed RGB 8:8:8, 24bpp, RGBRGB... + AV_PIX_FMT_BGR24 AVPixelFormat = C.AV_PIX_FMT_BGR24 ///< packed RGB 8:8:8, 24bpp, BGRBGR... + AV_PIX_FMT_YUV422P AVPixelFormat = C.AV_PIX_FMT_YUV422P ///< planar YUV 4:2:2, 16bpp, (1 Cr & Cb sample per 2x1 Y samples) + AV_PIX_FMT_YUV444P AVPixelFormat = C.AV_PIX_FMT_YUV444P ///< planar YUV 4:4:4, 24bpp, (1 Cr & Cb sample per 1x1 Y samples) + AV_PIX_FMT_YUV410P AVPixelFormat = C.AV_PIX_FMT_YUV410P ///< planar YUV 4:1:0, 9bpp, (1 Cr & Cb sample per 4x4 Y samples) + AV_PIX_FMT_YUV411P AVPixelFormat = C.AV_PIX_FMT_YUV411P ///< planar YUV 4:1:1, 12bpp, (1 Cr & Cb sample per 4x1 Y samples) + AV_PIX_FMT_GRAY8 AVPixelFormat = C.AV_PIX_FMT_GRAY8 ///< Y , 8bpp + AV_PIX_FMT_MONOWHITE AVPixelFormat = C.AV_PIX_FMT_MONOWHITE ///< Y , 1bpp, 0 is white, 1 is black, in each byte pixels are ordered from the msb to the lsb + AV_PIX_FMT_MONOBLACK AVPixelFormat = C.AV_PIX_FMT_MONOBLACK ///< Y , 1bpp, 0 is black, 1 is white, in each byte pixels are ordered from the msb to the lsb + AV_PIX_FMT_PAL8 AVPixelFormat = C.AV_PIX_FMT_PAL8 ///< 8 bits with AV_PIX_FMT_RGB32 palette + AV_PIX_FMT_YUVJ420P AVPixelFormat = C.AV_PIX_FMT_YUVJ420P ///< planar YUV 4:2:0, 12bpp, full scale (JPEG), deprecated in favor of AV_PIX_FMT_YUV420P and setting color_range + AV_PIX_FMT_YUVJ422P AVPixelFormat = C.AV_PIX_FMT_YUVJ422P ///< planar YUV 4:2:2, 16bpp, full scale (JPEG), deprecated in favor of AV_PIX_FMT_YUV422P and setting color_range + AV_PIX_FMT_YUVJ444P AVPixelFormat = C.AV_PIX_FMT_YUVJ444P ///< planar YUV 4:4:4, 24bpp, full scale (JPEG), deprecated in favor of AV_PIX_FMT_YUV444P and setting color_range + AV_PIX_FMT_UYVY422 AVPixelFormat = C.AV_PIX_FMT_UYVY422 ///< packed YUV 4:2:2, 16bpp, Cb Y0 Cr Y1 + AV_PIX_FMT_UYYVYY411 AVPixelFormat = C.AV_PIX_FMT_UYYVYY411 ///< packed YUV 4:1:1, 12bpp, Cb Y0 Y1 Cr Y2 Y3 + AV_PIX_FMT_BGR8 AVPixelFormat = C.AV_PIX_FMT_BGR8 ///< packed RGB 3:3:2, 8bpp, (msb)2B 3G 3R(lsb) + AV_PIX_FMT_BGR4 AVPixelFormat = C.AV_PIX_FMT_BGR4 ///< packed RGB 1:2:1 bitstream, 4bpp, (msb)1B 2G 1R(lsb), a byte contains two pixels, the first pixel in the byte is the one composed by the 4 msb bits + AV_PIX_FMT_BGR4_BYTE AVPixelFormat = C.AV_PIX_FMT_BGR4_BYTE ///< packed RGB 1:2:1, 8bpp, (msb)1B 2G 1R(lsb) + AV_PIX_FMT_RGB8 AVPixelFormat = C.AV_PIX_FMT_RGB8 ///< packed RGB 3:3:2, 8bpp, (msb)2R 3G 3B(lsb) + AV_PIX_FMT_RGB4 AVPixelFormat = C.AV_PIX_FMT_RGB4 ///< packed RGB 1:2:1 bitstream, 4bpp, (msb)1R 2G 1B(lsb), a byte contains two pixels, the first pixel in the byte is the one composed by the 4 msb bits + AV_PIX_FMT_RGB4_BYTE AVPixelFormat = C.AV_PIX_FMT_RGB4_BYTE ///< packed RGB 1:2:1, 8bpp, (msb)1R 2G 1B(lsb) + AV_PIX_FMT_NV12 AVPixelFormat = C.AV_PIX_FMT_NV12 ///< planar YUV 4:2:0, 12bpp, 1 plane for Y and 1 plane for the UV components, which are interleaved (first byte U and the following byte V) + AV_PIX_FMT_NV21 AVPixelFormat = C.AV_PIX_FMT_NV21 ///< as above, but U and V bytes are swapped + AV_PIX_FMT_ARGB AVPixelFormat = C.AV_PIX_FMT_ARGB ///< packed ARGB 8:8:8:8, 32bpp, ARGBARGB... + AV_PIX_FMT_RGBA AVPixelFormat = C.AV_PIX_FMT_RGBA ///< packed RGBA 8:8:8:8, 32bpp, RGBARGBA... + AV_PIX_FMT_ABGR AVPixelFormat = C.AV_PIX_FMT_ABGR ///< packed ABGR 8:8:8:8, 32bpp, ABGRABGR... + AV_PIX_FMT_BGRA AVPixelFormat = C.AV_PIX_FMT_BGRA ///< packed BGRA 8:8:8:8, 32bpp, BGRABGRA... + AV_PIX_FMT_GRAY16BE AVPixelFormat = C.AV_PIX_FMT_GRAY16BE ///< Y , 16bpp, big-endian + AV_PIX_FMT_GRAY16LE AVPixelFormat = C.AV_PIX_FMT_GRAY16LE ///< Y , 16bpp, little-endian + AV_PIX_FMT_YUV440P AVPixelFormat = C.AV_PIX_FMT_YUV440P ///< planar YUV 4:4:0 (1 Cr & Cb sample per 1x2 Y samples) + AV_PIX_FMT_YUVJ440P AVPixelFormat = C.AV_PIX_FMT_YUVJ440P ///< planar YUV 4:4:0 full scale (JPEG), deprecated in favor of AV_PIX_FMT_YUV440P and setting color_range + AV_PIX_FMT_YUVA420P AVPixelFormat = C.AV_PIX_FMT_YUVA420P ///< planar YUV 4:2:0, 20bpp, (1 Cr & Cb sample per 2x2 Y & A samples) + AV_PIX_FMT_RGB48BE AVPixelFormat = C.AV_PIX_FMT_RGB48BE ///< packed RGB 16:16:16, 48bpp, 16R, 16G, 16B, the 2-byte value for each R/G/B component is stored as big-endian + AV_PIX_FMT_RGB48LE AVPixelFormat = C.AV_PIX_FMT_RGB48LE ///< packed RGB 16:16:16, 48bpp, 16R, 16G, 16B, the 2-byte value for each R/G/B component is stored as little-endian + AV_PIX_FMT_RGB565BE AVPixelFormat = C.AV_PIX_FMT_RGB565BE ///< packed RGB 5:6:5, 16bpp, (msb) 5R 6G 5B(lsb), big-endian + AV_PIX_FMT_RGB565LE AVPixelFormat = C.AV_PIX_FMT_RGB565LE ///< packed RGB 5:6:5, 16bpp, (msb) 5R 6G 5B(lsb), little-endian + AV_PIX_FMT_RGB555BE AVPixelFormat = C.AV_PIX_FMT_RGB555BE ///< packed RGB 5:5:5, 16bpp, (msb)1X 5R 5G 5B(lsb), big-endian , X=unused/undefined + AV_PIX_FMT_RGB555LE AVPixelFormat = C.AV_PIX_FMT_RGB555LE ///< packed RGB 5:5:5, 16bpp, (msb)1X 5R 5G 5B(lsb), little-endian, X=unused/undefined + AV_PIX_FMT_BGR565BE AVPixelFormat = C.AV_PIX_FMT_BGR565BE ///< packed BGR 5:6:5, 16bpp, (msb) 5B 6G 5R(lsb), big-endian + AV_PIX_FMT_BGR565LE AVPixelFormat = C.AV_PIX_FMT_BGR565LE ///< packed BGR 5:6:5, 16bpp, (msb) 5B 6G 5R(lsb), little-endian + AV_PIX_FMT_BGR555BE AVPixelFormat = C.AV_PIX_FMT_BGR555BE ///< packed BGR 5:5:5, 16bpp, (msb)1X 5B 5G 5R(lsb), big-endian , X=unused/undefined + AV_PIX_FMT_BGR555LE AVPixelFormat = C.AV_PIX_FMT_BGR555LE ///< packed BGR 5:5:5, 16bpp, (msb)1X 5B 5G 5R(lsb), little-endian, X=unused/undefined + AV_PIX_FMT_VAAPI AVPixelFormat = C.AV_PIX_FMT_VAAPI + AV_PIX_FMT_YUV420P16LE AVPixelFormat = C.AV_PIX_FMT_YUV420P16LE ///< planar YUV 4:2:0, 24bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian + AV_PIX_FMT_YUV420P16BE AVPixelFormat = C.AV_PIX_FMT_YUV420P16BE ///< planar YUV 4:2:0, 24bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian + AV_PIX_FMT_YUV422P16LE AVPixelFormat = C.AV_PIX_FMT_YUV422P16LE ///< planar YUV 4:2:2, 32bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian + AV_PIX_FMT_YUV422P16BE AVPixelFormat = C.AV_PIX_FMT_YUV422P16BE ///< planar YUV 4:2:2, 32bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian + AV_PIX_FMT_YUV444P16LE AVPixelFormat = C.AV_PIX_FMT_YUV444P16LE ///< planar YUV 4:4:4, 48bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian + AV_PIX_FMT_YUV444P16BE AVPixelFormat = C.AV_PIX_FMT_YUV444P16BE ///< planar YUV 4:4:4, 48bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian + AV_PIX_FMT_DXVA2_VLD AVPixelFormat = C.AV_PIX_FMT_DXVA2_VLD ///< HW decoding through DXVA2, Picture.data[3] contains a LPDIRECT3DSURFACE9 pointer + AV_PIX_FMT_RGB444LE AVPixelFormat = C.AV_PIX_FMT_RGB444LE ///< packed RGB 4:4:4, 16bpp, (msb)4X 4R 4G 4B(lsb), little-endian, X=unused/undefined + AV_PIX_FMT_RGB444BE AVPixelFormat = C.AV_PIX_FMT_RGB444BE ///< packed RGB 4:4:4, 16bpp, (msb)4X 4R 4G 4B(lsb), big-endian, X=unused/undefined + AV_PIX_FMT_BGR444LE AVPixelFormat = C.AV_PIX_FMT_BGR444LE ///< packed BGR 4:4:4, 16bpp, (msb)4X 4B 4G 4R(lsb), little-endian, X=unused/undefined + AV_PIX_FMT_BGR444BE AVPixelFormat = C.AV_PIX_FMT_BGR444BE ///< packed BGR 4:4:4, 16bpp, (msb)4X 4B 4G 4R(lsb), big-endian, X=unused/undefined + AV_PIX_FMT_YA8 AVPixelFormat = C.AV_PIX_FMT_YA8 ///< 8 bits gray, 8 bits alpha + AV_PIX_FMT_Y400A AVPixelFormat = C.AV_PIX_FMT_Y400A ///< alias for AV_PIX_FMT_YA8 + AV_PIX_FMT_GRAY8A AVPixelFormat = C.AV_PIX_FMT_GRAY8A ///< alias for AV_PIX_FMT_YA8 + AV_PIX_FMT_BGR48BE AVPixelFormat = C.AV_PIX_FMT_BGR48BE ///< packed RGB 16:16:16, 48bpp, 16B, 16G, 16R, the 2-byte value for each R/G/B component is stored as big-endian + AV_PIX_FMT_BGR48LE AVPixelFormat = C.AV_PIX_FMT_BGR48LE ///< packed RGB 16:16:16, 48bpp, 16B, 16G, 16R, the 2-byte value for each R/G/B component is stored as little-endian + AV_PIX_FMT_YUV420P9BE AVPixelFormat = C.AV_PIX_FMT_YUV420P9BE ///< planar YUV 4:2:0, 13.5bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian + AV_PIX_FMT_YUV420P9LE AVPixelFormat = C.AV_PIX_FMT_YUV420P9LE ///< planar YUV 4:2:0, 13.5bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian + AV_PIX_FMT_YUV420P10BE AVPixelFormat = C.AV_PIX_FMT_YUV420P10BE ///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian + AV_PIX_FMT_YUV420P10LE AVPixelFormat = C.AV_PIX_FMT_YUV420P10LE ///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian + AV_PIX_FMT_YUV422P10BE AVPixelFormat = C.AV_PIX_FMT_YUV422P10BE ///< planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian + AV_PIX_FMT_YUV422P10LE AVPixelFormat = C.AV_PIX_FMT_YUV422P10LE ///< planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian + AV_PIX_FMT_YUV444P9BE AVPixelFormat = C.AV_PIX_FMT_YUV444P9BE ///< planar YUV 4:4:4, 27bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian + AV_PIX_FMT_YUV444P9LE AVPixelFormat = C.AV_PIX_FMT_YUV444P9LE ///< planar YUV 4:4:4, 27bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian + AV_PIX_FMT_YUV444P10BE AVPixelFormat = C.AV_PIX_FMT_YUV444P10BE ///< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian + AV_PIX_FMT_YUV444P10LE AVPixelFormat = C.AV_PIX_FMT_YUV444P10LE ///< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian + AV_PIX_FMT_YUV422P9BE AVPixelFormat = C.AV_PIX_FMT_YUV422P9BE ///< planar YUV 4:2:2, 18bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian + AV_PIX_FMT_YUV422P9LE AVPixelFormat = C.AV_PIX_FMT_YUV422P9LE ///< planar YUV 4:2:2, 18bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian + AV_PIX_FMT_GBRP AVPixelFormat = C.AV_PIX_FMT_GBRP ///< planar GBR 4:4:4 24bpp + AV_PIX_FMT_GBR24P AVPixelFormat = C.AV_PIX_FMT_GBR24P // alias for #AV_PIX_FMT_GBRP + AV_PIX_FMT_GBRP9BE AVPixelFormat = C.AV_PIX_FMT_GBRP9BE ///< planar GBR 4:4:4 27bpp, big-endian + AV_PIX_FMT_GBRP9LE AVPixelFormat = C.AV_PIX_FMT_GBRP9LE ///< planar GBR 4:4:4 27bpp, little-endian + AV_PIX_FMT_GBRP10BE AVPixelFormat = C.AV_PIX_FMT_GBRP10BE ///< planar GBR 4:4:4 30bpp, big-endian + AV_PIX_FMT_GBRP10LE AVPixelFormat = C.AV_PIX_FMT_GBRP10LE ///< planar GBR 4:4:4 30bpp, little-endian + AV_PIX_FMT_GBRP16BE AVPixelFormat = C.AV_PIX_FMT_GBRP16BE ///< planar GBR 4:4:4 48bpp, big-endian + AV_PIX_FMT_GBRP16LE AVPixelFormat = C.AV_PIX_FMT_GBRP16LE ///< planar GBR 4:4:4 48bpp, little-endian + AV_PIX_FMT_YUVA422P AVPixelFormat = C.AV_PIX_FMT_YUVA422P ///< planar YUV 4:2:2 24bpp, (1 Cr & Cb sample per 2x1 Y & A samples) + AV_PIX_FMT_YUVA444P AVPixelFormat = C.AV_PIX_FMT_YUVA444P ///< planar YUV 4:4:4 32bpp, (1 Cr & Cb sample per 1x1 Y & A samples) + AV_PIX_FMT_YUVA420P9BE AVPixelFormat = C.AV_PIX_FMT_YUVA420P9BE ///< planar YUV 4:2:0 22.5bpp, (1 Cr & Cb sample per 2x2 Y & A samples), big-endian + AV_PIX_FMT_YUVA420P9LE AVPixelFormat = C.AV_PIX_FMT_YUVA420P9LE ///< planar YUV 4:2:0 22.5bpp, (1 Cr & Cb sample per 2x2 Y & A samples), little-endian + AV_PIX_FMT_YUVA422P9BE AVPixelFormat = C.AV_PIX_FMT_YUVA422P9BE ///< planar YUV 4:2:2 27bpp, (1 Cr & Cb sample per 2x1 Y & A samples), big-endian + AV_PIX_FMT_YUVA422P9LE AVPixelFormat = C.AV_PIX_FMT_YUVA422P9LE ///< planar YUV 4:2:2 27bpp, (1 Cr & Cb sample per 2x1 Y & A samples), little-endian + AV_PIX_FMT_YUVA444P9BE AVPixelFormat = C.AV_PIX_FMT_YUVA444P9BE ///< planar YUV 4:4:4 36bpp, (1 Cr & Cb sample per 1x1 Y & A samples), big-endian + AV_PIX_FMT_YUVA444P9LE AVPixelFormat = C.AV_PIX_FMT_YUVA444P9LE ///< planar YUV 4:4:4 36bpp, (1 Cr & Cb sample per 1x1 Y & A samples), little-endian + AV_PIX_FMT_YUVA420P10BE AVPixelFormat = C.AV_PIX_FMT_YUVA420P10BE ///< planar YUV 4:2:0 25bpp, (1 Cr & Cb sample per 2x2 Y & A samples, big-endian) + AV_PIX_FMT_YUVA420P10LE AVPixelFormat = C.AV_PIX_FMT_YUVA420P10LE ///< planar YUV 4:2:0 25bpp, (1 Cr & Cb sample per 2x2 Y & A samples, little-endian) + AV_PIX_FMT_YUVA422P10BE AVPixelFormat = C.AV_PIX_FMT_YUVA422P10BE ///< planar YUV 4:2:2 30bpp, (1 Cr & Cb sample per 2x1 Y & A samples, big-endian) + AV_PIX_FMT_YUVA422P10LE AVPixelFormat = C.AV_PIX_FMT_YUVA422P10LE ///< planar YUV 4:2:2 30bpp, (1 Cr & Cb sample per 2x1 Y & A samples, little-endian) + AV_PIX_FMT_YUVA444P10BE AVPixelFormat = C.AV_PIX_FMT_YUVA444P10BE ///< planar YUV 4:4:4 40bpp, (1 Cr & Cb sample per 1x1 Y & A samples, big-endian) + AV_PIX_FMT_YUVA444P10LE AVPixelFormat = C.AV_PIX_FMT_YUVA444P10LE ///< planar YUV 4:4:4 40bpp, (1 Cr & Cb sample per 1x1 Y & A samples, little-endian) + AV_PIX_FMT_YUVA420P16BE AVPixelFormat = C.AV_PIX_FMT_YUVA420P16BE ///< planar YUV 4:2:0 40bpp, (1 Cr & Cb sample per 2x2 Y & A samples, big-endian) + AV_PIX_FMT_YUVA420P16LE AVPixelFormat = C.AV_PIX_FMT_YUVA420P16LE ///< planar YUV 4:2:0 40bpp, (1 Cr & Cb sample per 2x2 Y & A samples, little-endian) + AV_PIX_FMT_YUVA422P16BE AVPixelFormat = C.AV_PIX_FMT_YUVA422P16BE ///< planar YUV 4:2:2 48bpp, (1 Cr & Cb sample per 2x1 Y & A samples, big-endian) + AV_PIX_FMT_YUVA422P16LE AVPixelFormat = C.AV_PIX_FMT_YUVA422P16LE ///< planar YUV 4:2:2 48bpp, (1 Cr & Cb sample per 2x1 Y & A samples, little-endian) + AV_PIX_FMT_YUVA444P16BE AVPixelFormat = C.AV_PIX_FMT_YUVA444P16BE ///< planar YUV 4:4:4 64bpp, (1 Cr & Cb sample per 1x1 Y & A samples, big-endian) + AV_PIX_FMT_YUVA444P16LE AVPixelFormat = C.AV_PIX_FMT_YUVA444P16LE ///< planar YUV 4:4:4 64bpp, (1 Cr & Cb sample per 1x1 Y & A samples, little-endian) + AV_PIX_FMT_VDPAU AVPixelFormat = C.AV_PIX_FMT_VDPAU ///< HW acceleration through VDPAU, Picture.data[3] contains a VdpVideoSurface + AV_PIX_FMT_XYZ12LE AVPixelFormat = C.AV_PIX_FMT_XYZ12LE ///< packed XYZ 4:4:4, 36 bpp, (msb) 12X, 12Y, 12Z (lsb), the 2-byte value for each X/Y/Z is stored as little-endian, the 4 lower bits are set to 0 + AV_PIX_FMT_XYZ12BE AVPixelFormat = C.AV_PIX_FMT_XYZ12BE ///< packed XYZ 4:4:4, 36 bpp, (msb) 12X, 12Y, 12Z (lsb), the 2-byte value for each X/Y/Z is stored as big-endian, the 4 lower bits are set to 0 + AV_PIX_FMT_NV16 AVPixelFormat = C.AV_PIX_FMT_NV16 ///< interleaved chroma YUV 4:2:2, 16bpp, (1 Cr & Cb sample per 2x1 Y samples) + AV_PIX_FMT_NV20LE AVPixelFormat = C.AV_PIX_FMT_NV20LE ///< interleaved chroma YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian + AV_PIX_FMT_NV20BE AVPixelFormat = C.AV_PIX_FMT_NV20BE ///< interleaved chroma YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian + AV_PIX_FMT_RGBA64BE AVPixelFormat = C.AV_PIX_FMT_RGBA64BE ///< packed RGBA 16:16:16:16, 64bpp, 16R, 16G, 16B, 16A, the 2-byte value for each R/G/B/A component is stored as big-endian + AV_PIX_FMT_RGBA64LE AVPixelFormat = C.AV_PIX_FMT_RGBA64LE ///< packed RGBA 16:16:16:16, 64bpp, 16R, 16G, 16B, 16A, the 2-byte value for each R/G/B/A component is stored as little-endian + AV_PIX_FMT_BGRA64BE AVPixelFormat = C.AV_PIX_FMT_BGRA64BE ///< packed RGBA 16:16:16:16, 64bpp, 16B, 16G, 16R, 16A, the 2-byte value for each R/G/B/A component is stored as big-endian + AV_PIX_FMT_BGRA64LE AVPixelFormat = C.AV_PIX_FMT_BGRA64LE ///< packed RGBA 16:16:16:16, 64bpp, 16B, 16G, 16R, 16A, the 2-byte value for each R/G/B/A component is stored as little-endian + AV_PIX_FMT_YVYU422 AVPixelFormat = C.AV_PIX_FMT_YVYU422 ///< packed YUV 4:2:2, 16bpp, Y0 Cr Y1 Cb + AV_PIX_FMT_YA16BE AVPixelFormat = C.AV_PIX_FMT_YA16BE ///< 16 bits gray, 16 bits alpha (big-endian) + AV_PIX_FMT_YA16LE AVPixelFormat = C.AV_PIX_FMT_YA16LE ///< 16 bits gray, 16 bits alpha (little-endian) + AV_PIX_FMT_GBRAP AVPixelFormat = C.AV_PIX_FMT_GBRAP ///< planar GBRA 4:4:4:4 32bpp + AV_PIX_FMT_GBRAP16BE AVPixelFormat = C.AV_PIX_FMT_GBRAP16BE ///< planar GBRA 4:4:4:4 64bpp, big-endian + AV_PIX_FMT_GBRAP16LE AVPixelFormat = C.AV_PIX_FMT_GBRAP16LE ///< planar GBRA 4:4:4:4 64bpp, little-endian + AV_PIX_FMT_QSV AVPixelFormat = C.AV_PIX_FMT_QSV + AV_PIX_FMT_MMAL AVPixelFormat = C.AV_PIX_FMT_MMAL + AV_PIX_FMT_D3D11VA_VLD AVPixelFormat = C.AV_PIX_FMT_D3D11VA_VLD ///< HW decoding through Direct3D11 via old API, Picture.data[3] contains a ID3D11VideoDecoderOutputView pointer + AV_PIX_FMT_CUDA AVPixelFormat = C.AV_PIX_FMT_CUDA + AV_PIX_FMT_0RGB AVPixelFormat = C.AV_PIX_FMT_0RGB ///< packed RGB 8:8:8, 32bpp, XRGBXRGB... X=unused/undefined + AV_PIX_FMT_RGB0 AVPixelFormat = C.AV_PIX_FMT_RGB0 ///< packed RGB 8:8:8, 32bpp, RGBXRGBX... X=unused/undefined + AV_PIX_FMT_0BGR AVPixelFormat = C.AV_PIX_FMT_0BGR ///< packed BGR 8:8:8, 32bpp, XBGRXBGR... X=unused/undefined + AV_PIX_FMT_BGR0 AVPixelFormat = C.AV_PIX_FMT_BGR0 ///< packed BGR 8:8:8, 32bpp, BGRXBGRX... X=unused/undefined + AV_PIX_FMT_YUV420P12BE AVPixelFormat = C.AV_PIX_FMT_YUV420P12BE ///< planar YUV 4:2:0,18bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian + AV_PIX_FMT_YUV420P12LE AVPixelFormat = C.AV_PIX_FMT_YUV420P12LE ///< planar YUV 4:2:0,18bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian + AV_PIX_FMT_YUV420P14BE AVPixelFormat = C.AV_PIX_FMT_YUV420P14BE ///< planar YUV 4:2:0,21bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian + AV_PIX_FMT_YUV420P14LE AVPixelFormat = C.AV_PIX_FMT_YUV420P14LE ///< planar YUV 4:2:0,21bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian + AV_PIX_FMT_YUV422P12BE AVPixelFormat = C.AV_PIX_FMT_YUV422P12BE ///< planar YUV 4:2:2,24bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian + AV_PIX_FMT_YUV422P12LE AVPixelFormat = C.AV_PIX_FMT_YUV422P12LE ///< planar YUV 4:2:2,24bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian + AV_PIX_FMT_YUV422P14BE AVPixelFormat = C.AV_PIX_FMT_YUV422P14BE ///< planar YUV 4:2:2,28bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian + AV_PIX_FMT_YUV422P14LE AVPixelFormat = C.AV_PIX_FMT_YUV422P14LE ///< planar YUV 4:2:2,28bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian + AV_PIX_FMT_YUV444P12BE AVPixelFormat = C.AV_PIX_FMT_YUV444P12BE ///< planar YUV 4:4:4,36bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian + AV_PIX_FMT_YUV444P12LE AVPixelFormat = C.AV_PIX_FMT_YUV444P12LE ///< planar YUV 4:4:4,36bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian + AV_PIX_FMT_YUV444P14BE AVPixelFormat = C.AV_PIX_FMT_YUV444P14BE ///< planar YUV 4:4:4,42bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian + AV_PIX_FMT_YUV444P14LE AVPixelFormat = C.AV_PIX_FMT_YUV444P14LE ///< planar YUV 4:4:4,42bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian + AV_PIX_FMT_GBRP12BE AVPixelFormat = C.AV_PIX_FMT_GBRP12BE ///< planar GBR 4:4:4 36bpp, big-endian + AV_PIX_FMT_GBRP12LE AVPixelFormat = C.AV_PIX_FMT_GBRP12LE ///< planar GBR 4:4:4 36bpp, little-endian + AV_PIX_FMT_GBRP14BE AVPixelFormat = C.AV_PIX_FMT_GBRP14BE ///< planar GBR 4:4:4 42bpp, big-endian + AV_PIX_FMT_GBRP14LE AVPixelFormat = C.AV_PIX_FMT_GBRP14LE ///< planar GBR 4:4:4 42bpp, little-endian + AV_PIX_FMT_YUVJ411P AVPixelFormat = C.AV_PIX_FMT_YUVJ411P ///< planar YUV 4:1:1, 12bpp, (1 Cr & Cb sample per 4x1 Y samples) full scale (JPEG), deprecated in favor of AV_PIX_FMT_YUV411P and setting color_range + AV_PIX_FMT_BAYER_BGGR8 AVPixelFormat = C.AV_PIX_FMT_BAYER_BGGR8 ///< bayer, BGBG..(odd line), GRGR..(even line), 8-bit samples + AV_PIX_FMT_BAYER_RGGB8 AVPixelFormat = C.AV_PIX_FMT_BAYER_RGGB8 ///< bayer, RGRG..(odd line), GBGB..(even line), 8-bit samples + AV_PIX_FMT_BAYER_GBRG8 AVPixelFormat = C.AV_PIX_FMT_BAYER_GBRG8 ///< bayer, GBGB..(odd line), RGRG..(even line), 8-bit samples + AV_PIX_FMT_BAYER_GRBG8 AVPixelFormat = C.AV_PIX_FMT_BAYER_GRBG8 ///< bayer, GRGR..(odd line), BGBG..(even line), 8-bit samples + AV_PIX_FMT_BAYER_BGGR16LE AVPixelFormat = C.AV_PIX_FMT_BAYER_BGGR16LE ///< bayer, BGBG..(odd line), GRGR..(even line), 16-bit samples, little-endian + AV_PIX_FMT_BAYER_BGGR16BE AVPixelFormat = C.AV_PIX_FMT_BAYER_BGGR16BE ///< bayer, BGBG..(odd line), GRGR..(even line), 16-bit samples, big-endian + AV_PIX_FMT_BAYER_RGGB16LE AVPixelFormat = C.AV_PIX_FMT_BAYER_RGGB16LE ///< bayer, RGRG..(odd line), GBGB..(even line), 16-bit samples, little-endian + AV_PIX_FMT_BAYER_RGGB16BE AVPixelFormat = C.AV_PIX_FMT_BAYER_RGGB16BE ///< bayer, RGRG..(odd line), GBGB..(even line), 16-bit samples, big-endian + AV_PIX_FMT_BAYER_GBRG16LE AVPixelFormat = C.AV_PIX_FMT_BAYER_GBRG16LE ///< bayer, GBGB..(odd line), RGRG..(even line), 16-bit samples, little-endian + AV_PIX_FMT_BAYER_GBRG16BE AVPixelFormat = C.AV_PIX_FMT_BAYER_GBRG16BE ///< bayer, GBGB..(odd line), RGRG..(even line), 16-bit samples, big-endian + AV_PIX_FMT_BAYER_GRBG16LE AVPixelFormat = C.AV_PIX_FMT_BAYER_GRBG16LE ///< bayer, GRGR..(odd line), BGBG..(even line), 16-bit samples, little-endian + AV_PIX_FMT_BAYER_GRBG16BE AVPixelFormat = C.AV_PIX_FMT_BAYER_GRBG16BE ///< bayer, GRGR..(odd line), BGBG..(even line), 16-bit samples, big-endian + AV_PIX_FMT_YUV440P10LE AVPixelFormat = C.AV_PIX_FMT_YUV440P10LE ///< planar YUV 4:4:0,20bpp, (1 Cr & Cb sample per 1x2 Y samples), little-endian + AV_PIX_FMT_YUV440P10BE AVPixelFormat = C.AV_PIX_FMT_YUV440P10BE ///< planar YUV 4:4:0,20bpp, (1 Cr & Cb sample per 1x2 Y samples), big-endian + AV_PIX_FMT_YUV440P12LE AVPixelFormat = C.AV_PIX_FMT_YUV440P12LE ///< planar YUV 4:4:0,24bpp, (1 Cr & Cb sample per 1x2 Y samples), little-endian + AV_PIX_FMT_YUV440P12BE AVPixelFormat = C.AV_PIX_FMT_YUV440P12BE ///< planar YUV 4:4:0,24bpp, (1 Cr & Cb sample per 1x2 Y samples), big-endian + AV_PIX_FMT_AYUV64LE AVPixelFormat = C.AV_PIX_FMT_AYUV64LE ///< packed AYUV 4:4:4,64bpp (1 Cr & Cb sample per 1x1 Y & A samples), little-endian + AV_PIX_FMT_AYUV64BE AVPixelFormat = C.AV_PIX_FMT_AYUV64BE ///< packed AYUV 4:4:4,64bpp (1 Cr & Cb sample per 1x1 Y & A samples), big-endian + AV_PIX_FMT_VIDEOTOOLBOX AVPixelFormat = C.AV_PIX_FMT_VIDEOTOOLBOX ///< hardware decoding through Videotoolbox + AV_PIX_FMT_P010LE AVPixelFormat = C.AV_PIX_FMT_P010LE ///< like NV12, with 10bpp per component, data in the high bits, zeros in the low bits, little-endian + AV_PIX_FMT_P010BE AVPixelFormat = C.AV_PIX_FMT_P010BE ///< like NV12, with 10bpp per component, data in the high bits, zeros in the low bits, big-endian + AV_PIX_FMT_GBRAP12BE AVPixelFormat = C.AV_PIX_FMT_GBRAP12BE ///< planar GBR 4:4:4:4 48bpp, big-endian + AV_PIX_FMT_GBRAP12LE AVPixelFormat = C.AV_PIX_FMT_GBRAP12LE ///< planar GBR 4:4:4:4 48bpp, little-endian + AV_PIX_FMT_GBRAP10BE AVPixelFormat = C.AV_PIX_FMT_GBRAP10BE ///< planar GBR 4:4:4:4 40bpp, big-endian + AV_PIX_FMT_GBRAP10LE AVPixelFormat = C.AV_PIX_FMT_GBRAP10LE ///< planar GBR 4:4:4:4 40bpp, little-endian + AV_PIX_FMT_MEDIACODEC AVPixelFormat = C.AV_PIX_FMT_MEDIACODEC ///< hardware decoding through MediaCodec + AV_PIX_FMT_GRAY12BE AVPixelFormat = C.AV_PIX_FMT_GRAY12BE ///< Y , 12bpp, big-endian + AV_PIX_FMT_GRAY12LE AVPixelFormat = C.AV_PIX_FMT_GRAY12LE ///< Y , 12bpp, little-endian + AV_PIX_FMT_GRAY10BE AVPixelFormat = C.AV_PIX_FMT_GRAY10BE ///< Y , 10bpp, big-endian + AV_PIX_FMT_GRAY10LE AVPixelFormat = C.AV_PIX_FMT_GRAY10LE ///< Y , 10bpp, little-endian + AV_PIX_FMT_P016LE AVPixelFormat = C.AV_PIX_FMT_P016LE ///< like NV12, with 16bpp per component, little-endian + AV_PIX_FMT_P016BE AVPixelFormat = C.AV_PIX_FMT_P016BE ///< like NV12, with 16bpp per component, big-endian + AV_PIX_FMT_D3D11 AVPixelFormat = C.AV_PIX_FMT_D3D11 + AV_PIX_FMT_GRAY9BE AVPixelFormat = C.AV_PIX_FMT_GRAY9BE ///< Y , 9bpp, big-endian + AV_PIX_FMT_GRAY9LE AVPixelFormat = C.AV_PIX_FMT_GRAY9LE ///< Y , 9bpp, little-endian + AV_PIX_FMT_GBRPF32BE AVPixelFormat = C.AV_PIX_FMT_GBRPF32BE ///< IEEE-754 single precision planar GBR 4:4:4, 96bpp, big-endian + AV_PIX_FMT_GBRPF32LE AVPixelFormat = C.AV_PIX_FMT_GBRPF32LE ///< IEEE-754 single precision planar GBR 4:4:4, 96bpp, little-endian + AV_PIX_FMT_GBRAPF32BE AVPixelFormat = C.AV_PIX_FMT_GBRAPF32BE ///< IEEE-754 single precision planar GBRA 4:4:4:4, 128bpp, big-endian + AV_PIX_FMT_GBRAPF32LE AVPixelFormat = C.AV_PIX_FMT_GBRAPF32LE ///< IEEE-754 single precision planar GBRA 4:4:4:4, 128bpp, little-endian + AV_PIX_FMT_DRM_PRIME AVPixelFormat = C.AV_PIX_FMT_DRM_PRIME + AV_PIX_FMT_OPENCL AVPixelFormat = C.AV_PIX_FMT_OPENCL + AV_PIX_FMT_GRAY14BE AVPixelFormat = C.AV_PIX_FMT_GRAY14BE ///< Y , 14bpp, big-endian + AV_PIX_FMT_GRAY14LE AVPixelFormat = C.AV_PIX_FMT_GRAY14LE ///< Y , 14bpp, little-endian + AV_PIX_FMT_GRAYF32BE AVPixelFormat = C.AV_PIX_FMT_GRAYF32BE ///< IEEE-754 single precision Y, 32bpp, big-endian + AV_PIX_FMT_GRAYF32LE AVPixelFormat = C.AV_PIX_FMT_GRAYF32LE ///< IEEE-754 single precision Y, 32bpp, little-endian + AV_PIX_FMT_YUVA422P12BE AVPixelFormat = C.AV_PIX_FMT_YUVA422P12BE ///< planar YUV 4:2:2,24bpp, (1 Cr & Cb sample per 2x1 Y samples), 12b alpha, big-endian + AV_PIX_FMT_YUVA422P12LE AVPixelFormat = C.AV_PIX_FMT_YUVA422P12LE ///< planar YUV 4:2:2,24bpp, (1 Cr & Cb sample per 2x1 Y samples), 12b alpha, little-endian + AV_PIX_FMT_YUVA444P12BE AVPixelFormat = C.AV_PIX_FMT_YUVA444P12BE ///< planar YUV 4:4:4,36bpp, (1 Cr & Cb sample per 1x1 Y samples), 12b alpha, big-endian + AV_PIX_FMT_YUVA444P12LE AVPixelFormat = C.AV_PIX_FMT_YUVA444P12LE ///< planar YUV 4:4:4,36bpp, (1 Cr & Cb sample per 1x1 Y samples), 12b alpha, little-endian + AV_PIX_FMT_NV24 AVPixelFormat = C.AV_PIX_FMT_NV24 ///< planar YUV 4:4:4, 24bpp, 1 plane for Y and 1 plane for the UV components, which are interleaved (first byte U and the following byte V) + AV_PIX_FMT_NV42 AVPixelFormat = C.AV_PIX_FMT_NV42 ///< as above, but U and V bytes are swapped + AV_PIX_FMT_VULKAN AVPixelFormat = C.AV_PIX_FMT_VULKAN + AV_PIX_FMT_Y210BE AVPixelFormat = C.AV_PIX_FMT_Y210BE ///< packed YUV 4:2:2 like YUYV422, 20bpp, data in the high bits, big-endian + AV_PIX_FMT_Y210LE AVPixelFormat = C.AV_PIX_FMT_Y210LE ///< packed YUV 4:2:2 like YUYV422, 20bpp, data in the high bits, little-endian + AV_PIX_FMT_X2RGB10LE AVPixelFormat = C.AV_PIX_FMT_X2RGB10LE ///< packed RGB 10:10:10, 30bpp, (msb)2X 10R 10G 10B(lsb), little-endian, X=unused/undefined + AV_PIX_FMT_X2RGB10BE AVPixelFormat = C.AV_PIX_FMT_X2RGB10BE ///< packed RGB 10:10:10, 30bpp, (msb)2X 10R 10G 10B(lsb), big-endian, X=unused/undefined + AV_PIX_FMT_X2BGR10LE AVPixelFormat = C.AV_PIX_FMT_X2BGR10LE ///< packed BGR 10:10:10, 30bpp, (msb)2X 10B 10G 10R(lsb), little-endian, X=unused/undefined + AV_PIX_FMT_X2BGR10BE AVPixelFormat = C.AV_PIX_FMT_X2BGR10BE ///< packed BGR 10:10:10, 30bpp, (msb)2X 10B 10G 10R(lsb), big-endian, X=unused/undefined + AV_PIX_FMT_P210BE AVPixelFormat = C.AV_PIX_FMT_P210BE ///< interleaved chroma YUV 4:2:2, 20bpp, data in the high bits, big-endian + AV_PIX_FMT_P210LE AVPixelFormat = C.AV_PIX_FMT_P210LE ///< interleaved chroma YUV 4:2:2, 20bpp, data in the high bits, little-endian + AV_PIX_FMT_P410BE AVPixelFormat = C.AV_PIX_FMT_P410BE ///< interleaved chroma YUV 4:4:4, 30bpp, data in the high bits, big-endian + AV_PIX_FMT_P410LE AVPixelFormat = C.AV_PIX_FMT_P410LE ///< interleaved chroma YUV 4:4:4, 30bpp, data in the high bits, little-endian + AV_PIX_FMT_P216BE AVPixelFormat = C.AV_PIX_FMT_P216BE ///< interleaved chroma YUV 4:2:2, 32bpp, big-endian + AV_PIX_FMT_P216LE AVPixelFormat = C.AV_PIX_FMT_P216LE ///< interleaved chroma YUV 4:2:2, 32bpp, little-endian + AV_PIX_FMT_P416BE AVPixelFormat = C.AV_PIX_FMT_P416BE ///< interleaved chroma YUV 4:4:4, 48bpp, big-endian + AV_PIX_FMT_P416LE AVPixelFormat = C.AV_PIX_FMT_P416LE ///< interleaved chroma YUV 4:4:4, 48bpp, little-endian + //AV_PIX_FMT_VUYA AVPixelFormat = C.AV_PIX_FMT_VUYA ///< packed VUYA 4:4:4, 32bpp, VUYAVUYA... + //AV_PIX_FMT_RGBAF16BE AVPixelFormat = C.AV_PIX_FMT_RGBAF16BE ///< IEEE-754 half precision packed RGBA 16:16:16:16, 64bpp, RGBARGBA..., big-endian + //AV_PIX_FMT_RGBAF16LE AVPixelFormat = C.AV_PIX_FMT_RGBAF16LE ///< IEEE-754 half precision packed RGBA 16:16:16:16, 64bpp, RGBARGBA..., little-endian + //AV_PIX_FMT_VUYX AVPixelFormat = C.AV_PIX_FMT_VUYX ///< packed VUYX 4:4:4, 32bpp, Variant of VUYA where alpha channel is left undefined + //AV_PIX_FMT_P012LE AVPixelFormat = C.AV_PIX_FMT_P012LE ///< like NV12, with 12bpp per component, data in the high bits, zeros in the low bits, little-endian + //AV_PIX_FMT_P012BE AVPixelFormat = C.AV_PIX_FMT_P012BE ///< like NV12, with 12bpp per component, data in the high bits, zeros in the low bits, big-endian + //AV_PIX_FMT_Y212BE AVPixelFormat = C.AV_PIX_FMT_Y212BE ///< packed YUV 4:2:2 like YUYV422, 24bpp, data in the high bits, zeros in the low bits, big-endian + //AV_PIX_FMT_Y212LE AVPixelFormat = C.AV_PIX_FMT_Y212LE ///< packed YUV 4:2:2 like YUYV422, 24bpp, data in the high bits, zeros in the low bits, little-endian + //AV_PIX_FMT_XV30BE AVPixelFormat = C.AV_PIX_FMT_XV30BE ///< packed XVYU 4:4:4, 32bpp, (msb)2X 10V 10Y 10U(lsb), big-endian, variant of Y410 where alpha channel is left undefined + //AV_PIX_FMT_XV30LE AVPixelFormat = C.AV_PIX_FMT_XV30LE ///< packed XVYU 4:4:4, 32bpp, (msb)2X 10V 10Y 10U(lsb), little-endian, variant of Y410 where alpha channel is left undefined + //AV_PIX_FMT_XV36BE AVPixelFormat = C.AV_PIX_FMT_XV36BE ///< packed XVYU 4:4:4, 48bpp, data in the high bits, zeros in the low bits, big-endian, variant of Y412 where alpha channel is left undefined + //AV_PIX_FMT_XV36LE AVPixelFormat = C.AV_PIX_FMT_XV36LE ///< packed XVYU 4:4:4, 48bpp, data in the high bits, zeros in the low bits, little-endian, variant of Y412 where alpha channel is left undefined + //AV_PIX_FMT_RGBF32BE AVPixelFormat = C.AV_PIX_FMT_RGBF32BE ///< IEEE-754 single precision packed RGB 32:32:32, 96bpp, RGBRGB..., big-endian + //AV_PIX_FMT_RGBF32LE AVPixelFormat = C.AV_PIX_FMT_RGBF32LE ///< IEEE-754 single precision packed RGB 32:32:32, 96bpp, RGBRGB..., little-endian + //AV_PIX_FMT_RGBAF32BE AVPixelFormat = C.AV_PIX_FMT_RGBAF32BE ///< IEEE-754 single precision packed RGBA 32:32:32:32, 128bpp, RGBARGBA..., big-endian + //AV_PIX_FMT_RGBAF32LE AVPixelFormat = C.AV_PIX_FMT_RGBAF32LE ///< IEEE-754 single precision packed RGBA 32:32:32:32, 128bpp, RGBARGBA..., little-endian +) + +//////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (v AVPixelFormat) MarshalJSON() ([]byte, error) { + return json.Marshal(v.String()) +} + +func (v AVPixelFormat) String() string { + if f := AVUtil_get_pix_fmt_name(v); f != "" { + return f + } + return fmt.Sprintf("AVPixelFormat(%d)", int(v)) +} + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC FUNCTIONS + +func AVUtil_get_pix_fmt_name(pixfmt AVPixelFormat) string { + return C.GoString(C.av_get_pix_fmt_name((C.enum_AVPixelFormat)(pixfmt))) +} + +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 new file mode 100644 index 0000000..212733e --- /dev/null +++ b/sys/ffmpeg61/avutil_rational.go @@ -0,0 +1,58 @@ +package ffmpeg + +import ( + "encoding/json" + "fmt" +) + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavutil +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (r AVRational) MarshalJSON() ([]byte, error) { + if r.num == 0 { + return json.Marshal(0) + } + return json.Marshal(fmt.Sprintf("%d/%d", r.num, r.den)) +} + +func (r AVRational) String() string { + data, _ := json.MarshalIndent(r, "", " ") + return string(data) +} + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Create a new rational +func AVUtil_rational(num, den int) AVRational { + return AVRational{num: C.int(num), den: C.int(den)} +} + +// Numerator +func (r AVRational) Num() int { + return int(r.num) +} + +// Denominator +func (r AVRational) Den() int { + return int(r.den) +} + +// IsZero returns true if the rational is zero +func (r AVRational) IsZero() bool { + return r.num == 0 +} + +// Float is used to convert an int64 value multipled by the rational to a float64 +func (r AVRational) Float(multiplier int64) float64 { + return float64(int64(r.num)*multiplier) / float64(r.den) +} diff --git a/sys/ffmpeg61/avutil_samplefmt.go b/sys/ffmpeg61/avutil_samplefmt.go new file mode 100644 index 0000000..86e44f7 --- /dev/null +++ b/sys/ffmpeg61/avutil_samplefmt.go @@ -0,0 +1,84 @@ +package ffmpeg + +import ( + "encoding/json" + "unsafe" +) + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavutil +#include +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// CONSTANTS + +const ( + AV_SAMPLE_FMT_NONE AVSampleFormat = C.AV_SAMPLE_FMT_NONE + AV_SAMPLE_FMT_U8 AVSampleFormat = C.AV_SAMPLE_FMT_U8 + AV_SAMPLE_FMT_S16 AVSampleFormat = C.AV_SAMPLE_FMT_S16 + AV_SAMPLE_FMT_S32 AVSampleFormat = C.AV_SAMPLE_FMT_S32 + AV_SAMPLE_FMT_FLT AVSampleFormat = C.AV_SAMPLE_FMT_FLT + AV_SAMPLE_FMT_DBL AVSampleFormat = C.AV_SAMPLE_FMT_DBL + AV_SAMPLE_FMT_U8P AVSampleFormat = C.AV_SAMPLE_FMT_U8P + AV_SAMPLE_FMT_S16P AVSampleFormat = C.AV_SAMPLE_FMT_S16P + AV_SAMPLE_FMT_S32P AVSampleFormat = C.AV_SAMPLE_FMT_S32P + AV_SAMPLE_FMT_FLTP AVSampleFormat = C.AV_SAMPLE_FMT_FLTP + AV_SAMPLE_FMT_DBLP AVSampleFormat = C.AV_SAMPLE_FMT_DBLP + AV_SAMPLE_FMT_S64 AVSampleFormat = C.AV_SAMPLE_FMT_S64 + AV_SAMPLE_FMT_S64P AVSampleFormat = C.AV_SAMPLE_FMT_S64P + AV_SAMPLE_FMT_NB AVSampleFormat = C.AV_SAMPLE_FMT_NB +) + +//////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (v AVSampleFormat) String() string { + return AVUtil_get_sample_fmt_name(v) +} + +func (v AVSampleFormat) MarshalJSON() ([]byte, error) { + return json.Marshal(v.String()) +} + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC FUNCTIONS + +// 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))) +} + +// Return a sample format corresponding to name, or AV_SAMPLE_FMT_NONE on error. +func AVUtil_get_sample_fmt(name string) AVSampleFormat { + cName := C.CString(name) + defer C.free(unsafe.Pointer(cName)) + return AVSampleFormat(C.av_get_sample_fmt(cName)) +} + +// Return number of bytes per sample. +func AVUtil_get_bytes_per_sample(sample_fmt AVSampleFormat) int { + return int(C.av_get_bytes_per_sample(C.enum_AVSampleFormat(sample_fmt))) +} + +// Check if the sample format is planar. +func AVUtil_sample_fmt_is_planar(sample_fmt AVSampleFormat) bool { + return C.av_sample_fmt_is_planar(C.enum_AVSampleFormat(sample_fmt)) != 0 +} + +// Get the packed alternative form of the given sample format. +// If the passed sample_fmt is already in packed format, the format returned is the same as the input. +func AVUtil_get_packed_sample_fmt(sample_fmt AVSampleFormat) AVSampleFormat { + return AVSampleFormat(C.av_get_packed_sample_fmt(C.enum_AVSampleFormat(sample_fmt))) +} + +// Get the planar alternative form of the given sample format. +// If the passed sample_fmt is already in planar format, the format returned is the same as the input. +func AVUtil_get_planar_sample_fmt(sample_fmt AVSampleFormat) AVSampleFormat { + return AVSampleFormat(C.av_get_planar_sample_fmt(C.enum_AVSampleFormat(sample_fmt))) +} diff --git a/sys/ffmpeg61/avutil_samplefmt_test.go b/sys/ffmpeg61/avutil_samplefmt_test.go new file mode 100644 index 0000000..fdb328a --- /dev/null +++ b/sys/ffmpeg61/avutil_samplefmt_test.go @@ -0,0 +1,34 @@ +package ffmpeg_test + +import ( + "testing" + + // Packages + "github.com/stretchr/testify/assert" + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func Test_avutil_samplefmt_000(t *testing.T) { + assert := assert.New(t) + for fmt := AV_SAMPLE_FMT_NONE; fmt < AV_SAMPLE_FMT_NB; fmt++ { + if fmt == AV_SAMPLE_FMT_NONE { + continue + } + t.Logf("sample_fmt[%d]=%v", fmt, AVUtil_get_sample_fmt_name(fmt)) + fmt_ := AVUtil_get_sample_fmt(AVUtil_get_sample_fmt_name(fmt)) + assert.Equal(fmt, fmt_) + } +} + +func Test_avutil_samplefmt_001(t *testing.T) { + for fmt := AV_SAMPLE_FMT_NONE; fmt < AV_SAMPLE_FMT_NB; fmt++ { + if fmt == AV_SAMPLE_FMT_NONE { + continue + } + t.Logf("sample_fmt[%d]=%v", fmt, AVUtil_get_sample_fmt_name(fmt)) + t.Logf(" is_planar=%v", AVUtil_sample_fmt_is_planar(fmt)) + t.Logf(" bytes_per_sample=%v", AVUtil_get_bytes_per_sample(fmt)) + } +} diff --git a/sys/ffmpeg61/avutil_samples.go b/sys/ffmpeg61/avutil_samples.go new file mode 100644 index 0000000..26b34b0 --- /dev/null +++ b/sys/ffmpeg61/avutil_samples.go @@ -0,0 +1,147 @@ +package ffmpeg + +import ( + "errors" + "unsafe" +) + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavutil +#include +#include +*/ +import "C" + +const ( + AV_NUM_PLANES = 48 +) + +type AVSamples struct { + nb_samples C.int + nb_channels C.int + sample_fmt C.enum_AVSampleFormat + plane_size C.int + buffer_size C.int + planes [AV_NUM_PLANES]*C.uint8_t +} + +//////////////////////////////////////////////////////////////////////////////// +// METHODS + +func (data *AVSamples) Bytes(plane int) []byte { + if plane < 0 || plane >= AV_NUM_PLANES { + return nil + } + if ptr := data.planes[plane]; ptr == nil { + return nil + } else { + return cByteSlice(unsafe.Pointer(ptr), data.plane_size) + } +} + +func (data *AVSamples) Int16(plane int) []int16 { + if plane < 0 || plane >= AV_NUM_PLANES { + return nil + } + if ptr := data.planes[plane]; ptr == nil { + return nil + } else { + return cInt16Slice(unsafe.Pointer(ptr), data.plane_size>>1) + } +} + +func (data *AVSamples) Float64(plane int) []float64 { + if plane < 0 || plane >= AV_NUM_PLANES { + return nil + } + if ptr := data.planes[plane]; ptr == nil { + return nil + } else { + return cFloat64Slice(unsafe.Pointer(ptr), data.plane_size>>3) + } +} + +func (data *AVSamples) NumPlanes() int { + if AVUtil_sample_fmt_is_planar(AVSampleFormat(data.sample_fmt)) { + return int(data.nb_channels) + } else { + return 1 + } +} + +func (data *AVSamples) NumChannels() int { + return int(data.nb_channels) +} + +func (data *AVSamples) NumSamples() int { + return int(data.nb_samples) +} + +//////////////////////////////////////////////////////////////////////////////// +// BINDINGS + +// Allocate a samples buffer for nb_samples samples. Return allocated data for each plane, and the stride. +func AVUtil_samples_alloc(nb_samples, nb_channels int, sample_fmt AVSampleFormat, align bool) (*AVSamples, error) { + if nb_channels < 1 { + return nil, errors.New("too few channels") + } + if AVUtil_sample_fmt_is_planar(sample_fmt) && nb_channels > AV_NUM_PLANES { + return nil, errors.New("too many channels") + } + data := &AVSamples{nb_samples: C.int(nb_samples), nb_channels: C.int(nb_channels), sample_fmt: C.enum_AVSampleFormat(sample_fmt)} + + // Get the size + if size := C.av_samples_get_buffer_size(nil, data.nb_channels, data.nb_samples, data.sample_fmt, boolToInt(align)); size < 0 { + return nil, AVError(size) + } else { + data.buffer_size = size + } + + // Allocate the buffer + if err := AVError(C.av_samples_alloc(&data.planes[0], &data.plane_size, data.nb_channels, data.nb_samples, data.sample_fmt, boolToInt(align))); err < 0 { + return nil, err + } + + // Return success + return data, nil +} + +// Free the samples +func AVUtil_samples_free(samples *AVSamples) { + C.av_freep(unsafe.Pointer(&samples.planes[0])) +} + +// Get the required buffer size for the given audio parameters. Returns the calculated buffer size and plane size. +func AVUtil_samples_get_buffer_size(nb_samples, nb_channels int, sample_fmt AVSampleFormat, align bool) (int, int, error) { + var plane_size C.int + ret := int(C.av_samples_get_buffer_size(&plane_size, C.int(nb_channels), C.int(nb_samples), C.enum_AVSampleFormat(sample_fmt), boolToInt(align))) + if ret < 0 { + return 0, 0, AVError(ret) + } else { + return ret, int(plane_size), nil + } +} + +// Copy samples - dst and src channels and formats need to match +func AVUtil_samples_copy(dst, src *AVSamples, dst_offset, src_offset, nb_samples int) error { + if dst.sample_fmt != src.sample_fmt { + return errors.New("sample formats do not match") + } + if dst.nb_channels != src.nb_channels { + return errors.New("sample channels do not match") + } + + // Perform the copy + C.av_samples_copy(&dst.planes[0], &src.planes[0], C.int(dst_offset), C.int(src_offset), C.int(nb_samples), dst.nb_channels, dst.sample_fmt) + + // Return success + return nil +} + +// Fill an audio buffer with silence +func AVUtil_samples_set_silence(data *AVSamples, offset int, nb_samples int) { + C.av_samples_set_silence(&data.planes[0], C.int(offset), C.int(nb_samples), data.nb_channels, data.sample_fmt) +} diff --git a/sys/ffmpeg61/avutil_samples_test.go b/sys/ffmpeg61/avutil_samples_test.go new file mode 100644 index 0000000..1262d1e --- /dev/null +++ b/sys/ffmpeg61/avutil_samples_test.go @@ -0,0 +1,31 @@ +package ffmpeg_test + +import ( + "testing" + + // Packages + "github.com/stretchr/testify/assert" + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func Test_avutil_samples_000(t *testing.T) { + assert := assert.New(t) + + num_channels := 6 + num_samples := 1 + format := AV_SAMPLE_FMT_U8P + data, err := AVUtil_samples_alloc(num_samples, num_channels, format, true) + if !assert.NoError(err) { + t.SkipNow() + } + assert.NotNil(data) + AVUtil_samples_set_silence(data, 0, num_samples) + + for plane := 0; plane < data.NumPlanes(); plane++ { + assert.NotNil(data.Bytes(plane)) + } + + AVUtil_samples_free(data) +} diff --git a/sys/ffmpeg61/avutil_time.go b/sys/ffmpeg61/avutil_time.go new file mode 100644 index 0000000..2073b12 --- /dev/null +++ b/sys/ffmpeg61/avutil_time.go @@ -0,0 +1,33 @@ +package ffmpeg + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavutil +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// GLOBALS + +var ( + buf [C.AV_TS_MAX_STRING_SIZE]C.char +) + +func AVUtil_ts_make_string(ts int64) string { + return C.GoString(C.av_ts_make_string(&buf[0], C.int64_t(ts))) +} + +func AVUtil_ts_make_time_string(ts int64, tb *AVRational) string { + return C.GoString(C.av_ts_make_time_string(&buf[0], C.int64_t(ts), (*C.struct_AVRational)(tb))) +} + +func AVUtil_ts2str(ts int64) string { + return AVUtil_ts_make_string(ts) +} + +func AVUtil_ts2timestr(ts int64, tb *AVRational) string { + return AVUtil_ts_make_time_string(ts, tb) +} diff --git a/sys/ffmpeg61/avutil_version.go b/sys/ffmpeg61/avutil_version.go new file mode 100644 index 0000000..f9a8ffe --- /dev/null +++ b/sys/ffmpeg61/avutil_version.go @@ -0,0 +1,28 @@ +package ffmpeg + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libavutil +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS - VERSION + +// Return the LIBAVFORMAT_VERSION_INT constant. +func AVUtil_version() uint { + return uint(C.avutil_version()) +} + +// Return the libavformat build-time configuration. +func AVUtil_configuration() string { + return C.GoString(C.avutil_configuration()) +} + +// Return the libavformat license. +func AVUtil_license() string { + return C.GoString(C.avutil_license()) +} diff --git a/sys/ffmpeg61/avutil_version_test.go b/sys/ffmpeg61/avutil_version_test.go new file mode 100644 index 0000000..c2212a7 --- /dev/null +++ b/sys/ffmpeg61/avutil_version_test.go @@ -0,0 +1,20 @@ +package ffmpeg_test + +import ( + "testing" + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func Test_avutil_version_000(t *testing.T) { + t.Log("avutil_version=", AVUtil_version()) +} + +func Test_avutil_version_001(t *testing.T) { + t.Log("avutil_configuration=", AVUtil_configuration()) +} + +func Test_avutil_version_002(t *testing.T) { + t.Log("avutil_license=", AVUtil_license()) +} diff --git a/sys/ffmpeg61/doc.go b/sys/ffmpeg61/doc.go new file mode 100644 index 0000000..7ba7576 --- /dev/null +++ b/sys/ffmpeg61/doc.go @@ -0,0 +1,8 @@ +/* +The low-level ffmpeg bindings for ffmpeg version 6.1. It's more likely +you would use the higher-level package github.com/mutablelogic/go-media/pkg/ffmpeg +which provides a more idiomatic interface to ffmpeg. + +Ref: https://ffmpeg.org/doxygen/6.1/index.html +*/ +package ffmpeg diff --git a/sys/ffmpeg61/swresample.go b/sys/ffmpeg61/swresample.go new file mode 100644 index 0000000..9d9903c --- /dev/null +++ b/sys/ffmpeg61/swresample.go @@ -0,0 +1,17 @@ +package ffmpeg + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libswresample +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// TYPES + +type ( + SWRContext C.struct_SwrContext +) diff --git a/sys/ffmpeg61/swresample.go_old b/sys/ffmpeg61/swresample.go_old new file mode 100755 index 0000000..878cdeb --- /dev/null +++ b/sys/ffmpeg61/swresample.go_old @@ -0,0 +1,10 @@ + +// 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 + } +} diff --git a/sys/ffmpeg61/swresample_convert.go b/sys/ffmpeg61/swresample_convert.go new file mode 100644 index 0000000..ee49901 --- /dev/null +++ b/sys/ffmpeg61/swresample_convert.go @@ -0,0 +1,86 @@ +package ffmpeg + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libswresample +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Core conversion function. Returns number of samples output per channel. +// in and in_count can be set to 0 to flush the last few samples out at the end. +func SWResample_convert(ctx *SWRContext, dst *AVSamples, dst_nb_samples int, src *AVSamples, src_nb_samples int) (int, error) { + n := int(C.swr_convert( + (*C.struct_SwrContext)(ctx), + &dst.planes[0], + C.int(dst_nb_samples), + &src.planes[0], + C.int(src_nb_samples), + )) + if n < 0 { + return 0, AVError(n) + } else { + return n, nil + } +} + +// Convert the next timestamp from input to output timestamps are in 1/(in_sample_rate * out_sample_rate) units. +func SWResample_next_pts(ctx *SWRContext, pts int64) int64 { + return int64(C.swr_next_pts((*C.struct_SwrContext)(ctx), C.int64_t(pts))) +} + +// Drops the specified number of output samples. +func SWResample_drop_output(ctx *SWRContext, count int) error { + if err := AVError(C.swr_drop_output((*C.struct_SwrContext)(ctx), C.int(count))); err != 0 { + return err + } else { + return nil + } +} + +// Inject the specified number of silence samples. +func SWResample_inject_silence(ctx *SWRContext, count int) error { + if err := AVError(C.swr_inject_silence((*C.struct_SwrContext)(ctx), C.int(count))); err != 0 { + return err + } else { + return nil + } +} + +// Gets the delay the next input sample will experience relative to the next output sample. +func SWResample_get_delay(ctx *SWRContext, base int64) int64 { + return int64(C.swr_get_delay((*C.struct_SwrContext)(ctx), C.int64_t(base))) +} + +// Find an upper bound on the number of samples that the next swr_convert call will output, if called with in_samples of input samples. +func SWResample_get_out_samples(ctx *SWRContext, in_samples int) (int, error) { + n := int(C.swr_get_out_samples((*C.struct_SwrContext)(ctx), C.int(in_samples))) + if n < 0 { + return n, AVError(n) + } else { + return n, nil + } +} + +// Convert the samples in the input AVFrame and write them to the output AVFrame. +func SWResample_convert_frame(ctx *SWRContext, src, dest *AVFrame) error { + if err := AVError(C.swr_convert_frame((*C.struct_SwrContext)(ctx), (*C.struct_AVFrame)(dest), (*C.struct_AVFrame)(src))); err != 0 { + return err + } else { + return nil + } +} + +// Configure or reconfigure the SwrContext using the information provided by the AVFrames. +func SWResample_config_frame(ctx *SWRContext, src, dest *AVFrame) error { + if err := AVError(C.swr_config_frame((*C.struct_SwrContext)(ctx), (*C.struct_AVFrame)(dest), (*C.struct_AVFrame)(src))); err != 0 { + return err + } else { + return nil + } +} diff --git a/sys/ffmpeg61/swresample_core.go b/sys/ffmpeg61/swresample_core.go new file mode 100644 index 0000000..2c50a3d --- /dev/null +++ b/sys/ffmpeg61/swresample_core.go @@ -0,0 +1,46 @@ +package ffmpeg + +import ( + "unsafe" +) + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libswresample +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Allocate SwrContext. +func SWResample_alloc() *SWRContext { + return (*SWRContext)(C.swr_alloc()) +} + +// Free the given SwrContext. +func SWResample_free(ctx *SWRContext) { + C.swr_free((**C.struct_SwrContext)(unsafe.Pointer(&ctx))) +} + +// Initialize context after user parameters have been set. +func SWResample_init(ctx *SWRContext) error { + if err := AVError(C.swr_init((*C.struct_SwrContext)(ctx))); err == 0 { + return nil + } else { + return err + } +} + +// Closes the context so that swr_is_initialized() returns 0 +func SWResample_close(ctx *SWRContext) { + C.swr_close((*C.struct_SwrContext)(ctx)) +} + +// Check whether an swr context has been initialized or not. +func SWResample_is_initialized(ctx *SWRContext) bool { + return C.swr_is_initialized((*C.struct_SwrContext)(ctx)) != 0 +} diff --git a/sys/ffmpeg61/swresample_core_test.go b/sys/ffmpeg61/swresample_core_test.go new file mode 100644 index 0000000..1d1524d --- /dev/null +++ b/sys/ffmpeg61/swresample_core_test.go @@ -0,0 +1,17 @@ +package ffmpeg_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func Test_swresample_core_000(t *testing.T) { + assert := assert.New(t) + ctx := SWResample_alloc() + assert.NotNil(ctx) + SWResample_free(ctx) +} diff --git a/sys/ffmpeg61/swresample_opts.go b/sys/ffmpeg61/swresample_opts.go new file mode 100644 index 0000000..c64424d --- /dev/null +++ b/sys/ffmpeg61/swresample_opts.go @@ -0,0 +1,33 @@ +package ffmpeg + +import "unsafe" + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libswresample libavutil +#include +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Set common parameters for resampling. +func SWResample_set_opts(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) error { + if err := AVError(C.swr_alloc_set_opts2((**C.struct_SwrContext)(unsafe.Pointer(&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), + 0, nil)); err < 0 { + return err + } + + // Return success + return nil +} diff --git a/sys/ffmpeg61/swresample_opts_test.go b/sys/ffmpeg61/swresample_opts_test.go new file mode 100644 index 0000000..30c95f3 --- /dev/null +++ b/sys/ffmpeg61/swresample_opts_test.go @@ -0,0 +1,25 @@ +package ffmpeg_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func Test_swresample_opts_000(t *testing.T) { + in_chlayout := AVChannelLayout(AV_CHANNEL_LAYOUT_STEREO) + out_chlayout := AVChannelLayout(AV_CHANNEL_LAYOUT_MONO) + in_format := AV_SAMPLE_FMT_FLTP + out_format := AV_SAMPLE_FMT_S16 + + assert := assert.New(t) + ctx := SWResample_alloc() + assert.NotNil(ctx) + assert.NoError(SWResample_set_opts(ctx, in_chlayout, in_format, 44100, out_chlayout, out_format, 48000)) + assert.NoError(SWResample_init(ctx)) + assert.True(SWResample_is_initialized(ctx)) + SWResample_free(ctx) +} diff --git a/sys/ffmpeg61/swresample_version.go b/sys/ffmpeg61/swresample_version.go new file mode 100644 index 0000000..5e028ef --- /dev/null +++ b/sys/ffmpeg61/swresample_version.go @@ -0,0 +1,28 @@ +package ffmpeg + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libswresample +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Return the LIBSWRESAMPLE_VERSION_INT constant. +func SWResample_version() uint { + return uint(C.swresample_version()) +} + +// Return the swr build-time configuration. +func SWResample_configuration() string { + return C.GoString(C.swresample_configuration()) +} + +// Return the swr license. +func SWResample_license() string { + return C.GoString(C.swresample_license()) +} diff --git a/sys/ffmpeg61/swresample_version_test.go b/sys/ffmpeg61/swresample_version_test.go new file mode 100644 index 0000000..554e9c0 --- /dev/null +++ b/sys/ffmpeg61/swresample_version_test.go @@ -0,0 +1,20 @@ +package ffmpeg_test + +import ( + "testing" + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func Test_swresample_version_000(t *testing.T) { + t.Log("swresample_version=", SWResample_version()) +} + +func Test_swresample_version_001(t *testing.T) { + t.Log("swresample_configuration=", SWResample_configuration()) +} + +func Test_swresample_version_002(t *testing.T) { + t.Log("swresample_license=", SWResample_license()) +} diff --git a/sys/ffmpeg61/swscale.go b/sys/ffmpeg61/swscale.go new file mode 100644 index 0000000..026317f --- /dev/null +++ b/sys/ffmpeg61/swscale.go @@ -0,0 +1,86 @@ +package ffmpeg + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libswscale +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// TYPES + +type ( + SWSContext C.struct_SwsContext + SWSFilter C.struct_SwsFilter + SWSFlag C.int +) + +//////////////////////////////////////////////////////////////////////////////// +// CONSTANTS + +const ( + SWS_NONE SWSFlag = 0 + SWS_FAST_BILINEAR SWSFlag = C.SWS_FAST_BILINEAR + SWS_BILINEAR SWSFlag = C.SWS_BILINEAR + SWS_BICUBIC SWSFlag = C.SWS_BICUBIC + SWS_X SWSFlag = C.SWS_X + SWS_POINT SWSFlag = C.SWS_POINT + SWS_AREA SWSFlag = C.SWS_AREA + SWS_BICUBLIN SWSFlag = C.SWS_BICUBLIN + SWS_GAUSS SWSFlag = C.SWS_GAUSS + SWS_SINC SWSFlag = C.SWS_SINC + SWS_LANCZOS SWSFlag = C.SWS_LANCZOS + SWS_SPLINE SWSFlag = C.SWS_SPLINE + SWS_MIN = SWS_FAST_BILINEAR + SWS_MAX = SWS_SPLINE +) + +//////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (v SWSFlag) FlagString() string { + switch v { + case SWS_NONE: + return "SWS_NONE" + case SWS_FAST_BILINEAR: + return "SWS_FAST_BILINEAR" + case SWS_BILINEAR: + return "SWS_BILINEAR" + case SWS_BICUBIC: + return "SWS_BICUBIC" + case SWS_X: + return "SWS_X" + case SWS_POINT: + return "SWS_POINT" + case SWS_AREA: + return "SWS_AREA" + case SWS_BICUBLIN: + return "SWS_BICUBLIN" + case SWS_GAUSS: + return "SWS_GAUSS" + case SWS_SINC: + return "SWS_SINC" + case SWS_LANCZOS: + return "SWS_LANCZOS" + case SWS_SPLINE: + return "SWS_SPLINE" + default: + return "[?? Invalid SWSFlag value]" + } +} + +func (v SWSFlag) String() string { + if v == SWS_NONE { + return v.FlagString() + } + str := "" + for i := SWS_MIN; i <= SWS_MAX; i <<= 1 { + if v&i != 0 { + str += "|" + i.FlagString() + } + } + return str[1:] +} diff --git a/sys/ffmpeg61/swscale_core.go b/sys/ffmpeg61/swscale_core.go new file mode 100644 index 0000000..1cf3fce --- /dev/null +++ b/sys/ffmpeg61/swscale_core.go @@ -0,0 +1,114 @@ +package ffmpeg + +import ( + "fmt" + "unsafe" +) + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libswscale +#include +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Allocate an empty SWSContext. +func SWScale_alloc_context() *SWSContext { + return (*SWSContext)(C.sws_alloc_context()) +} + +// Initialize the swscaler context sws_context. +func SWScale_init_context(ctx *SWSContext, src, dst *SWSFilter) { + C.sws_init_context((*C.struct_SwsContext)(ctx), (*C.struct_SwsFilter)(src), (*C.struct_SwsFilter)(dst)) +} + +// Free the swscaler context swsContext. +func SWScale_free_context(ctx *SWSContext) { + C.sws_freeContext((*C.struct_SwsContext)(ctx)) +} + +// Allocate and return an SwsContext. +func SWScale_get_context(src_width, src_height int, src_format AVPixelFormat, dst_width, dst_height int, dst_format AVPixelFormat, flags SWSFlag, src_filter, dst_filter *SWSFilter, param []float64) *SWSContext { + var params *C.double + if len(param) > 0 { + params = (*C.double)(unsafe.Pointer(¶m[0])) + } + ctx := C.sws_getContext(C.int(src_width), C.int(src_height), C.enum_AVPixelFormat(src_format), C.int(dst_width), C.int(dst_height), C.enum_AVPixelFormat(dst_format), C.int(flags), (*C.struct_SwsFilter)(src_filter), (*C.struct_SwsFilter)(dst_filter), params) + return (*SWSContext)(ctx) +} + +// Scale the image slice in src and put the resulting scaled slice in the image in dst. +// Returns the height of the output slice. +func SWScale_scale(ctx *SWSContext, src [][]byte, src_stride []int, src_slice_y, src_slice_height int, dest [][]byte, dest_stride []int) int { + src_, src_stride_ := avutil_image_ptr(src, src_stride) + dest_, dest_stride_ := avutil_image_ptr(dest, dest_stride) + return int(C.sws_scale( + (*C.struct_SwsContext)(ctx), + &src_[0], &src_stride_[0], + C.int(src_slice_y), + C.int(src_slice_height), + &dest_[0], &dest_stride_[0], + )) +} + +// Scale source data from src and write the output to dst. Need to find out +// why the native version is returning -22 error TODO +func SWScale_scale_frame(ctx *SWSContext, dest, src *AVFrame, native bool) error { + if native { + if ret := C.sws_scale_frame((*C.struct_SwsContext)(ctx), (*C.struct_AVFrame)(dest), (*C.struct_AVFrame)(src)); ret != 0 { + return AVError(ret) + } + } else { + if err := SWScale_frame_start(ctx, dest, src); err != nil { + return fmt.Errorf("SWScale_frame_start: %w", err) + } + if err := SWScale_send_slice(ctx, 0, uint(src.Height())); err != nil { + return fmt.Errorf("SWScale_send_slice: %w", err) + } + if err := SWScale_receive_slice(ctx, 0, uint(dest.Height())); err != nil { + return fmt.Errorf("SWScale_receive_slice: %w", err) + } + SWScale_frame_end(ctx) + } + + // Return success + return nil +} + +// Initialize the scaling process for a given pair of source/destination frames. +func SWScale_frame_start(ctx *SWSContext, dest, src *AVFrame) error { + if ret := C.sws_frame_start((*C.struct_SwsContext)(ctx), (*C.struct_AVFrame)(dest), (*C.struct_AVFrame)(src)); ret != 0 { + return AVError(ret) + } else { + return nil + } +} + +// Finish the scaling process for a pair of source/destination frames. +func SWScale_frame_end(ctx *SWSContext) { + C.sws_frame_end((*C.struct_SwsContext)(ctx)) +} + +// Indicate that a horizontal slice of input data is available in the source frame +func SWScale_send_slice(ctx *SWSContext, slice_start, slice_height uint) error { + if ret := C.sws_send_slice((*C.struct_SwsContext)(ctx), C.uint(slice_start), C.uint(slice_height)); ret < 0 { + return AVError(ret) + } else { + return nil + } +} + +// Request a horizontal slice of the output data to be written into the frame +func SWScale_receive_slice(ctx *SWSContext, slice_start, slice_height uint) error { + if ret := C.sws_receive_slice((*C.struct_SwsContext)(ctx), C.uint(slice_start), C.uint(slice_height)); ret < 0 { + return AVError(ret) + } else { + return nil + } +} diff --git a/sys/ffmpeg61/swscale_core_test.go b/sys/ffmpeg61/swscale_core_test.go new file mode 100644 index 0000000..4ee7bcb --- /dev/null +++ b/sys/ffmpeg61/swscale_core_test.go @@ -0,0 +1,27 @@ +package ffmpeg_test + +import ( + "testing" + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" + "github.com/stretchr/testify/assert" +) + +func Test_swscale_core_000(t *testing.T) { + assert := assert.New(t) + ctx := SWScale_alloc_context() + if !assert.NotNil(ctx) { + t.SkipNow() + } + SWScale_free_context(ctx) +} + +func Test_swscale_core_001(t *testing.T) { + assert := assert.New(t) + ctx := SWScale_get_context(320, 240, AV_PIX_FMT_YUV420P, 640, 480, AV_PIX_FMT_RGB24, SWS_BILINEAR, nil, nil, nil) + if !assert.NotNil(ctx) { + t.SkipNow() + } + SWScale_free_context(ctx) +} diff --git a/sys/ffmpeg61/swscale_version.go b/sys/ffmpeg61/swscale_version.go new file mode 100644 index 0000000..cd7f107 --- /dev/null +++ b/sys/ffmpeg61/swscale_version.go @@ -0,0 +1,33 @@ +package ffmpeg + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +/* +#cgo pkg-config: libswscale +#include +*/ +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Return the LIBSWSCALE_VERSION_INT constant. +func SWScale_version() uint { + return uint(C.swscale_version()) +} + +// Return the swr build-time configuration. +func SWScale_configuration() string { + return C.GoString(C.swscale_configuration()) +} + +// Return the swr license. +func SWScale_license() string { + return C.GoString(C.swscale_license()) +} + +// Get the AVClass for swsContext. +func SWScale_get_class() *AVClass { + return (*AVClass)(C.sws_get_class()) +} diff --git a/sys/ffmpeg61/swscale_version_test.go b/sys/ffmpeg61/swscale_version_test.go new file mode 100644 index 0000000..a0a8fcc --- /dev/null +++ b/sys/ffmpeg61/swscale_version_test.go @@ -0,0 +1,24 @@ +package ffmpeg_test + +import ( + "testing" + + // Namespace imports + . "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +func Test_swscale_version_000(t *testing.T) { + t.Log("swscale_version=", SWScale_version()) +} + +func Test_swscale_version_001(t *testing.T) { + t.Log("swscale_configuration=", SWScale_configuration()) +} + +func Test_swscale_version_002(t *testing.T) { + t.Log("swscale_license=", SWScale_license()) +} + +func Test_swscale_version_003(t *testing.T) { + t.Log("swscale_class=", SWScale_get_class()) +} diff --git a/sys/ffmpeg61/util.go b/sys/ffmpeg61/util.go new file mode 100644 index 0000000..73623f9 --- /dev/null +++ b/sys/ffmpeg61/util.go @@ -0,0 +1,69 @@ +package ffmpeg + +import ( + "unsafe" +) + +//////////////////////////////////////////////////////////////////////////////// +// CGO + +import "C" + +//////////////////////////////////////////////////////////////////////////////// +// FUNCTIONS + +func boolToInt(v bool) C.int { + if v { + return C.int(1) + } + return 0 +} + +func cUint8Slice(p unsafe.Pointer, sz C.int) []uint8 { + if p == nil { + return nil + } + return (*[1 << 30]uint8)(p)[:int(sz)] +} + +func cInt8Slice(p unsafe.Pointer, sz C.int) []int8 { + if p == nil { + return nil + } + return (*[1 << 30]int8)(p)[:int(sz)] +} + +func cByteSlice(p unsafe.Pointer, sz C.int) []byte { + if p == nil { + return nil + } + return (*[1 << 30]byte)(p)[:int(sz)] +} + +func cUint16Slice(p unsafe.Pointer, sz C.int) []uint16 { + if p == nil { + return nil + } + return (*[1 << 30]uint16)(p)[:int(sz)] +} + +func cInt16Slice(p unsafe.Pointer, sz C.int) []int16 { + if p == nil { + return nil + } + return (*[1 << 30]int16)(p)[:int(sz)] +} + +func cFloat64Slice(p unsafe.Pointer, sz C.int) []float64 { + if p == nil { + return nil + } + return (*[1 << 30]float64)(p)[:int(sz)] +} + +func cAVStreamSlice(p unsafe.Pointer, sz C.int) []*AVStream { + if p == nil { + return nil + } + return (*[1 << 30]*AVStream)(p)[:int(sz)] +}