From b6c39fba3856ae2d8905070a76309ae775968fb3 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sun, 30 Jun 2024 20:39:32 +0200 Subject: [PATCH] Added reader --- pkg/ffmpeg/opts.go | 25 +++++ pkg/ffmpeg/reader.go | 223 ++++++++++++++++++++++++++++++++++++++ pkg/ffmpeg/reader_test.go | 41 +++++++ pkg/ffmpeg/writer.go | 15 +++ 4 files changed, 304 insertions(+) create mode 100644 pkg/ffmpeg/reader.go create mode 100644 pkg/ffmpeg/reader_test.go diff --git a/pkg/ffmpeg/opts.go b/pkg/ffmpeg/opts.go index dbe1660..6431802 100644 --- a/pkg/ffmpeg/opts.go +++ b/pkg/ffmpeg/opts.go @@ -22,6 +22,10 @@ type opts struct { oformat *ffmpeg.AVOutputFormat streams map[int]*Par metadata []*Metadata + + // Reader options + iformat *ffmpeg.AVInputFormat + opts []string // These are key=value pairs } //////////////////////////////////////////////////////////////////////////////// @@ -50,6 +54,27 @@ func OptOutputFormat(name string) Opt { } } +// Input format from name or url +func OptInputFormat(name string) Opt { + return func(o *opts) error { + // By name + if iformat := ffmpeg.AVFormat_find_input_format(name); iformat != nil { + o.iformat = iformat + } else { + return ErrBadParameter.Withf("invalid input format %q", name) + } + return nil + } +} + +// Input format options +func OptInputOpt(opt ...string) Opt { + return func(o *opts) error { + o.opts = append(o.opts, opt...) + return nil + } +} + // New stream with parameters func OptStream(stream int, par *Par) Opt { return func(o *opts) error { diff --git a/pkg/ffmpeg/reader.go b/pkg/ffmpeg/reader.go new file mode 100644 index 0000000..8088899 --- /dev/null +++ b/pkg/ffmpeg/reader.go @@ -0,0 +1,223 @@ +package ffmpeg + +import ( + "context" + "encoding/json" + "errors" + "io" + "slices" + "strings" + "time" + + // Packages + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + +//////////////////////////////////////////////////////////////////////////////// +// TYPES + +// Media reader which reads from a URL, file path or device +type Reader struct { + input *ff.AVFormatContext + avio *ff.AVIOContextEx +} + +type reader_callback struct { + r io.Reader +} + +// Return parameters if a stream should be decoded and either resampled or +// resized. Return nil if you want to ignore the stream, or pass back the +// stream parameters if you want to copy the stream without any changes. +type DecoderMapFunc func(int, *Par) (*Par, error) + +//////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// Open media from a url, file path or device +func Open(url string, opt ...Opt) (*Reader, error) { + options := newOpts() + reader := new(Reader) + + // Apply options + for _, opt := range opt { + if err := opt(options); err != nil { + return nil, err + } + } + + // Get the options + dict := ff.AVUtil_dict_alloc() + defer ff.AVUtil_dict_free(dict) + if len(options.opts) > 0 { + if err := ff.AVUtil_dict_parse_string(dict, strings.Join(options.opts, " "), "=", " ", 0); err != nil { + return nil, err + } + } + + // Open the device or stream + if ctx, err := ff.AVFormat_open_url(url, options.iformat, dict); err != nil { + return nil, err + } else { + reader.input = ctx + } + + // Find stream information and do rest of the initialization + return reader.open(options) +} + +// Create a new reader from an io.Reader +func NewReader(r io.Reader, opt ...Opt) (*Reader, error) { + options := newOpts() + reader := new(Reader) + + // Apply options + for _, opt := range opt { + if err := opt(options); err != nil { + return nil, err + } + } + + // Get the options + dict := ff.AVUtil_dict_alloc() + defer ff.AVUtil_dict_free(dict) + if len(options.opts) > 0 { + if err := ff.AVUtil_dict_parse_string(dict, strings.Join(options.opts, " "), "=", " ", 0); err != nil { + return nil, err + } + } + + // 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, options.iformat, dict); 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(options) +} + +func (r *Reader) open(_ *opts) (*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 + } + + // Return success + return r, nil +} + +// Close the reader +func (r *Reader) Close() error { + var result error + + // Free resources + ff.AVFormat_free_context(r.input) + if r.avio != nil { + ff.AVFormat_avio_context_free(r.avio) + } + + // Release resources + r.input = nil + r.avio = nil + + // Return any errors + return result +} + +//////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +// Display the reader as a string +func (r *Reader) MarshalJSON() ([]byte, error) { + return json.Marshal(r.input) +} + +// Display the reader as a string +func (r *Reader) String() string { + data, _ := json.MarshalIndent(r, "", " ") + return string(data) +} + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Return the duration of the media stream, returns zero if unknown +func (r *Reader) Duration() time.Duration { + duration := r.input.Duration() + if duration > 0 { + return time.Duration(duration) * time.Second / time.Duration(ff.AV_TIME_BASE) + } + return 0 +} + +// Return the metadata for the media stream, filtering by the specified keys +// if there are any. Artwork is returned with the "artwork" key. +func (r *Reader) Metadata(keys ...string) []*Metadata { + entries := ff.AVUtil_dict_entries(r.input.Metadata()) + result := make([]*Metadata, 0, len(entries)) + for _, entry := range entries { + if len(keys) == 0 || slices.Contains(keys, entry.Key()) { + result = append(result, NewMetadata(entry.Key(), entry.Value())) + } + } + + // Obtain any artwork from the streams + if slices.Contains(keys, MetaArtwork) { + for _, stream := range r.input.Streams() { + if packet := stream.AttachedPic(); packet != nil { + result = append(result, NewMetadata(MetaArtwork, packet.Bytes())) + } + } + } + + // Return all the metadata + return result +} + +// TODO Decode the media stream into packets and frames +func (r *Reader) Decode(ctx context.Context, fn DecoderMapFunc) error { + return errors.New("not implemented yet") +} + +//////////////////////////////////////////////////////////////////////////////// +// 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/pkg/ffmpeg/reader_test.go b/pkg/ffmpeg/reader_test.go new file mode 100644 index 0000000..4410adf --- /dev/null +++ b/pkg/ffmpeg/reader_test.go @@ -0,0 +1,41 @@ +package ffmpeg_test + +import ( + "os" + "testing" + + ffmpeg "github.com/mutablelogic/go-media/pkg/ffmpeg" + assert "github.com/stretchr/testify/assert" +) + +func Test_reader_001(t *testing.T) { + assert := assert.New(t) + + // Read a file + r, err := ffmpeg.Open("../../etc/test/sample.mp4") + if !assert.NoError(err) { + t.FailNow() + } + defer r.Close() + + t.Log(r) +} + +func Test_reader_002(t *testing.T) { + assert := assert.New(t) + + // Read a file + r, err := os.Open("../../etc/test/sample.mp4") + if !assert.NoError(err) { + t.FailNow() + } + defer r.Close() + + media, err := ffmpeg.NewReader(r) + if !assert.NoError(err) { + t.FailNow() + } + defer media.Close() + + t.Log(media) +} diff --git a/pkg/ffmpeg/writer.go b/pkg/ffmpeg/writer.go index a1c9eaa..b6d8062 100644 --- a/pkg/ffmpeg/writer.go +++ b/pkg/ffmpeg/writer.go @@ -1,6 +1,7 @@ package ffmpeg import ( + "encoding/json" "errors" "fmt" "io" @@ -191,6 +192,20 @@ func (w *Writer) Close() error { return result } +//////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +// Display the writer as a string +func (w *Writer) MarshalJSON() ([]byte, error) { + return json.Marshal(w.output) +} + +// Display the writer as a string +func (w *Writer) String() string { + data, _ := json.MarshalIndent(w, "", " ") + return string(data) +} + ////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS