Skip to content

Commit 80bf31d

Browse files
committed
Updated writer
1 parent c932e17 commit 80bf31d

File tree

7 files changed

+244
-55
lines changed

7 files changed

+244
-55
lines changed

pkg/ffmpeg/encoder.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package ffmpeg
33
import (
44
"encoding/json"
55
"errors"
6-
"fmt"
76
"io"
87
"syscall"
98

@@ -21,16 +20,19 @@ type Encoder struct {
2120
ctx *ff.AVCodecContext
2221
stream *ff.AVStream
2322
packet *ff.AVPacket
23+
24+
// We are flushing the encoder
25+
eof bool
2426
//next_pts int64
2527
}
2628

2729
// EncoderFrameFn is a function which is called to receive a frame to encode. It should
2830
// return nil to continue encoding or io.EOF to stop encoding.
2931
type EncoderFrameFn func(int) (*ff.AVFrame, error)
3032

31-
// EncoderPacketFn is a function which is called for each packet encoded. It should
32-
// return nil to continue encoding or io.EOF to stop encoding immediately.
33-
type EncoderPacketFn func(*ff.AVPacket) error
33+
// EncoderPacketFn is a function which is called for each packet encoded, with
34+
// the stream timebase.
35+
type EncoderPacketFn func(*ff.AVPacket, *ff.AVRational) error
3436

3537
////////////////////////////////////////////////////////////////////////////////
3638
// LIFECYCLE
@@ -178,8 +180,9 @@ func (e *Encoder) Par() *Par {
178180
// PRIVATE METHODS
179181

180182
func (e *Encoder) encode(frame *ff.AVFrame, fn EncoderPacketFn) error {
183+
timebase := e.stream.TimeBase()
184+
181185
// Send the frame to the encoder
182-
fmt.Println("Sending frame", frame)
183186
if err := ff.AVCodec_send_frame(e.ctx, frame); err != nil {
184187
if errors.Is(err, syscall.EAGAIN) || errors.Is(err, io.EOF) {
185188
return nil
@@ -191,7 +194,6 @@ func (e *Encoder) encode(frame *ff.AVFrame, fn EncoderPacketFn) error {
191194
var result error
192195
for {
193196
// Receive the packet
194-
fmt.Println("Receiving packet")
195197
if err := ff.AVCodec_receive_packet(e.ctx, e.packet); errors.Is(err, syscall.EAGAIN) || errors.Is(err, io.EOF) {
196198
// Finished receiving packet or EOF
197199
break
@@ -200,7 +202,7 @@ func (e *Encoder) encode(frame *ff.AVFrame, fn EncoderPacketFn) error {
200202
}
201203

202204
// Pass back to the caller
203-
if err := fn(e.packet); errors.Is(err, io.EOF) {
205+
if err := fn(e.packet, &timebase); errors.Is(err, io.EOF) {
204206
// End early, return EOF
205207
result = io.EOF
206208
break
@@ -214,7 +216,7 @@ func (e *Encoder) encode(frame *ff.AVFrame, fn EncoderPacketFn) error {
214216

215217
// Flush
216218
if result == nil {
217-
result = fn(nil)
219+
result = fn(nil, &timebase)
218220
}
219221

220222
// Return success or EOF

pkg/ffmpeg/metadata.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package ffmpeg
2+
3+
import (
4+
"encoding/json"
5+
)
6+
7+
////////////////////////////////////////////////////////////////////////////////
8+
// TYPES
9+
10+
type Metadata struct {
11+
Key string `json:"key" writer:",width:30"`
12+
Value any `json:"value,omitempty" writer:",wrap,width:50"`
13+
}
14+
15+
const (
16+
MetaArtwork = "artwork" // Metadata key for artwork, set the value as []byte
17+
)
18+
19+
////////////////////////////////////////////////////////////////////////////////
20+
// LIFECYCLE
21+
22+
func NewMetadata(key string, value any) *Metadata {
23+
return &Metadata{
24+
Key: key,
25+
Value: value,
26+
}
27+
}
28+
29+
////////////////////////////////////////////////////////////////////////////////
30+
// STRINIGY
31+
32+
func (m *Metadata) String() string {
33+
data, _ := json.MarshalIndent(m, "", " ")
34+
return string(data)
35+
}

pkg/ffmpeg/opts.go

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

33
import (
4-
// Namespace imports
4+
// Package imports
5+
ffmpeg "github.com/mutablelogic/go-media/sys/ffmpeg61"
56

7+
// Namespace imports
68
. "github.com/djthorpe/go-errors"
7-
ffmpeg "github.com/mutablelogic/go-media/sys/ffmpeg61"
89
)
910

1011
////////////////////////////////////////////////////////////////////////////////
@@ -17,11 +18,10 @@ type opts struct {
1718
force bool
1819
par *Par
1920

20-
// Format options
21-
oformat *ffmpeg.AVOutputFormat
22-
23-
// Stream options
24-
streams map[int]*Par
21+
// Writer options
22+
oformat *ffmpeg.AVOutputFormat
23+
streams map[int]*Par
24+
metadata []*Metadata
2525
}
2626

2727
////////////////////////////////////////////////////////////////////////////////
@@ -103,6 +103,14 @@ func OptForce() Opt {
103103
}
104104
}
105105

106+
// Append metadata to the output file, including artwork
107+
func OptMetadata(entry ...*Metadata) Opt {
108+
return func(o *opts) error {
109+
o.metadata = append(o.metadata, entry...)
110+
return nil
111+
}
112+
}
113+
106114
// Pixel format of the output frame
107115
func OptPixFormat(format string) Opt {
108116
return func(o *opts) error {

pkg/ffmpeg/writer.go

Lines changed: 89 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,40 @@ const (
3939
//////////////////////////////////////////////////////////////////////////////
4040
// LIFECYCLE
4141

42+
// Create a new writer with a URL and options
43+
func Create(url string, opt ...Opt) (*Writer, error) {
44+
options := newOpts()
45+
writer := new(Writer)
46+
47+
// Apply options
48+
for _, opt := range opt {
49+
if err := opt(options); err != nil {
50+
return nil, err
51+
}
52+
}
53+
54+
// Guess the output format
55+
var ofmt *ff.AVOutputFormat
56+
if options.oformat == nil && url != "" {
57+
options.oformat = ff.AVFormat_guess_format("", url, "")
58+
}
59+
if options.oformat == nil {
60+
return nil, ErrBadParameter.With("unable to guess the output format")
61+
}
62+
63+
// Allocate the output media context
64+
ctx, err := ff.AVFormat_create_file(url, ofmt)
65+
if err != nil {
66+
return nil, err
67+
} else {
68+
writer.output = ctx
69+
}
70+
71+
// Continue with open
72+
return writer.open(options)
73+
}
74+
75+
// Create a new writer with an io.Writer and options
4276
func NewWriter(w io.Writer, opt ...Opt) (*Writer, error) {
4377
options := newOpts()
4478
writer := new(Writer)
@@ -68,6 +102,11 @@ func NewWriter(w io.Writer, opt ...Opt) (*Writer, error) {
68102
writer.output = ctx
69103
}
70104

105+
// Continue with open
106+
return writer.open(options)
107+
}
108+
109+
func (writer *Writer) open(options *opts) (*Writer, error) {
71110
// Create codec contexts for each stream
72111
var result error
73112
keys := sort.IntSlice(maps.Keys(options.streams))
@@ -86,7 +125,25 @@ func NewWriter(w io.Writer, opt ...Opt) (*Writer, error) {
86125
return nil, errors.Join(result, writer.Close())
87126
}
88127

89-
// Write the header
128+
// Add metadata
129+
metadata := ff.AVUtil_dict_alloc()
130+
if metadata == nil {
131+
return nil, errors.Join(errors.New("unable to allocate metadata dictionary"), writer.Close())
132+
}
133+
for _, entry := range options.metadata {
134+
// Ignore artwork fields
135+
if entry.Key == MetaArtwork || entry.Key == "" || entry.Value == nil {
136+
continue
137+
}
138+
// Set dictionary entry
139+
if err := ff.AVUtil_dict_set(metadata, entry.Key, fmt.Sprint(entry.Value), ff.AV_DICT_APPEND); err != nil {
140+
return nil, errors.Join(err, writer.Close())
141+
}
142+
}
143+
144+
// Set metadata, write the header
145+
// Metadata ownership is transferred to the output context
146+
writer.output.SetMetadata(metadata)
90147
if err := ff.AVFormat_write_header(writer.output, nil); err != nil {
91148
return nil, errors.Join(err, writer.Close())
92149
} else {
@@ -97,6 +154,7 @@ func NewWriter(w io.Writer, opt ...Opt) (*Writer, error) {
97154
return writer, nil
98155
}
99156

157+
// Close a writer and release resources
100158
func (w *Writer) Close() error {
101159
var result error
102160

@@ -148,7 +206,9 @@ func (w *Writer) Encode(in EncoderFrameFn, out EncoderPacketFn) error {
148206
}
149207
if out == nil {
150208
// By default, write packet to output
151-
out = w.Write
209+
out = func(pkt *ff.AVPacket, tb *ff.AVRational) error {
210+
return w.Write(pkt)
211+
}
152212
}
153213

154214
// Initialise encoders
@@ -159,29 +219,41 @@ func (w *Writer) Encode(in EncoderFrameFn, out EncoderPacketFn) error {
159219
return ErrBadParameter.Withf("duplicate stream %v", stream)
160220
}
161221
encoders[stream] = encoder
222+
223+
// Initialize the encoder
224+
encoder.eof = false
162225
}
163226

164-
// Continue until all encoders have returned io.EOF
227+
// Continue until all encoders have returned io.EOF and have been flushed
165228
for {
166229
// No more encoding to do
167230
if len(encoders) == 0 {
168231
break
169232
}
233+
234+
// TODO: We get the encoder with the lowest timestamp
170235
for stream, encoder := range encoders {
171-
// Receive a frame for the encoder
172-
frame, err := in(stream)
173-
if errors.Is(err, io.EOF) {
174-
fmt.Println("EOF for frame on stream", stream)
175-
delete(encoders, stream)
176-
} else if err != nil {
236+
var frame *ff.AVFrame
237+
var err error
238+
239+
// Receive a frame if not EOF
240+
if !encoder.eof {
241+
frame, err = in(stream)
242+
if errors.Is(err, io.EOF) {
243+
encoder.eof = true
244+
} else if err != nil {
245+
return fmt.Errorf("stream %v: %w", stream, err)
246+
}
247+
}
248+
249+
// Send a frame for encoding
250+
if err := encoder.Encode(frame, out); err != nil {
177251
return fmt.Errorf("stream %v: %w", stream, err)
178-
} else if frame == nil {
179-
return fmt.Errorf("stream %v: nil frame received", stream)
180-
} else if err := encoder.Encode(frame, out); errors.Is(err, io.EOF) {
181-
fmt.Println("EOF for packet on stream", stream)
252+
}
253+
254+
// If eof then delete the encoder
255+
if encoder.eof {
182256
delete(encoders, stream)
183-
} else if err != nil {
184-
return fmt.Errorf("stream %v: %w", stream, err)
185257
}
186258
}
187259
}
@@ -190,7 +262,8 @@ func (w *Writer) Encode(in EncoderFrameFn, out EncoderPacketFn) error {
190262
return nil
191263
}
192264

193-
// Write a packet to the output
265+
// Write a packet to the output. If you intercept the packets in the
266+
// Encode method, then you can use this method to write packets to the output.
194267
func (w *Writer) Write(packet *ff.AVPacket) error {
195268
return ff.AVCodec_interleaved_write_frame(w.output, packet)
196269
}

pkg/ffmpeg/writer_test.go

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func Test_writer_001(t *testing.T) {
2525
// Create a writer with an audio stream
2626
writer, err := ffmpeg.NewWriter(w,
2727
ffmpeg.OptOutputFormat(w.Name()),
28+
ffmpeg.OptMetadata(ffmpeg.NewMetadata("title", t.Name())),
2829
ffmpeg.OptAudioStream(1, ffmpeg.AudioPar("fltp", "mono", 22050)),
2930
)
3031
if !assert.NoError(err) {
@@ -40,21 +41,66 @@ func Test_writer_001(t *testing.T) {
4041
defer audio.Close()
4142

4243
// Write frames
43-
n := 0
44+
duration := 1000 * time.Minute
4445
assert.NoError(writer.Encode(func(stream int) (*ff.AVFrame, error) {
4546
frame := audio.Frame()
46-
t.Log("Frame s", frame.Time().Truncate(time.Millisecond))
47-
if frame.Time() > 10*time.Second {
47+
if frame.Time() >= duration {
4848
return nil, io.EOF
4949
} else {
50+
t.Log("Frame s", frame.Time().Truncate(time.Millisecond))
5051
return frame.(*ffmpeg.Frame).AVFrame(), nil
5152
}
52-
}, func(packet *ff.AVPacket) error {
53+
}, func(packet *ff.AVPacket, timebase *ff.AVRational) error {
5354
if packet != nil {
54-
t.Log("Packet ts", packet.Pts())
55-
n += packet.Size()
55+
t.Log("Packet", packet)
5656
}
5757
return writer.Write(packet)
5858
}))
59-
t.Log("Written", n, "bytes to", w.Name())
59+
t.Log("Written to", w.Name())
60+
}
61+
62+
func Test_writer_002(t *testing.T) {
63+
assert := assert.New(t)
64+
65+
// Write to a file
66+
w, err := os.CreateTemp("", t.Name()+"_*.mp3")
67+
if !assert.NoError(err) {
68+
t.FailNow()
69+
}
70+
defer w.Close()
71+
72+
// Create a writer with an audio stream
73+
writer, err := ffmpeg.Create(w.Name(),
74+
ffmpeg.OptMetadata(ffmpeg.NewMetadata("title", t.Name())),
75+
ffmpeg.OptAudioStream(1, ffmpeg.AudioPar("fltp", "mono", 22050)),
76+
)
77+
if !assert.NoError(err) {
78+
t.FailNow()
79+
}
80+
defer writer.Close()
81+
82+
// Make an audio generator
83+
audio, err := generator.NewSine(440, -5, writer.Stream(1).Par())
84+
if !assert.NoError(err) {
85+
t.FailNow()
86+
}
87+
defer audio.Close()
88+
89+
// Write frames
90+
duration := 1000 * time.Minute
91+
assert.NoError(writer.Encode(func(stream int) (*ff.AVFrame, error) {
92+
frame := audio.Frame()
93+
if frame.Time() >= duration {
94+
return nil, io.EOF
95+
} else {
96+
t.Log("Frame s", frame.Time().Truncate(time.Millisecond))
97+
return frame.(*ffmpeg.Frame).AVFrame(), nil
98+
}
99+
}, func(packet *ff.AVPacket, timebase *ff.AVRational) error {
100+
if packet != nil {
101+
t.Log("Packet", packet)
102+
}
103+
return writer.Write(packet)
104+
}))
105+
t.Log("Written to", w.Name())
60106
}

0 commit comments

Comments
 (0)