Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Added reader #27

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions pkg/ffmpeg/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -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 {
Expand Down
223 changes: 223 additions & 0 deletions pkg/ffmpeg/reader.go
Original file line number Diff line number Diff line change
@@ -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
}
41 changes: 41 additions & 0 deletions pkg/ffmpeg/reader_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
15 changes: 15 additions & 0 deletions pkg/ffmpeg/writer.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ffmpeg

import (
"encoding/json"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -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

Expand Down
Loading