From 1af16fad330a913c4db3382b20675c2da5405e72 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sun, 23 Jun 2024 22:50:28 +0200 Subject: [PATCH 01/17] Try different packages --- .github/workflows/on_pull_request_merge.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/on_pull_request_merge.yaml b/.github/workflows/on_pull_request_merge.yaml index 76d5313..6bb3a34 100644 --- a/.github/workflows/on_pull_request_merge.yaml +++ b/.github/workflows/on_pull_request_merge.yaml @@ -41,5 +41,5 @@ jobs: go-version: ${{ matrix.go-version }} - name: Run tests run: | - sudo apt install -y libavcodec-dev libavdevice-dev libavfilter-dev libavutil-dev libswscale-dev libswresample-dev libchromaprint-dev + sudo apt install -y libavcodec60-dev libavdevice60-dev libavfilter-dev libavutil-dev libswscale-dev libswresample-dev libchromaprint-dev make test From c918b2806c9e21b400d851c8ad5c6e1e910fe930 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sun, 23 Jun 2024 22:54:34 +0200 Subject: [PATCH 02/17] Try new ubuntu --- .github/workflows/on_pull_request_merge.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/on_pull_request_merge.yaml b/.github/workflows/on_pull_request_merge.yaml index 6bb3a34..42c70c9 100644 --- a/.github/workflows/on_pull_request_merge.yaml +++ b/.github/workflows/on_pull_request_merge.yaml @@ -28,7 +28,7 @@ jobs: uses: github/codeql-action/analyze@v3 test: name: Test - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: matrix: go-version: [ '1.21', '1.22' ] From e906cfdc8c5c040a9a0a0bcf663acd241ff41fe4 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sun, 23 Jun 2024 22:57:21 +0200 Subject: [PATCH 03/17] try new paxkages --- .github/workflows/on_pull_request_merge.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/on_pull_request_merge.yaml b/.github/workflows/on_pull_request_merge.yaml index 42c70c9..eef4bcd 100644 --- a/.github/workflows/on_pull_request_merge.yaml +++ b/.github/workflows/on_pull_request_merge.yaml @@ -41,5 +41,5 @@ jobs: go-version: ${{ matrix.go-version }} - name: Run tests run: | - sudo apt install -y libavcodec60-dev libavdevice60-dev libavfilter-dev libavutil-dev libswscale-dev libswresample-dev libchromaprint-dev + sudo apt install -y libavcodec-dev libavdevice-dev libavfilter-dev libavutil-dev libswscale-dev libswresample-dev libchromaprint-dev make test From dc97548a5d2ed12b793dfe845834bd0b435c5f4b Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Mon, 24 Jun 2024 09:59:38 +0200 Subject: [PATCH 04/17] Added encoder --- encoder.go | 148 +++++++++++++++++++++++++++++++++++++++++++++++++ parameters.go | 12 ++-- stream.go | 22 +------- writer.go | 111 +++++++++++++++++++++++++++++++------ writer_test.go | 37 +++++++++++++ 5 files changed, 285 insertions(+), 45 deletions(-) create mode 100644 encoder.go create mode 100644 writer_test.go diff --git a/encoder.go b/encoder.go new file mode 100644 index 0000000..038243c --- /dev/null +++ b/encoder.go @@ -0,0 +1,148 @@ +package media + +import ( + + // Packages + "fmt" + + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" + + // Namespace imports + . "github.com/djthorpe/go-errors" +) + +//////////////////////////////////////////////////////////////////////////////// +// TYPES + +type encoder struct { + t MediaType + ctx *ff.AVCodecContext + stream *ff.AVStream + packet *ff.AVPacket +} + +//////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// Create an encoder with the given parameters +func newEncoder(ctx *ff.AVFormatContext, stream_id int, param Parameters) (*encoder, error) { + encoder := new(encoder) + par := param.(*par) + + // Get codec + codec_id := ff.AV_CODEC_ID_NONE + if param.Type().Is(CODEC) { + codec_id = par.codecpar.Codec + } else if par.Type().Is(AUDIO) { + codec_id = ctx.Output().AudioCodec() + } else if par.Type().Is(VIDEO) { + codec_id = ctx.Output().VideoCodec() + } else if par.Type().Is(SUBTITLE) { + codec_id = ctx.Output().SubtitleCodec() + } + if codec_id == ff.AV_CODEC_ID_NONE { + return nil, ErrBadParameter.With("no codec specified for stream") + } + + // Allocate codec + codec := ff.AVCodec_find_encoder(codec_id) + if codec == nil { + return nil, ErrBadParameter.Withf("codec %q cannot encode", codec_id) + } + codecctx := ff.AVCodec_alloc_context(codec) + if codecctx == nil { + return nil, ErrInternalAppError.With("could not allocate audio codec context") + } else { + encoder.ctx = codecctx + } + + // Create the stream + if stream := ff.AVFormat_new_stream(ctx, nil); stream == nil { + ff.AVCodec_free_context(codecctx) + return nil, ErrInternalAppError.With("could not allocate stream") + } else { + stream.SetId(stream_id) + encoder.stream = stream + } + + // Set parameters + switch codec.Type() { + case ff.AVMEDIA_TYPE_AUDIO: + encoder.t = AUDIO + // TODO: Check codec supports this configuration + + // Set codec parameters + if err := codecctx.SetChannelLayout(par.audiopar.Ch); err != nil { + ff.AVCodec_free_context(codecctx) + return nil, err + } + codecctx.SetSampleFormat(par.audiopar.SampleFormat) + codecctx.SetSampleRate(par.audiopar.Samplerate) + + // Set stream parameters + encoder.stream.SetTimeBase(ff.AVUtil_rational(1, par.audiopar.Samplerate)) + case ff.AVMEDIA_TYPE_VIDEO: + encoder.t = VIDEO + // TODO: Check codec supports this configuration + + // Set codec parameters + codecctx.SetPixFmt(par.videopar.PixelFormat) + codecctx.SetWidth(par.videopar.Width) + codecctx.SetHeight(par.videopar.Height) + + // Set stream parameters + encoder.stream.SetTimeBase(ff.AVUtil_rational_d2q(1/par.codecpar.Framerate, 1<<24)) + case ff.AVMEDIA_TYPE_SUBTITLE: + encoder.t = SUBTITLE + fmt.Println("TODO: Set encoding subtitle parameters") + default: + encoder.t = DATA + } + encoder.t |= OUTPUT + + // copy parameters to the stream + if err := ff.AVCodec_parameters_from_context(encoder.stream.CodecPar(), codecctx); err != nil { + ff.AVCodec_free_context(codecctx) + return nil, err + } + + // Some formats want stream headers to be separate. + if ctx.Flags().Is(ff.AVFMT_GLOBALHEADER) { + codecctx.SetFlags(codecctx.Flags() | ff.AV_CODEC_FLAG_GLOBAL_HEADER) + } + + // Open it + if err := ff.AVCodec_open(codecctx, codec, nil); err != nil { + ff.AVCodec_free_context(codecctx) + return nil, ErrInternalAppError.Withf("codec_open: %v", err) + } + + // Allocate packet + if packet := ff.AVCodec_packet_alloc(); packet == nil { + ff.AVCodec_free_context(codecctx) + return nil, ErrInternalAppError.With("could not allocate packet") + } else { + encoder.packet = packet + } + + // Return it + return encoder, nil +} + +func (encoder *encoder) Close() error { + // Free respurces + if encoder.packet != nil { + ff.AVCodec_packet_free(encoder.packet) + } + if encoder.ctx != nil { + ff.AVCodec_free_context(encoder.ctx) + } + + // Release resources + encoder.stream = nil + encoder.packet = nil + encoder.ctx = nil + + // Return success + return nil +} diff --git a/parameters.go b/parameters.go index c2b379b..b1660ab 100644 --- a/parameters.go +++ b/parameters.go @@ -16,11 +16,15 @@ type par struct { t MediaType audiopar videopar + codecpar planepar } type codecpar struct { - Framerate ff.AVRational + Codec ff.AVCodecID `json:"codec"` + + // For video (in fps) + Framerate float64 `json:"framerate"` } type audiopar struct { @@ -39,12 +43,6 @@ type planepar struct { NumPlanes int `json:"num_video_planes"` } -type timingpar struct { - Framerate ff.AVRational `json:"framerate"` - Pts int64 `json:"pts"` - TimeBase ff.AVRational `json:"time_base"` -} - var _ Parameters = (*par)(nil) //////////////////////////////////////////////////////////////////////////////// diff --git a/stream.go b/stream.go index d8113d2..044814c 100644 --- a/stream.go +++ b/stream.go @@ -13,7 +13,8 @@ type stream struct { } type writerstream struct { - *ff.AVStream + t MediaType + codec *ff.AVCodec } var _ Stream = (*stream)(nil) @@ -28,25 +29,6 @@ func newStream(ctx *ff.AVStream) *stream { return &stream{ctx} } -/* -// Stream wrapper for encoding -func newWriterStream(ctx *ff.AVFormatContext, param Parameters) (*writerstream, error) { - // Parameters - Codec - var codec_id ff.AVCodecID - if param.Type().Is(CODEC) { - codec_id = param.Codec().ID() - } else if param.Type().Is(VIDEO) { - codec_id = ctx.Input().VideoCodec() - } else if param.Type().Is(AUDIO) { - codec_id = ctx.Input().AudioCodec() - } else { - return nil, ErrBadParameter.With("invalid stream parameters") - - } - - return nil, ErrNotImplemented -} -*/ //////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS diff --git a/writer.go b/writer.go index 64c79d8..c919ee4 100644 --- a/writer.go +++ b/writer.go @@ -1,7 +1,9 @@ package media import ( + "encoding/json" "errors" + "fmt" "io" // Packages @@ -15,9 +17,12 @@ import ( // TYPES type writer struct { - t MediaType - output *ff.AVFormatContext - avio *ff.AVIOContextEx + t MediaType + output *ff.AVFormatContext + avio *ff.AVIOContextEx + metadata *ff.AVDictionary + header bool + encoder map[int]*encoder } type writer_callback struct { @@ -33,6 +38,12 @@ var _ Media = (*writer)(nil) func createMedia(url string, format Format, metadata []Metadata, params ...Parameters) (*writer, error) { writer := new(writer) writer.t = OUTPUT + writer.encoder = make(map[int]*encoder, len(params)) + + // If there are no streams, then return an error + if len(params) == 0 { + return nil, ErrBadParameter.With("no streams specified for encoder") + } // Guess the output format var ofmt *ff.AVOutputFormat @@ -53,17 +64,22 @@ func createMedia(url string, format Format, metadata []Metadata, params ...Param writer.output = ctx } - // Add streams - /* - for _, param := range params { - stream, err := newWriterStream(ctx, param) - if err != nil { - return nil, errors.Join(err, writer.Close()) - } else { - fmt.Println("TODO: STREAM", stream) - } + // Add encoders and streams + var result error + for i, param := range params { + encoder, err := newEncoder(ctx, i, param) + if err != nil { + result = errors.Join(result, err) + } else { + writer.encoder[i] = encoder } - */ + } + + // Return any errors from creating the streams + if result != nil { + return nil, errors.Join(result, writer.Close()) + } + // Open the output file, if needed if !ctx.Flags().Is(ff.AVFMT_NOFILE) { w, err := ff.AVFormat_avio_open(url, ff.AVIO_FLAG_WRITE) @@ -75,9 +91,32 @@ func createMedia(url string, format Format, metadata []Metadata, params ...Param } } - // TODO: Metadata + // Set metadata + if len(metadata) > 0 { + writer.metadata = ff.AVUtil_dict_alloc() + if writer.metadata == nil { + return nil, errors.Join(errors.New("unable to allocate metadata dictionary"), writer.Close()) + } + for _, m := range metadata { + // Ignore duration and artwork fields + key := m.Key() + if key == MetaArtwork || key == MetaDuration { + continue + } + // Set dictionary entry + if err := ff.AVUtil_dict_set(writer.metadata, key, fmt.Sprint(m.Value()), ff.AV_DICT_APPEND); err != nil { + return nil, errors.Join(err, writer.Close()) + } + } + // TODO: Create artwork streams + } - // TODO: Write the header + // Write the header + if err := ff.AVFormat_write_header(ctx, nil); err != nil { + return nil, errors.Join(err, writer.Close()) + } else { + writer.header = true + } // Return success return writer, nil @@ -92,18 +131,54 @@ func createWriter(w io.Writer, format Format, metadata []Metadata, params ...Par func (w *writer) Close() error { var result error - // TODO: Write the trailer + // Write the trailer if the header was written + if w.header { + if err := ff.AVFormat_write_trailer(w.output); err != nil { + result = errors.Join(result, err) + } + } + + // Close encoders + for _, encoder := range w.encoder { + result = errors.Join(result, encoder.Close()) + } // Free resources + if w.metadata != nil { + ff.AVUtil_dict_free(w.metadata) + } + if w.output != nil { + result = errors.Join(result, ff.AVFormat_close_writer(w.output)) + } if w.avio != nil { - result = errors.Join(result, ff.AVFormat_avio_close(w.avio)) + fmt.Println("TODO AVIO") + // result = errors.Join(result, ff.AVFormat_avio_close(w.avio)) } - result = errors.Join(result, ff.AVFormat_close_writer(w.output)) + + // Release resources + w.encoder = nil + w.metadata = nil + w.avio = nil + w.output = nil // Return any errors return result } +//////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +// Display the reader as a string +func (w *writer) MarshalJSON() ([]byte, error) { + return json.Marshal(w.output) +} + +// Display the reader as a string +func (w *writer) String() string { + data, _ := json.MarshalIndent(w, "", " ") + return string(data) +} + //////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS diff --git a/writer_test.go b/writer_test.go new file mode 100644 index 0000000..589575b --- /dev/null +++ b/writer_test.go @@ -0,0 +1,37 @@ +package media_test + +import ( + "path/filepath" + "strings" + "testing" + + // Package imports + "github.com/stretchr/testify/assert" + + // Namespace imports + . "github.com/mutablelogic/go-media" +) + +func Test_writer_001(t *testing.T) { + assert := assert.New(t) + manager, err := NewManager(OptLog(true, func(v string) { + t.Log(strings.TrimSpace(v)) + })) + if !assert.NoError(err) { + t.SkipNow() + } + + // Write audio file + filename := filepath.Join(t.TempDir(), t.Name()+".sw") + stream, err := manager.AudioParameters("mono", "s16", 22050) + if !assert.NoError(err) { + t.SkipNow() + } + + writer, err := manager.Create(filename, nil, nil, stream) + if !assert.NoError(err) { + t.SkipNow() + } + defer writer.Close() + t.Log(writer) +} From 5db5f2b713ac2153ec121cb7f8654807befe79cc Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Mon, 24 Jun 2024 10:27:41 +0200 Subject: [PATCH 05/17] Start of mux method --- frame.go | 5 ++++ interfaces.go | 13 ++++++++++ parameters.go | 8 ++++++ reader.go | 8 ++++++ writer.go | 66 ++++++++++++++++++++++++++++++++++++++++++-------- writer_test.go | 4 +-- 6 files changed, 92 insertions(+), 12 deletions(-) diff --git a/frame.go b/frame.go index 12321c0..512be63 100644 --- a/frame.go +++ b/frame.go @@ -71,6 +71,11 @@ func (frame *frame) Type() MediaType { return NONE } +// Id is unused +func (frame *frame) Id() int { + return 0 +} + // Return the timestamp as a duration, or minus one if not set func (frame *frame) Time() time.Duration { pts := frame.ctx.Pts() diff --git a/interfaces.go b/interfaces.go index fe17bc5..ddee065 100644 --- a/interfaces.go +++ b/interfaces.go @@ -140,8 +140,14 @@ type Media interface { // Return a decoding context for the media stream, and // map the streams to decoders. If no function is provided // (ie, the argument is nil) then all streams are demultiplexed. + // Will return an error if called on a writer. Decoder(DecoderMapFunc) (Decoder, error) + // Multiplex media into packets. Pass a packet to a muxer function. + // Stop when the context is cancelled or the end of the media stream is + // signalled. Will return an error if called on a reader. + Mux(context.Context, MuxFunc) error + // Return INPUT for a demuxer or source, OUTPUT for a muxer or // sink, DEVICE for a device, FILE for a file or stream. Type() MediaType @@ -191,6 +197,9 @@ type Parameters interface { // Return the media type (AUDIO, VIDEO, SUBTITLE, DATA) Type() MediaType + // Return the stream id for encoding, or zero if not set + Id() int + // Return number of planes for a specific PixelFormat // or SampleFormat and ChannelLayout combination NumPlanes() int @@ -224,6 +233,10 @@ type VideoParameters interface { // io.EOF if you want to stop processing the packets early. type DecoderFunc func(Packet) error +// MuxFunc is a function that multiplexes a packet. Return +// io.EOF to stop multiplexing normally. +type MuxFunc func(Packet) error + // FrameFunc is a function that processes a frame of audio // or video data. Return io.EOF if you want to stop // processing the frames early. diff --git a/parameters.go b/parameters.go index b1660ab..0fd4c5c 100644 --- a/parameters.go +++ b/parameters.go @@ -23,6 +23,9 @@ type par struct { type codecpar struct { Codec ff.AVCodecID `json:"codec"` + // Stream Id + StreamId int `json:"stream_id"` + // For video (in fps) Framerate float64 `json:"framerate"` } @@ -168,6 +171,11 @@ func (par *par) Type() MediaType { return par.t } +// Return stream id +func (par *par) Id() int { + return par.codecpar.StreamId +} + // Return number of planes for a specific PixelFormat // or SampleFormat and ChannelLayout combination func (par *par) NumPlanes() int { diff --git a/reader.go b/reader.go index 5bf780b..cf5079e 100644 --- a/reader.go +++ b/reader.go @@ -1,6 +1,7 @@ package media import ( + "context" "encoding/json" "errors" "io" @@ -9,6 +10,9 @@ import ( // Packages ff "github.com/mutablelogic/go-media/sys/ffmpeg61" + + // Namespace imports + . "github.com/djthorpe/go-errors" ) //////////////////////////////////////////////////////////////////////////////// @@ -229,6 +233,10 @@ func (r *reader) Metadata(keys ...string) []Metadata { return result } +func (r *reader) Mux(context.Context, MuxFunc) error { + return ErrOutOfOrder.With("not an output stream") +} + //////////////////////////////////////////////////////////////////////////////// // PRIVATE METHODS diff --git a/writer.go b/writer.go index c919ee4..2e7d297 100644 --- a/writer.go +++ b/writer.go @@ -1,6 +1,7 @@ package media import ( + "context" "encoding/json" "errors" "fmt" @@ -67,11 +68,18 @@ func createMedia(url string, format Format, metadata []Metadata, params ...Param // Add encoders and streams var result error for i, param := range params { - encoder, err := newEncoder(ctx, i, param) + // Stream Id from codec parameters, or use the index + stream_id := param.Id() + if stream_id <= 0 { + stream_id = i + 1 + } + encoder, err := newEncoder(ctx, stream_id, param) if err != nil { result = errors.Join(result, err) + } else if _, exists := writer.encoder[stream_id]; exists { + } else { - writer.encoder[i] = encoder + writer.encoder[stream_id] = encoder } } @@ -143,16 +151,15 @@ func (w *writer) Close() error { result = errors.Join(result, encoder.Close()) } - // Free resources - if w.metadata != nil { - ff.AVUtil_dict_free(w.metadata) - } + // Free output resources if w.output != nil { + // This calls avio_close(w.avio) result = errors.Join(result, ff.AVFormat_close_writer(w.output)) } - if w.avio != nil { - fmt.Println("TODO AVIO") - // result = errors.Join(result, ff.AVFormat_avio_close(w.avio)) + + // Free resources + if w.metadata != nil { + ff.AVUtil_dict_free(w.metadata) } // Release resources @@ -183,7 +190,46 @@ func (w *writer) String() string { // PUBLIC METHODS func (w *writer) Decoder(DecoderMapFunc) (Decoder, error) { - return nil, ErrNotImplemented + return nil, ErrOutOfOrder.With("not an input stream") +} + +func (w *writer) Mux(context.Context, MuxFunc) error { + return ErrNotImplemented + + /* + while (1) { + AVStream *in_stream, *out_stream; + + ret = av_read_frame(ifmt_ctx, pkt); + if (ret < 0) + break; + + in_stream = ifmt_ctx->streams[pkt->stream_index]; + if (pkt->stream_index >= stream_mapping_size || + stream_mapping[pkt->stream_index] < 0) { + av_packet_unref(pkt); + continue; + } + + pkt->stream_index = stream_mapping[pkt->stream_index]; + out_stream = ofmt_ctx->streams[pkt->stream_index]; + log_packet(ifmt_ctx, pkt, "in"); + + // copy packet + av_packet_rescale_ts(pkt, in_stream->time_base, out_stream->time_base); + pkt->pos = -1; + log_packet(ofmt_ctx, pkt, "out"); + + ret = av_interleaved_write_frame(ofmt_ctx, pkt); + // pkt is now blank (av_interleaved_write_frame() takes ownership of + // its contents and resets pkt), so that no unreferencing is necessary. + // This would be different if one used av_write_frame(). + if (ret < 0) { + fprintf(stderr, "Error muxing packet\n"); + break; + } + } + */ } // Return OUTPUT and combination of DEVICE and STREAM diff --git a/writer_test.go b/writer_test.go index 589575b..0338b96 100644 --- a/writer_test.go +++ b/writer_test.go @@ -22,7 +22,7 @@ func Test_writer_001(t *testing.T) { } // Write audio file - filename := filepath.Join(t.TempDir(), t.Name()+".sw") + filename := filepath.Join(t.TempDir(), t.Name()+".mp3") stream, err := manager.AudioParameters("mono", "s16", 22050) if !assert.NoError(err) { t.SkipNow() @@ -33,5 +33,5 @@ func Test_writer_001(t *testing.T) { t.SkipNow() } defer writer.Close() - t.Log(writer) + t.Log(writer, "=>", filename) } From 0975c7812874292cc931be582a2d4aa0865a783e Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Mon, 24 Jun 2024 10:45:19 +0200 Subject: [PATCH 06/17] Updated docs --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7e06492..d1bd08f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ This module provides an interface for media services, including: -* Bindings in golang for [ffmpeg 6](https://ffmpeg.org/); +* Bindings in golang for [FFmpeg 6.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; @@ -235,8 +235,8 @@ 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: -> go-media -> https://github.com/mutablelogic/go-media/ +> go-media\ +> https://github.com/mutablelogic/go-media/\ > Copyright (c) 2021-2024 David Thorpe, All rights reserved. This software links to shared libraries of [FFmpeg](http://ffmpeg.org/) licensed under From 4c75dd439c8961753116411c745662d1c645170c Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Mon, 24 Jun 2024 10:47:12 +0200 Subject: [PATCH 07/17] Updated docs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d1bd08f..babf3f3 100644 --- a/README.md +++ b/README.md @@ -235,8 +235,8 @@ 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: -> go-media\ -> https://github.com/mutablelogic/go-media/\ +> __go-media__\ +> [https://github.com/mutablelogic/go-media/](https://github.com/mutablelogic/go-media/)\ > Copyright (c) 2021-2024 David Thorpe, All rights reserved. This software links to shared libraries of [FFmpeg](http://ffmpeg.org/) licensed under From 36e8b19b471103082a046709427054e6afcdab5d Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Mon, 24 Jun 2024 11:05:09 +0200 Subject: [PATCH 08/17] Updated interfaces --- interfaces.go | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/interfaces.go b/interfaces.go index ddee065..f5bc5a2 100644 --- a/interfaces.go +++ b/interfaces.go @@ -33,7 +33,7 @@ type Manager interface { Create(string, Format, []Metadata, ...Parameters) (Media, error) // Create a media stream for writing. The format will be used to - // determine the formar type and one or more CodecParameters used to + // determine the format and one or more CodecParameters used to // create the streams. If no parameters are provided, then the // default parameters for the format are used. It is the responsibility // of the caller to also close the writer when done. @@ -233,9 +233,12 @@ type VideoParameters interface { // io.EOF if you want to stop processing the packets early. type DecoderFunc func(Packet) error -// MuxFunc is a function that multiplexes a packet. Return -// io.EOF to stop multiplexing normally. -type MuxFunc func(Packet) error +// MuxFunc is a function that multiplexes a packet. It is +// repeatedly called with a stream identifier - return a packet +// for that stream if one is available, or nil if no +// packet is available for muxing. Return io.EOF to +// stop multiplexing. +type MuxFunc func(int) (Packet, error) // FrameFunc is a function that processes a frame of audio // or video data. Return io.EOF if you want to stop @@ -254,9 +257,15 @@ type Codec interface { Type() MediaType } -// Packet represents a packet of demultiplexed data. -// Currently this is quite opaque! -type Packet interface{} +// Packet represents a packet of demultiplexed data, or a packet +// to be multiplexed. +type Packet interface { + // The packet can be audio, video, subtitle or data. + Type() MediaType + + // The stream identifier for the packet + Id() int +} // Frame represents a frame of audio or video data. type Frame interface { From 200f3bc51a6ede242a0fa2fcf4ff9bc9aa7f3943 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Tue, 25 Jun 2024 08:43:36 +0200 Subject: [PATCH 09/17] Updated muxing code --- encoder.go | 10 +++ sys/ffmpeg61/avcodec_encoding.go | 12 +++ writer.go | 138 ++++++++++++++++++++++--------- 3 files changed, 123 insertions(+), 37 deletions(-) diff --git a/encoder.go b/encoder.go index 038243c..1f1245b 100644 --- a/encoder.go +++ b/encoder.go @@ -146,3 +146,13 @@ func (encoder *encoder) Close() error { // Return success return nil } + +//////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +func (encoder *encoder) encode(fn MuxFunc) (*ff.AVPacket, error) { + packet, err := fn(encoder.stream.Id()) + if packet != nil { + } + +} diff --git a/sys/ffmpeg61/avcodec_encoding.go b/sys/ffmpeg61/avcodec_encoding.go index 1ecf7c1..690413b 100644 --- a/sys/ffmpeg61/avcodec_encoding.go +++ b/sys/ffmpeg61/avcodec_encoding.go @@ -50,3 +50,15 @@ func AVCodec_receive_packet(ctx *AVCodecContext, pkt *AVPacket) error { } return nil } + +// Write a packet to an output media file ensuring correct interleaving. +// This function will buffer the packets internally as needed to make sure the packets in the output file are +// properly interleaved, usually ordered by increasing dts. Callers doing their own interleaving should +// call av_write_frame() instead of this function. +func AVCodec_interleaved_write_frame(ctx *AVFormatContext, pkt *AVPacket) error { + if err := AVError(C.av_interleaved_write_frame((*C.AVFormatContext)(ctx), (*C.AVPacket)(pkt))); err != 0 { + return err + } + // Return success + return nil +} diff --git a/writer.go b/writer.go index 2e7d297..af855a2 100644 --- a/writer.go +++ b/writer.go @@ -193,45 +193,109 @@ func (w *writer) Decoder(DecoderMapFunc) (Decoder, error) { return nil, ErrOutOfOrder.With("not an input stream") } -func (w *writer) Mux(context.Context, MuxFunc) error { - return ErrNotImplemented - - /* - while (1) { - AVStream *in_stream, *out_stream; - - ret = av_read_frame(ifmt_ctx, pkt); - if (ret < 0) - break; - - in_stream = ifmt_ctx->streams[pkt->stream_index]; - if (pkt->stream_index >= stream_mapping_size || - stream_mapping[pkt->stream_index] < 0) { - av_packet_unref(pkt); - continue; - } - - pkt->stream_index = stream_mapping[pkt->stream_index]; - out_stream = ofmt_ctx->streams[pkt->stream_index]; - log_packet(ifmt_ctx, pkt, "in"); - - // copy packet - av_packet_rescale_ts(pkt, in_stream->time_base, out_stream->time_base); - pkt->pos = -1; - log_packet(ofmt_ctx, pkt, "out"); - - ret = av_interleaved_write_frame(ofmt_ctx, pkt); - // pkt is now blank (av_interleaved_write_frame() takes ownership of - // its contents and resets pkt), so that no unreferencing is necessary. - // This would be different if one used av_write_frame(). - if (ret < 0) { - fprintf(stderr, "Error muxing packet\n"); - break; - } - } - */ +func (w *writer) Mux(ctx context.Context, fn MuxFunc) error { + // Check fn + if fn == nil { + return ErrBadParameter.With("nil mux function") + } + + // Create a new map of encoders + encoders := make(map[int]*encoder, len(w.encoder)) + for k, v := range w.encoder { + encoders[k] = v + } + +FOR_LOOP: + for { + select { + case <-ctx.Done(): + break FOR_LOOP + default: + // Loop until no more encoders are available to send packets + if len(encoders) == 0 { + break FOR_LOOP + } + + // Find the first encoder which should return a packet + next_time := 0 + next_stream := 0 + for stream := range encoders { + if next_time == 0 || encoder.nextTime() < next_time { + next_time = encoder.nextTime() + next_stream = stream + } + } + + // Get a packet from the encoder + packet, err := encoders[next_stream].encode(fn) + if errors.Is(err, io.EOF) { + break FOR_LOOP + } else if err != nil { + return err + } else if packet == nil { + // Remove the encoder from the map + delete(encoders, next_stream) + continue FOR_LOOP + } + + // Send the packet to the muxer + //av_packet_rescale_ts(pkt, in_stream->time_base, out_stream->time_base); + // Packet's stream_index field must be set to the index of the corresponding stream in s->streams. + // The timestamps (pts, dts) must be set to correct values in the stream's timebase + // (unless the output format is flagged with the AVFMT_NOTIMESTAMPS flag, then they can be set + // to AV_NOPTS_VALUE). The dts for subsequent packets in one stream must be strictly increasing + // (unless the output format is flagged with the AVFMT_TS_NONSTRICT, then they merely have to + // be nondecreasing). duration should also be set if known. + if err := ff.AVCodec_interleaved_write_frame(w.output, packet); err != nil { + return err + } + } + } + + // Flush + if err := ff.AVCodec_interleaved_write_frame(w.output, nil); err != nil { + return err + } + + // Return the context error, which will be nil if the loop ended normally + return ctx.Err() } +/* + while (1) { + AVStream *in_stream, *out_stream; + + ret = av_read_frame(ifmt_ctx, pkt); + if (ret < 0) + break; + + in_stream = ifmt_ctx->streams[pkt->stream_index]; + if (pkt->stream_index >= stream_mapping_size || + stream_mapping[pkt->stream_index] < 0) { + av_packet_unref(pkt); + continue; + } + + pkt->stream_index = stream_mapping[pkt->stream_index]; + out_stream = ofmt_ctx->streams[pkt->stream_index]; + log_packet(ifmt_ctx, pkt, "in"); + + // copy packet + av_packet_rescale_ts(pkt, in_stream->time_base, out_stream->time_base); + pkt->pos = -1; + log_packet(ofmt_ctx, pkt, "out"); + + ret = av_interleaved_write_frame(ofmt_ctx, pkt); + // pkt is now blank (av_interleaved_write_frame() takes ownership of + // its contents and resets pkt), so that no unreferencing is necessary. + // This would be different if one used av_write_frame(). + if (ret < 0) { + fprintf(stderr, "Error muxing packet\n"); + break; + } + } +*/ + // Return OUTPUT and combination of DEVICE and STREAM func (w *writer) Type() MediaType { return OUTPUT From 8b30ed0b28f6f4bf2e407c833d973f022343ab20 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Tue, 25 Jun 2024 21:40:16 +0200 Subject: [PATCH 10/17] Adding param checking for encoder --- encoder.go | 37 ++++++++++++++++++++++------ sys/ffmpeg61/avcodec_core.go | 47 +++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/encoder.go b/encoder.go index 1f1245b..c11b824 100644 --- a/encoder.go +++ b/encoder.go @@ -69,24 +69,47 @@ func newEncoder(ctx *ff.AVFormatContext, stream_id int, param Parameters) (*enco switch codec.Type() { case ff.AVMEDIA_TYPE_AUDIO: encoder.t = AUDIO - // TODO: Check codec supports this configuration - // Set codec parameters - if err := codecctx.SetChannelLayout(par.audiopar.Ch); err != nil { + // Choose sample format + if sampleformat, err := ff.AVCodec_supported_sampleformat(codec, par.audiopar.SampleFormat); err != nil { + ff.AVCodec_free_context(codecctx) + return nil, err + } else { + codecctx.SetSampleFormat(sampleformat) + } + + // Choose sample rate + if samplerate, err := ff.AVCodec_supported_samplerate(codec, par.audiopar.Samplerate); err != nil { + ff.AVCodec_free_context(codecctx) + return nil, err + } else { + codecctx.SetSampleRate(samplerate) + } + + // Choose channel layout + if channellayout, err := ff.AVCodec_supported_channellayout(codec, par.audiopar.Ch); err != nil { + ff.AVCodec_free_context(codecctx) + return nil, err + } else if err := codecctx.SetChannelLayout(channellayout); err != nil { ff.AVCodec_free_context(codecctx) return nil, err } - codecctx.SetSampleFormat(par.audiopar.SampleFormat) - codecctx.SetSampleRate(par.audiopar.Samplerate) // Set stream parameters encoder.stream.SetTimeBase(ff.AVUtil_rational(1, par.audiopar.Samplerate)) + case ff.AVMEDIA_TYPE_VIDEO: encoder.t = VIDEO - // TODO: Check codec supports this configuration + + // Choose pixel format + if pixelformat, err := ff.AVCodec_supported_pixelformat(codec, par.videopar.PixelFormat); err != nil { + ff.AVCodec_free_context(codecctx) + return nil, err + } else { + codecctx.SetPixFmt(pixelformat) + } // Set codec parameters - codecctx.SetPixFmt(par.videopar.PixelFormat) codecctx.SetWidth(par.videopar.Width) codecctx.SetHeight(par.videopar.Height) diff --git a/sys/ffmpeg61/avcodec_core.go b/sys/ffmpeg61/avcodec_core.go index d42bac2..e6f1f1f 100644 --- a/sys/ffmpeg61/avcodec_core.go +++ b/sys/ffmpeg61/avcodec_core.go @@ -1,6 +1,9 @@ package ffmpeg -import "unsafe" +import ( + "fmt" + "unsafe" +) //////////////////////////////////////////////////////////////////////////////// // CGO @@ -99,3 +102,45 @@ func AVCodec_is_encoder(codec *AVCodec) bool { func AVCodec_is_decoder(codec *AVCodec) bool { return C.av_codec_is_decoder((*C.struct_AVCodec)(codec)) != 0 } + +// Return a supported sample format that is closest to the given sample format. +func AVCodec_supported_sampleformat(codec *AVCodec, samplefmt AVSampleFormat) (AVSampleFormat, error) { + for _, fmt := range codec.SampleFormats() { + if fmt == samplefmt { + return samplefmt, nil + } + } + return AVSampleFormat(AV_SAMPLE_FMT_NONE), fmt.Errorf("sample format %v is not supported by codec %q", samplefmt, codec.Name()) +} + +// Return a supported sample rate that is closest to the given sample rate. +func AVCodec_supported_samplerate(codec *AVCodec, samplerate int) (int, error) { + max := 0 + for _, rate := range codec.SupportedSamplerates() { + if rate == samplerate { + return samplerate, nil + } + if rate > max { + max = rate + } + } + if max > 0 { + return max, nil + } else { + return 0, fmt.Errorf("sample rate %v is not supported by codec %q", samplerate, codec.Name()) + } +} + +// Return a supported channel layout that is closest to the given channel layout. +func AVCodec_supported_channellayout(codec *AVCodec, channellayout AVChannelLayout) (AVChannelLayout, error) { + for _, layout := range codec.ChannelLayouts() { + if C.av_channel_layout_compare(&layout, &channellayout) == 0 { + return channellayout, nil + } + } +} + +// Return a supported pixel format that is closest to the given pixel format. +func AVCodec_supported_pixelformat(AVCodec *codec, AVPixelFormat pixelformat) (AVPixelFormat, error) { + +} From 5181acb29c3ea488d30bbf99fd2bbc3fc6ab800f Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Wed, 26 Jun 2024 09:13:52 +0200 Subject: [PATCH 11/17] Updated encoder --- encoder.go | 44 +++++++++++++++++--------------- packet.go | 42 ++++++++++++++++++++++-------- sys/ffmpeg61/avcodec_core.go | 31 +++++++++++++++++----- sys/ffmpeg61/avcodec_encoding.go | 3 ++- writer.go | 24 ++++++++++++----- 5 files changed, 99 insertions(+), 45 deletions(-) diff --git a/encoder.go b/encoder.go index c11b824..16d1254 100644 --- a/encoder.go +++ b/encoder.go @@ -4,6 +4,7 @@ import ( // Packages "fmt" + "io" ff "github.com/mutablelogic/go-media/sys/ffmpeg61" @@ -15,10 +16,11 @@ import ( // TYPES type encoder struct { - t MediaType - ctx *ff.AVCodecContext - stream *ff.AVStream - packet *ff.AVPacket + t MediaType + ctx *ff.AVCodecContext + stream *ff.AVStream + packet *ff.AVPacket + next_pts int64 } //////////////////////////////////////////////////////////////////////////////// @@ -78,19 +80,22 @@ func newEncoder(ctx *ff.AVFormatContext, stream_id int, param Parameters) (*enco codecctx.SetSampleFormat(sampleformat) } - // Choose sample rate - if samplerate, err := ff.AVCodec_supported_samplerate(codec, par.audiopar.Samplerate); err != nil { - ff.AVCodec_free_context(codecctx) - return nil, err - } else { - codecctx.SetSampleRate(samplerate) - } + // TODO Choose sample rate + codecctx.SetSampleRate(par.audiopar.Samplerate) - // Choose channel layout - if channellayout, err := ff.AVCodec_supported_channellayout(codec, par.audiopar.Ch); err != nil { - ff.AVCodec_free_context(codecctx) - return nil, err - } else if err := codecctx.SetChannelLayout(channellayout); err != nil { + // TODO + //if samplerate, err := ff.AVCodec_supported_samplerate(codec, par.audiopar.Samplerate); err != nil { + // ff.AVCodec_free_context(codecctx) + // return nil, err + //} + + // TODO Choose channel layout + //if channellayout, err := ff.AVCodec_supported_channellayout(codec, par.audiopar.Ch); err != nil { + // ff.AVCodec_free_context(codecctx) + // return nil, err + //} + + if err := codecctx.SetChannelLayout(par.audiopar.Ch); err != nil { ff.AVCodec_free_context(codecctx) return nil, err } @@ -174,8 +179,7 @@ func (encoder *encoder) Close() error { // PRIVATE METHODS func (encoder *encoder) encode(fn MuxFunc) (*ff.AVPacket, error) { - packet, err := fn(encoder.stream.Id()) - if packet != nil { - } - + // TODO + fmt.Println("TODO: encode - get packet") + return nil, io.EOF } diff --git a/packet.go b/packet.go index a70ab0a..47c3703 100644 --- a/packet.go +++ b/packet.go @@ -11,13 +11,13 @@ import ( // TYPES type packetmeta struct { - StreamIndex int `json:"stream_index" writer:",width:10,right"` - MediaType ff.AVMediaType `json:"media_type" writer:",width:20"` - Size int `json:"size,omitempty" writer:",width:7,right"` - Pts ff.AVTimestamp `json:"pts,omitempty" writer:",width:9,right"` - TimeBase ff.AVRational `json:"time_base,omitempty" writer:",width:10,right"` - Duration ff.AVTimestamp `json:"duration,omitempty" writer:",width:10,right"` - Pos *int64 `json:"pos,omitempty" writer:",width:10,right"` + Stream int `json:"stream" writer:",width:10,right"` + MediaType ff.AVMediaType `json:"media_type" writer:",width:20"` + Size int `json:"size,omitempty" writer:",width:7,right"` + Pts int64 `json:"pts,omitempty" writer:",width:9,right"` + TimeBase ff.AVRational `json:"time_base,omitempty" writer:",width:10,right"` + Duration int64 `json:"duration,omitempty" writer:",width:10,right"` + Pos *int64 `json:"pos,omitempty" writer:",width:10,right"` } type packet struct { @@ -34,15 +34,15 @@ func newPacket(ctx *ff.AVPacket, stream int, t ff.AVMediaType, timeBase ff.AVRat pkt := &packet{ ctx: ctx, packetmeta: packetmeta{ - StreamIndex: stream, - MediaType: t, + Stream: stream, + MediaType: t, }, } if ctx != nil { pkt.packetmeta.Size = ctx.Size() - pkt.packetmeta.Pts = ff.AVTimestamp(ctx.Pts()) + pkt.packetmeta.Pts = ctx.Pts() pkt.packetmeta.TimeBase = timeBase - pkt.packetmeta.Duration = ff.AVTimestamp(ctx.Duration()) + pkt.packetmeta.Duration = ctx.Duration() if ctx.Pos() != -1 { pos := ctx.Pos() pkt.packetmeta.Pos = &pos @@ -62,3 +62,23 @@ func (packet *packet) String() string { data, _ := json.MarshalIndent(packet, "", " ") return string(data) } + +//////////////////////////////////////////////////////////////////////////////// +// PROPERTIES + +func (packet *packet) Id() int { + return packet.packetmeta.Stream +} + +func (packet *packet) Type() MediaType { + switch packet.packetmeta.MediaType { + case ff.AVMEDIA_TYPE_AUDIO: + return AUDIO + case ff.AVMEDIA_TYPE_VIDEO: + return VIDEO + case ff.AVMEDIA_TYPE_SUBTITLE: + return SUBTITLE + default: + return DATA + } +} diff --git a/sys/ffmpeg61/avcodec_core.go b/sys/ffmpeg61/avcodec_core.go index e6f1f1f..dbee1ea 100644 --- a/sys/ffmpeg61/avcodec_core.go +++ b/sys/ffmpeg61/avcodec_core.go @@ -105,14 +105,35 @@ func AVCodec_is_decoder(codec *AVCodec) bool { // Return a supported sample format that is closest to the given sample format. func AVCodec_supported_sampleformat(codec *AVCodec, samplefmt AVSampleFormat) (AVSampleFormat, error) { - for _, fmt := range codec.SampleFormats() { + first := AV_SAMPLE_FMT_NONE + for i, fmt := range codec.SampleFormats() { if fmt == samplefmt { return samplefmt, nil } + if i == 0 { + first = fmt + } + } + // Return an error and the first supported sample format + return first, fmt.Errorf("sample format %v is not supported by codec %q", samplefmt, codec.Name()) +} + +// Return a supported pixel format that is closest to the given pixel format. +func AVCodec_supported_pixelformat(codec *AVCodec, pixelfmt AVPixelFormat) (AVPixelFormat, error) { + first := AV_PIX_FMT_NONE + for i, fmt := range codec.PixelFormats() { + if fmt == pixelfmt { + return pixelfmt, nil + } + if i == 0 { + first = fmt + } } - return AVSampleFormat(AV_SAMPLE_FMT_NONE), fmt.Errorf("sample format %v is not supported by codec %q", samplefmt, codec.Name()) + // Return an error and the first supported sample format + return first, fmt.Errorf("pixel format %v is not supported by codec %q", pixelfmt, codec.Name()) } +/* // Return a supported sample rate that is closest to the given sample rate. func AVCodec_supported_samplerate(codec *AVCodec, samplerate int) (int, error) { max := 0 @@ -139,8 +160,4 @@ func AVCodec_supported_channellayout(codec *AVCodec, channellayout AVChannelLayo } } } - -// Return a supported pixel format that is closest to the given pixel format. -func AVCodec_supported_pixelformat(AVCodec *codec, AVPixelFormat pixelformat) (AVPixelFormat, error) { - -} +*/ diff --git a/sys/ffmpeg61/avcodec_encoding.go b/sys/ffmpeg61/avcodec_encoding.go index 690413b..b3288e5 100644 --- a/sys/ffmpeg61/avcodec_encoding.go +++ b/sys/ffmpeg61/avcodec_encoding.go @@ -9,8 +9,9 @@ import ( // CGO /* -#cgo pkg-config: libavcodec +#cgo pkg-config: libavcodec libavformat #include +#include #include */ import "C" diff --git a/writer.go b/writer.go index af855a2..ed76dc9 100644 --- a/writer.go +++ b/writer.go @@ -217,17 +217,24 @@ FOR_LOOP: } // Find the first encoder which should return a packet - next_time := 0 - next_stream := 0 - for stream := range encoders { - if next_time == 0 || encoder.nextTime() < next_time { - next_time = encoder.nextTime() + var next_encoder *encoder + var next_stream int + for stream, encoder := range encoders { + // Initialise the next encoder + if next_encoder == nil { + next_encoder = encoder + next_stream = stream + continue + } + // Compare + if !compareNextPts(next_encoder, encoder) { + next_encoder = encoder next_stream = stream } } // Get a packet from the encoder - packet, err := encoders[next_stream].encode(fn) + packet, err := next_encoder.encode(fn) if errors.Is(err, io.EOF) { break FOR_LOOP } else if err != nil { @@ -261,6 +268,11 @@ FOR_LOOP: return ctx.Err() } +// Returns true if a.next_pts is greater than b.next_pts +func compareNextPts(a, b *encoder) bool { + return ff.AVUtil_compare_ts(a.next_pts, a.stream.TimeBase(), b.next_pts, b.stream.TimeBase()) > 0 +} + /* while (1) { AVStream *in_stream, *out_stream; From d5a5ac99b2f6648628c9d0abddb1d5183d8db360 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Wed, 26 Jun 2024 09:33:52 +0200 Subject: [PATCH 12/17] Added AVFormatFlag strings --- sys/ffmpeg61/avcodec_parameters.go | 73 +++++++++++++++++---------- sys/ffmpeg61/avformat.go | 81 +++++++++++++++++++++++++++++- writer_test.go | 2 +- 3 files changed, 127 insertions(+), 29 deletions(-) diff --git a/sys/ffmpeg61/avcodec_parameters.go b/sys/ffmpeg61/avcodec_parameters.go index 20a6b74..9d77ec3 100644 --- a/sys/ffmpeg61/avcodec_parameters.go +++ b/sys/ffmpeg61/avcodec_parameters.go @@ -17,43 +17,62 @@ import "C" //////////////////////////////////////////////////////////////////////////////// // TYPES +type jsonAVCodecParametersAudio struct { + SampleFormat AVSampleFormat `json:"format,omitempty"` + SampleRate int `json:"sample_rate,omitempty"` + ChannelLayout AVChannelLayout `json:"channel_layout,omitempty"` + FrameSize int `json:"frame_size,omitempty"` +} + +type jsonAVCodecParameterVideo struct { + PixelFormat AVPixelFormat `json:"format,omitempty"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` + SampleAspectRatio AVRational `json:"sample_aspect_ratio,omitempty"` +} + 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"` + CodecType AVMediaType `json:"codec_type"` + CodecID AVCodecID `json:"codec_id,omitempty"` + CodecTag uint32 `json:"codec_tag,omitempty"` + BitRate int64 `json:"bit_rate,omitempty"` + *jsonAVCodecParametersAudio + *jsonAVCodecParameterVideo } //////////////////////////////////////////////////////////////////////////////// // STRINGIFY 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), - }) + par := jsonAVCodecParameters{ + CodecType: AVMediaType(ctx.codec_type), + CodecID: AVCodecID(ctx.codec_id), + CodecTag: uint32(ctx.codec_tag), + BitRate: int64(ctx.bit_rate), + } + switch ctx.CodecType() { + case AVMEDIA_TYPE_AUDIO: + par.jsonAVCodecParametersAudio = &jsonAVCodecParametersAudio{ + SampleFormat: AVSampleFormat(ctx.format), + SampleRate: int(ctx.sample_rate), + ChannelLayout: AVChannelLayout(ctx.ch_layout), + FrameSize: int(ctx.frame_size), + } + case AVMEDIA_TYPE_VIDEO: + par.jsonAVCodecParameterVideo = &jsonAVCodecParameterVideo{ + PixelFormat: AVPixelFormat(ctx.format), + Width: int(ctx.width), + Height: int(ctx.height), + SampleAspectRatio: AVRational(ctx.sample_aspect_ratio), + } + } + + return json.Marshal(par) } func (ctx *AVCodecParameters) String() string { - if str, err := json.MarshalIndent(ctx, "", " "); err != nil { - return err.Error() - } else { - return string(str) - } + data, _ := json.MarshalIndent(ctx, "", " ") + return string(data) } //////////////////////////////////////////////////////////////////////////////// diff --git a/sys/ffmpeg61/avformat.go b/sys/ffmpeg61/avformat.go index 5417f01..8b76de8 100644 --- a/sys/ffmpeg61/avformat.go +++ b/sys/ffmpeg61/avformat.go @@ -112,11 +112,12 @@ type jsonAVFormatContext struct { Input *AVInputFormat `json:"input_format,omitempty"` Output *AVOutputFormat `json:"output_format,omitempty"` Url string `json:"url,omitempty"` - NumStreams uint `json:"num_streams,omitempty"` + NumStreams uint `json:"nb_streams,omitempty"` Streams []*AVStream `json:"streams,omitempty"` StartTime int64 `json:"start_time,omitempty"` Duration int64 `json:"duration,omitempty"` BitRate int64 `json:"bit_rate,omitempty"` + PacketSize uint `json:"packet_size,omitempty"` Flags AVFormatFlag `json:"flags,omitempty"` } @@ -131,6 +132,7 @@ func (ctx *AVFormatContext) MarshalJSON() ([]byte, error) { StartTime: int64(ctx.start_time), Duration: int64(ctx.duration), BitRate: int64(ctx.bit_rate), + PacketSize: uint(ctx.packet_size), Flags: AVFormatFlag(ctx.flags), }) } @@ -263,6 +265,83 @@ func (ctx *AVFormatContext) Duration() int64 { //////////////////////////////////////////////////////////////////////////////// // AVFormatFlag +const ( + AVFMT_FLAG_NONE AVFormatFlag = 0 + AVFMT_FLAG_GENPTS AVFormatFlag = C.AVFMT_FLAG_GENPTS ///< Generate missing pts even if it requires parsing future frames. + AVFMT_FLAG_IGNIDX AVFormatFlag = C.AVFMT_FLAG_IGNIDX ///< Ignore index. + AVFMT_FLAG_NONBLOCK AVFormatFlag = C.AVFMT_FLAG_NONBLOCK ///< Do not block when reading packets from input. + AVFMT_FLAG_IGNDTS AVFormatFlag = C.AVFMT_FLAG_IGNDTS ///< Ignore DTS on frames that contain both DTS & PTS + AVFMT_FLAG_NOFILLIN AVFormatFlag = C.AVFMT_FLAG_NOFILLIN ///< Do not infer any values from other values, just return what is stored in the container + AVFMT_FLAG_NOPARSE AVFormatFlag = C.AVFMT_FLAG_NOPARSE ///< Do not use AVParsers, you also must set AVFMT_FLAG_NOFILLIN as the fillin code works on frames and no parsing -> no frames. Also seeking to frames can not work if parsing to find frame boundaries has been disabled + AVFMT_FLAG_NOBUFFER AVFormatFlag = C.AVFMT_FLAG_NOBUFFER ///< Do not buffer frames when possible + AVFMT_FLAG_CUSTOM_IO AVFormatFlag = C.AVFMT_FLAG_CUSTOM_IO ///< The caller has supplied a custom AVIOContext, don't avio_close() it. + AVFMT_FLAG_DISCARD_CORRUPT AVFormatFlag = C.AVFMT_FLAG_DISCARD_CORRUPT ///< Discard frames marked corrupted + AVFMT_FLAG_FLUSH_PACKETS AVFormatFlag = C.AVFMT_FLAG_FLUSH_PACKETS ///< Flush the AVIOContext every packet. + AVFMT_FLAG_BITEXACT AVFormatFlag = C.AVFMT_FLAG_BITEXACT // When muxing, try to avoid writing any random/volatile data to the output. + AVFMT_FLAG_SORT_DTS AVFormatFlag = C.AVFMT_FLAG_SORT_DTS ///< try to interleave outputted packets by dts (using this flag can slow demuxing down) + AVFMT_FLAG_FAST_SEEK AVFormatFlag = C.AVFMT_FLAG_FAST_SEEK ///< Enable fast, but inaccurate seeks for some formats + AVFMT_FLAG_SHORTEST AVFormatFlag = C.AVFMT_FLAG_SHORTEST ///< Stop muxing when the shortest stream stops. + AVFMT_FLAG_AUTO_BSF AVFormatFlag = C.AVFMT_FLAG_AUTO_BSF ///< Add bitstream filters as requested by the muxer + AVFMT_FLAG_MIN = AVFMT_FLAG_GENPTS + AVFMT_FLAG_MAX = AVFMT_FLAG_AUTO_BSF +) + +func (f AVFormatFlag) FlagString() string { + switch f { + case AVFMT_FLAG_NONE: + return "AVFMT_FLAG_NONE" + case AVFMT_FLAG_GENPTS: + return "AVFMT_FLAG_GENPTS" + case AVFMT_FLAG_IGNIDX: + return "AVFMT_FLAG_IGNIDX" + case AVFMT_FLAG_NONBLOCK: + return "AVFMT_FLAG_NONBLOCK" + case AVFMT_FLAG_IGNDTS: + return "AVFMT_FLAG_IGNDTS" + case AVFMT_FLAG_NOFILLIN: + return "AVFMT_FLAG_NOFILLIN" + case AVFMT_FLAG_NOPARSE: + return "AVFMT_FLAG_NOPARSE" + case AVFMT_FLAG_NOBUFFER: + return "AVFMT_FLAG_NOBUFFER" + case AVFMT_FLAG_CUSTOM_IO: + return "AVFMT_FLAG_CUSTOM_IO" + case AVFMT_FLAG_DISCARD_CORRUPT: + return "AVFMT_FLAG_DISCARD_CORRUPT" + case AVFMT_FLAG_FLUSH_PACKETS: + return "AVFMT_FLAG_FLUSH_PACKETS" + case AVFMT_FLAG_BITEXACT: + return "AVFMT_FLAG_BITEXACT" + case AVFMT_FLAG_SORT_DTS: + return "AVFMT_FLAG_SORT_DTS" + case AVFMT_FLAG_FAST_SEEK: + return "AVFMT_FLAG_FAST_SEEK" + case AVFMT_FLAG_SHORTEST: + return "AVFMT_FLAG_SHORTEST" + case AVFMT_FLAG_AUTO_BSF: + return "AVFMT_FLAG_AUTO_BSF" + default: + return fmt.Sprintf("AVFormatFlag(0x%06X)", int(f)) + } +} + +func (f AVFormatFlag) MarshalJSON() ([]byte, error) { + return json.Marshal(f.String()) +} + +func (f AVFormatFlag) String() string { + if f == AVFMT_FLAG_NONE { + return f.FlagString() + } + str := "" + for i := AVFMT_FLAG_MIN; i <= AVFMT_FLAG_MAX; i <<= 1 { + if f&i != 0 { + str += "|" + i.FlagString() + } + } + return str[1:] +} + func (f AVFormatFlag) Is(flag AVFormatFlag) bool { return f&flag == flag } diff --git a/writer_test.go b/writer_test.go index 0338b96..16daf8f 100644 --- a/writer_test.go +++ b/writer_test.go @@ -23,7 +23,7 @@ func Test_writer_001(t *testing.T) { // Write audio file filename := filepath.Join(t.TempDir(), t.Name()+".mp3") - stream, err := manager.AudioParameters("mono", "s16", 22050) + stream, err := manager.AudioParameters("mono", "fltp", 22050) if !assert.NoError(err) { t.SkipNow() } From 0fcfbc936358fa5bdf47395d1d08239d1be295ae Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Wed, 26 Jun 2024 09:37:52 +0200 Subject: [PATCH 13/17] Added muxing to the testing --- writer_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/writer_test.go b/writer_test.go index 16daf8f..7485062 100644 --- a/writer_test.go +++ b/writer_test.go @@ -1,6 +1,7 @@ package media_test import ( + "context" "path/filepath" "strings" "testing" @@ -33,5 +34,12 @@ func Test_writer_001(t *testing.T) { t.SkipNow() } defer writer.Close() + t.Log(writer, "=>", filename) + + // Perform muxing of packets + writer.Mux(context.Background(), func(stream int) (Packet, error) { + t.Log("Muxing packet for stream", stream) + return nil, nil + }) } From bea00ffe91744707fe0ff8e3fb3ab85df747c63a Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Wed, 26 Jun 2024 09:43:51 +0200 Subject: [PATCH 14/17] Added AV example --- writer_test.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/writer_test.go b/writer_test.go index 7485062..cb8853c 100644 --- a/writer_test.go +++ b/writer_test.go @@ -43,3 +43,38 @@ func Test_writer_001(t *testing.T) { return nil, nil }) } + +func Test_writer_002(t *testing.T) { + assert := assert.New(t) + manager, err := NewManager(OptLog(true, func(v string) { + t.Log(strings.TrimSpace(v)) + })) + if !assert.NoError(err) { + t.SkipNow() + } + + // Write file with both audio and video + filename := filepath.Join(t.TempDir(), t.Name()+".mp4") + audio, err := manager.AudioParameters("mono", "fltp", 22050) + if !assert.NoError(err) { + t.SkipNow() + } + video, err := manager.VideoParameters(1280, 720, "yuv420p") + if !assert.NoError(err) { + t.SkipNow() + } + + writer, err := manager.Create(filename, nil, nil, audio, video) + if !assert.NoError(err) { + t.SkipNow() + } + defer writer.Close() + + t.Log(writer, "=>", filename) + + // Perform muxing of packets + writer.Mux(context.Background(), func(stream int) (Packet, error) { + t.Log("Muxing packet for stream", stream) + return nil, nil + }) +} From 9733e0c8691a256c83b4a23249c66776366c5574 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Wed, 26 Jun 2024 09:45:24 +0200 Subject: [PATCH 15/17] Cleanup --- stream.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/stream.go b/stream.go index 044814c..2c0231a 100644 --- a/stream.go +++ b/stream.go @@ -12,15 +12,8 @@ type stream struct { *ff.AVStream } -type writerstream struct { - t MediaType - codec *ff.AVCodec -} - var _ Stream = (*stream)(nil) -//var _ Stream = (*writerstream)(nil) - //////////////////////////////////////////////////////////////////////////////// // LIFECYCLE From 32fd13fc13012cb728ab23a87bdd244e41877599 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Wed, 26 Jun 2024 09:52:31 +0200 Subject: [PATCH 16/17] Updated tests --- .github/workflows/on_pull_request_merge.yaml | 5 +-- Makefile | 9 ++++++ sys/ffmpeg61/avdevice_input_test.go | 2 ++ sys/ffmpeg61/avutil_log_test.go | 32 ++++++++++---------- writer_test.go | 4 +++ 5 files changed, 34 insertions(+), 18 deletions(-) diff --git a/.github/workflows/on_pull_request_merge.yaml b/.github/workflows/on_pull_request_merge.yaml index eef4bcd..3909585 100644 --- a/.github/workflows/on_pull_request_merge.yaml +++ b/.github/workflows/on_pull_request_merge.yaml @@ -6,7 +6,7 @@ on: jobs: analyze: name: Analyze - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: actions: read contents: read @@ -42,4 +42,5 @@ jobs: - name: Run tests run: | sudo apt install -y libavcodec-dev libavdevice-dev libavfilter-dev libavutil-dev libswscale-dev libswresample-dev libchromaprint-dev - make test + make container-test + diff --git a/Makefile b/Makefile index 945d9ad..08d83ad 100755 --- a/Makefile +++ b/Makefile @@ -48,6 +48,15 @@ test: go-dep @${GO} test ./pkg/... @${GO} test . +container-test: go-dep + @echo Test + @${GO} mod tidy + @${GO} test --tags=container ./sys/ffmpeg61 + @${GO} test --tags=container ./sys/chromaprint + @${GO} test --tags=container ./pkg/... + @${GO} test --tags=container . + + cli: go-dep mkdir @echo Build media tool diff --git a/sys/ffmpeg61/avdevice_input_test.go b/sys/ffmpeg61/avdevice_input_test.go index 9c977ce..ba7a8fa 100644 --- a/sys/ffmpeg61/avdevice_input_test.go +++ b/sys/ffmpeg61/avdevice_input_test.go @@ -1,3 +1,5 @@ +//go:build !container + package ffmpeg_test import ( diff --git a/sys/ffmpeg61/avutil_log_test.go b/sys/ffmpeg61/avutil_log_test.go index 6ab6082..1787643 100644 --- a/sys/ffmpeg61/avutil_log_test.go +++ b/sys/ffmpeg61/avutil_log_test.go @@ -18,14 +18,14 @@ func Test_avutil_log_000(t *testing.T) { assert.Equal(AV_LOG_TRACE, AVUtil_log_get_level()) // Log a message - AVUtil_log(nil, AV_LOG_TRACE, "This is a trace message") - AVUtil_log(nil, AV_LOG_DEBUG, "This is a debug message") - AVUtil_log(nil, AV_LOG_VERBOSE, "This is a verbose message") - AVUtil_log(nil, AV_LOG_INFO, "This is a info message") - AVUtil_log(nil, AV_LOG_WARNING, "This is a warning message") - AVUtil_log(nil, AV_LOG_ERROR, "This is a error message") - AVUtil_log(nil, AV_LOG_FATAL, "This is a fatal message") - AVUtil_log(nil, AV_LOG_PANIC, "This is a panic message") + AVUtil_log(nil, AV_LOG_TRACE, "This is a trace message\n") + AVUtil_log(nil, AV_LOG_DEBUG, "This is a debug message\n") + AVUtil_log(nil, AV_LOG_VERBOSE, "This is a verbose message\n") + AVUtil_log(nil, AV_LOG_INFO, "This is a info message\n") + AVUtil_log(nil, AV_LOG_WARNING, "This is a warning message\n") + AVUtil_log(nil, AV_LOG_ERROR, "This is a error message\n") + AVUtil_log(nil, AV_LOG_FATAL, "This is a fatal message\n") + AVUtil_log(nil, AV_LOG_PANIC, "This is a panic message\n") } func Test_avutil_log_001(t *testing.T) { @@ -41,12 +41,12 @@ func Test_avutil_log_001(t *testing.T) { }) // Log a message - AVUtil_log(nil, AV_LOG_TRACE, "This is a trace message") - AVUtil_log(nil, AV_LOG_DEBUG, "This is a debug message") - AVUtil_log(nil, AV_LOG_VERBOSE, "This is a verbose message") - AVUtil_log(nil, AV_LOG_INFO, "This is a info message") - AVUtil_log(nil, AV_LOG_WARNING, "This is a warning message") - AVUtil_log(nil, AV_LOG_ERROR, "This is a error message") - AVUtil_log(nil, AV_LOG_FATAL, "This is a fatal message") - AVUtil_log(nil, AV_LOG_PANIC, "This is a panic message") + AVUtil_log(nil, AV_LOG_TRACE, "This is a trace message\n") + AVUtil_log(nil, AV_LOG_DEBUG, "This is a debug message\n") + AVUtil_log(nil, AV_LOG_VERBOSE, "This is a verbose message\n") + AVUtil_log(nil, AV_LOG_INFO, "This is a info message\n") + AVUtil_log(nil, AV_LOG_WARNING, "This is a warning message\n") + AVUtil_log(nil, AV_LOG_ERROR, "This is a error message\n") + AVUtil_log(nil, AV_LOG_FATAL, "This is a fatal message\n") + AVUtil_log(nil, AV_LOG_PANIC, "This is a panic message\n") } diff --git a/writer_test.go b/writer_test.go index cb8853c..5e21d43 100644 --- a/writer_test.go +++ b/writer_test.go @@ -1,5 +1,9 @@ +//go:build !container + package media_test +// TODO: Allow this test to run in containers + import ( "context" "path/filepath" From 3b9c6dbb19f889fa08e8eb607919b5a0974897c7 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Wed, 26 Jun 2024 09:56:07 +0200 Subject: [PATCH 17/17] Updated tests --- manager_ex_test.go | 34 ++++++++++++++++++++++++++++++++++ manager_test.go | 19 ------------------- 2 files changed, 34 insertions(+), 19 deletions(-) create mode 100644 manager_ex_test.go diff --git a/manager_ex_test.go b/manager_ex_test.go new file mode 100644 index 0000000..6c27dcd --- /dev/null +++ b/manager_ex_test.go @@ -0,0 +1,34 @@ +//go:build !container + +package media_test + +import ( + "testing" + + // Package imports + "github.com/stretchr/testify/assert" + + // Namespace imports + . "github.com/mutablelogic/go-media" +) + +// These tests do not run in containers + +func Test_manager_008(t *testing.T) { + assert := assert.New(t) + + manager, err := NewManager() + if !assert.NoError(err) { + t.SkipNow() + } + + formats := manager.InputFormats(ANY) + assert.NotNil(formats) + for _, format := range formats { + if format.Type().Is(DEVICE) { + devices := manager.Devices(format) + assert.NotNil(devices) + t.Log(format, devices) + } + } +} diff --git a/manager_test.go b/manager_test.go index 8049d4c..37f1994 100644 --- a/manager_test.go +++ b/manager_test.go @@ -108,22 +108,3 @@ func Test_manager_007(t *testing.T) { tablewriter.New(os.Stderr, tablewriter.OptHeader(), tablewriter.OptOutputText()).Write(codecs) } - -func Test_manager_008(t *testing.T) { - assert := assert.New(t) - - manager, err := NewManager() - if !assert.NoError(err) { - t.SkipNow() - } - - formats := manager.InputFormats(ANY) - assert.NotNil(formats) - for _, format := range formats { - if format.Type().Is(DEVICE) { - devices := manager.Devices(format) - assert.NotNil(devices) - t.Log(format, devices) - } - } -}