Skip to content

Commit 7ea948e

Browse files
authored
Merge pull request #27 from mutablelogic/ffmpeg61
Added reader
2 parents b34c872 + 57d5ea9 commit 7ea948e

File tree

4 files changed

+304
-0
lines changed

4 files changed

+304
-0
lines changed

pkg/ffmpeg/opts.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ type opts struct {
2222
oformat *ffmpeg.AVOutputFormat
2323
streams map[int]*Par
2424
metadata []*Metadata
25+
26+
// Reader options
27+
iformat *ffmpeg.AVInputFormat
28+
opts []string // These are key=value pairs
2529
}
2630

2731
////////////////////////////////////////////////////////////////////////////////
@@ -50,6 +54,27 @@ func OptOutputFormat(name string) Opt {
5054
}
5155
}
5256

57+
// Input format from name or url
58+
func OptInputFormat(name string) Opt {
59+
return func(o *opts) error {
60+
// By name
61+
if iformat := ffmpeg.AVFormat_find_input_format(name); iformat != nil {
62+
o.iformat = iformat
63+
} else {
64+
return ErrBadParameter.Withf("invalid input format %q", name)
65+
}
66+
return nil
67+
}
68+
}
69+
70+
// Input format options
71+
func OptInputOpt(opt ...string) Opt {
72+
return func(o *opts) error {
73+
o.opts = append(o.opts, opt...)
74+
return nil
75+
}
76+
}
77+
5378
// New stream with parameters
5479
func OptStream(stream int, par *Par) Opt {
5580
return func(o *opts) error {

pkg/ffmpeg/reader.go

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
package ffmpeg
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"io"
8+
"slices"
9+
"strings"
10+
"time"
11+
12+
// Packages
13+
ff "github.com/mutablelogic/go-media/sys/ffmpeg61"
14+
)
15+
16+
////////////////////////////////////////////////////////////////////////////////
17+
// TYPES
18+
19+
// Media reader which reads from a URL, file path or device
20+
type Reader struct {
21+
input *ff.AVFormatContext
22+
avio *ff.AVIOContextEx
23+
}
24+
25+
type reader_callback struct {
26+
r io.Reader
27+
}
28+
29+
// Return parameters if a stream should be decoded and either resampled or
30+
// resized. Return nil if you want to ignore the stream, or pass back the
31+
// stream parameters if you want to copy the stream without any changes.
32+
type DecoderMapFunc func(int, *Par) (*Par, error)
33+
34+
////////////////////////////////////////////////////////////////////////////////
35+
// LIFECYCLE
36+
37+
// Open media from a url, file path or device
38+
func Open(url string, opt ...Opt) (*Reader, error) {
39+
options := newOpts()
40+
reader := new(Reader)
41+
42+
// Apply options
43+
for _, opt := range opt {
44+
if err := opt(options); err != nil {
45+
return nil, err
46+
}
47+
}
48+
49+
// Get the options
50+
dict := ff.AVUtil_dict_alloc()
51+
defer ff.AVUtil_dict_free(dict)
52+
if len(options.opts) > 0 {
53+
if err := ff.AVUtil_dict_parse_string(dict, strings.Join(options.opts, " "), "=", " ", 0); err != nil {
54+
return nil, err
55+
}
56+
}
57+
58+
// Open the device or stream
59+
if ctx, err := ff.AVFormat_open_url(url, options.iformat, dict); err != nil {
60+
return nil, err
61+
} else {
62+
reader.input = ctx
63+
}
64+
65+
// Find stream information and do rest of the initialization
66+
return reader.open(options)
67+
}
68+
69+
// Create a new reader from an io.Reader
70+
func NewReader(r io.Reader, opt ...Opt) (*Reader, error) {
71+
options := newOpts()
72+
reader := new(Reader)
73+
74+
// Apply options
75+
for _, opt := range opt {
76+
if err := opt(options); err != nil {
77+
return nil, err
78+
}
79+
}
80+
81+
// Get the options
82+
dict := ff.AVUtil_dict_alloc()
83+
defer ff.AVUtil_dict_free(dict)
84+
if len(options.opts) > 0 {
85+
if err := ff.AVUtil_dict_parse_string(dict, strings.Join(options.opts, " "), "=", " ", 0); err != nil {
86+
return nil, err
87+
}
88+
}
89+
90+
// Allocate the AVIO context
91+
reader.avio = ff.AVFormat_avio_alloc_context(bufSize, false, &reader_callback{r})
92+
if reader.avio == nil {
93+
return nil, errors.New("failed to allocate avio context")
94+
}
95+
96+
// Open the stream
97+
if ctx, err := ff.AVFormat_open_reader(reader.avio, options.iformat, dict); err != nil {
98+
ff.AVFormat_avio_context_free(reader.avio)
99+
return nil, err
100+
} else {
101+
reader.input = ctx
102+
}
103+
104+
// Find stream information and do rest of the initialization
105+
return reader.open(options)
106+
}
107+
108+
func (r *Reader) open(_ *opts) (*Reader, error) {
109+
// Find stream information
110+
if err := ff.AVFormat_find_stream_info(r.input, nil); err != nil {
111+
ff.AVFormat_free_context(r.input)
112+
ff.AVFormat_avio_context_free(r.avio)
113+
return nil, err
114+
}
115+
116+
// Return success
117+
return r, nil
118+
}
119+
120+
// Close the reader
121+
func (r *Reader) Close() error {
122+
var result error
123+
124+
// Free resources
125+
ff.AVFormat_free_context(r.input)
126+
if r.avio != nil {
127+
ff.AVFormat_avio_context_free(r.avio)
128+
}
129+
130+
// Release resources
131+
r.input = nil
132+
r.avio = nil
133+
134+
// Return any errors
135+
return result
136+
}
137+
138+
////////////////////////////////////////////////////////////////////////////////
139+
// STRINGIFY
140+
141+
// Display the reader as a string
142+
func (r *Reader) MarshalJSON() ([]byte, error) {
143+
return json.Marshal(r.input)
144+
}
145+
146+
// Display the reader as a string
147+
func (r *Reader) String() string {
148+
data, _ := json.MarshalIndent(r, "", " ")
149+
return string(data)
150+
}
151+
152+
////////////////////////////////////////////////////////////////////////////////
153+
// PUBLIC METHODS
154+
155+
// Return the duration of the media stream, returns zero if unknown
156+
func (r *Reader) Duration() time.Duration {
157+
duration := r.input.Duration()
158+
if duration > 0 {
159+
return time.Duration(duration) * time.Second / time.Duration(ff.AV_TIME_BASE)
160+
}
161+
return 0
162+
}
163+
164+
// Return the metadata for the media stream, filtering by the specified keys
165+
// if there are any. Artwork is returned with the "artwork" key.
166+
func (r *Reader) Metadata(keys ...string) []*Metadata {
167+
entries := ff.AVUtil_dict_entries(r.input.Metadata())
168+
result := make([]*Metadata, 0, len(entries))
169+
for _, entry := range entries {
170+
if len(keys) == 0 || slices.Contains(keys, entry.Key()) {
171+
result = append(result, NewMetadata(entry.Key(), entry.Value()))
172+
}
173+
}
174+
175+
// Obtain any artwork from the streams
176+
if slices.Contains(keys, MetaArtwork) {
177+
for _, stream := range r.input.Streams() {
178+
if packet := stream.AttachedPic(); packet != nil {
179+
result = append(result, NewMetadata(MetaArtwork, packet.Bytes()))
180+
}
181+
}
182+
}
183+
184+
// Return all the metadata
185+
return result
186+
}
187+
188+
// TODO Decode the media stream into packets and frames
189+
func (r *Reader) Decode(ctx context.Context, fn DecoderMapFunc) error {
190+
return errors.New("not implemented yet")
191+
}
192+
193+
////////////////////////////////////////////////////////////////////////////////
194+
// PRIVATE METHODS
195+
196+
func (r *reader_callback) Reader(buf []byte) int {
197+
n, err := r.r.Read(buf)
198+
if err != nil {
199+
return ff.AVERROR_EOF
200+
}
201+
return n
202+
}
203+
204+
func (r *reader_callback) Seeker(offset int64, whence int) int64 {
205+
whence = whence & ^ff.AVSEEK_FORCE
206+
seeker, ok := r.r.(io.ReadSeeker)
207+
if !ok {
208+
return -1
209+
}
210+
switch whence {
211+
case io.SeekStart, io.SeekCurrent, io.SeekEnd:
212+
n, err := seeker.Seek(offset, whence)
213+
if err != nil {
214+
return -1
215+
}
216+
return n
217+
}
218+
return -1
219+
}
220+
221+
func (r *reader_callback) Writer([]byte) int {
222+
return ff.AVERROR_EOF
223+
}

pkg/ffmpeg/reader_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package ffmpeg_test
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
ffmpeg "github.com/mutablelogic/go-media/pkg/ffmpeg"
8+
assert "github.com/stretchr/testify/assert"
9+
)
10+
11+
func Test_reader_001(t *testing.T) {
12+
assert := assert.New(t)
13+
14+
// Read a file
15+
r, err := ffmpeg.Open("../../etc/test/sample.mp4")
16+
if !assert.NoError(err) {
17+
t.FailNow()
18+
}
19+
defer r.Close()
20+
21+
t.Log(r)
22+
}
23+
24+
func Test_reader_002(t *testing.T) {
25+
assert := assert.New(t)
26+
27+
// Read a file
28+
r, err := os.Open("../../etc/test/sample.mp4")
29+
if !assert.NoError(err) {
30+
t.FailNow()
31+
}
32+
defer r.Close()
33+
34+
media, err := ffmpeg.NewReader(r)
35+
if !assert.NoError(err) {
36+
t.FailNow()
37+
}
38+
defer media.Close()
39+
40+
t.Log(media)
41+
}

pkg/ffmpeg/writer.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package ffmpeg
22

33
import (
4+
"encoding/json"
45
"errors"
56
"fmt"
67
"io"
@@ -191,6 +192,20 @@ func (w *Writer) Close() error {
191192
return result
192193
}
193194

195+
////////////////////////////////////////////////////////////////////////////////
196+
// STRINGIFY
197+
198+
// Display the writer as a string
199+
func (w *Writer) MarshalJSON() ([]byte, error) {
200+
return json.Marshal(w.output)
201+
}
202+
203+
// Display the writer as a string
204+
func (w *Writer) String() string {
205+
data, _ := json.MarshalIndent(w, "", " ")
206+
return string(data)
207+
}
208+
194209
//////////////////////////////////////////////////////////////////////////////
195210
// PUBLIC METHODS
196211

0 commit comments

Comments
 (0)