Skip to content

Commit c142e8e

Browse files
committed
Fixed timebases
1 parent 36dd616 commit c142e8e

File tree

6 files changed

+177
-79
lines changed

6 files changed

+177
-79
lines changed

cmd/examples/encode/context.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"os"
6+
"os/signal"
7+
)
8+
9+
///////////////////////////////////////////////////////////////////////////////
10+
// PUBLIC METHODS
11+
12+
// ContextForSignal returns a context object which is cancelled when a signal
13+
// is received. It returns nil if no signal parameter is provided
14+
func ContextForSignal(signals ...os.Signal) context.Context {
15+
if len(signals) == 0 {
16+
return nil
17+
}
18+
19+
ch := make(chan os.Signal, 1)
20+
ctx, cancel := context.WithCancel(context.Background())
21+
22+
// Send message on channel when signal received
23+
signal.Notify(ch, signals...)
24+
25+
// When any signal received, call cancel
26+
go func() {
27+
<-ch
28+
cancel()
29+
}()
30+
31+
// Return success
32+
return ctx
33+
}

cmd/examples/encode/main.go

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

33
import (
4+
"context"
5+
"errors"
46
"fmt"
57
"io"
68
"log"
79
"os"
10+
"syscall"
811

912
// Packages
1013
ffmpeg "github.com/mutablelogic/go-media/pkg/ffmpeg"
@@ -13,9 +16,14 @@ import (
1316

1417
// This example encodes an audio an video stream to a file
1518
func main() {
19+
// Check we have a filename
20+
if len(os.Args) != 2 {
21+
log.Fatal("Usage: encode filename")
22+
}
23+
1624
// Create a new file with an audio and video stream
1725
file, err := ffmpeg.Create(os.Args[1],
18-
ffmpeg.OptStream(1, ffmpeg.VideoPar("yuv420p", "1280x720", 30)),
26+
ffmpeg.OptStream(1, ffmpeg.VideoPar("yuv420p", "1280x720", 25, ffmpeg.NewMetadata("crf", 2))),
1927
ffmpeg.OptStream(2, ffmpeg.AudioPar("fltp", "mono", 22050)),
2028
)
2129
if err != nil {
@@ -39,10 +47,13 @@ func main() {
3947
}
4048
defer audio.Close()
4149

50+
// Bail out when we receive a signal
51+
ctx := ContextForSignal(os.Interrupt, syscall.SIGQUIT)
52+
4253
// Write 90 seconds, passing video and audio frames to the encoder
4354
// and returning io.EOF when the duration is reached
4455
duration := float64(90)
45-
err = file.Encode(func(stream int) (*ffmpeg.Frame, error) {
56+
err = file.Encode(ctx, func(stream int) (*ffmpeg.Frame, error) {
4657
var frame *ffmpeg.Frame
4758
switch stream {
4859
case 1:
@@ -51,12 +62,12 @@ func main() {
5162
frame = audio.Frame()
5263
}
5364
if frame != nil && frame.Ts() < duration {
54-
fmt.Print(".")
65+
fmt.Println(stream, frame.Ts())
5566
return frame, nil
5667
}
5768
return nil, io.EOF
5869
}, nil)
59-
if err != nil {
70+
if err != nil && !errors.Is(err, context.Canceled) {
6071
log.Fatal(err)
6172
}
6273
fmt.Print("\n")

pkg/ffmpeg/encoder.go

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func NewEncoder(ctx *ff.AVFormatContext, stream int, par *Par) (*Encoder, error)
7272
}
7373

7474
// Create the stream
75-
if streamctx := ff.AVFormat_new_stream(ctx, nil); streamctx == nil {
75+
if streamctx := ff.AVFormat_new_stream(ctx, codec); streamctx == nil {
7676
ff.AVCodec_free_context(encoder.ctx)
7777
return nil, ErrInternalAppError.With("could not allocate stream")
7878
} else {
@@ -86,20 +86,40 @@ func NewEncoder(ctx *ff.AVFormatContext, stream int, par *Par) (*Encoder, error)
8686
encoder.ctx.SetFlags(encoder.ctx.Flags() | ff.AV_CODEC_FLAG_GLOBAL_HEADER)
8787
}
8888

89+
// Get the options
90+
opts := par.newOpts()
91+
if opts == nil {
92+
ff.AVCodec_free_context(encoder.ctx)
93+
return nil, ErrInternalAppError.With("could not allocate options dictionary")
94+
}
95+
defer ff.AVUtil_dict_free(opts)
96+
8997
// Open it
90-
if err := ff.AVCodec_open(encoder.ctx, codec, nil); err != nil {
98+
if err := ff.AVCodec_open(encoder.ctx, codec, opts); err != nil {
9199
ff.AVCodec_free_context(encoder.ctx)
92100
return nil, ErrInternalAppError.Withf("codec_open: %v", err)
93101
}
94102

103+
// If there are any non-consumed options, then error
104+
var result error
105+
for _, key := range ff.AVUtil_dict_keys(opts) {
106+
result = errors.Join(result, ErrBadParameter.Withf("Stream %d: invalid codec option %q", stream, key))
107+
}
108+
if result != nil {
109+
ff.AVCodec_free_context(encoder.ctx)
110+
return nil, result
111+
}
112+
95113
// Copy parameters to stream
96114
if err := ff.AVCodec_parameters_from_context(encoder.stream.CodecPar(), encoder.ctx); err != nil {
97115
ff.AVCodec_free_context(encoder.ctx)
98116
return nil, err
99-
} else {
100-
encoder.stream.SetTimeBase(par.timebase)
101117
}
102118

119+
// Hint what timebase we want to encode at. This will change when writing the
120+
// headers for the encoding process
121+
encoder.stream.SetTimeBase(par.timebase)
122+
103123
// Create a packet
104124
packet := ff.AVCodec_packet_alloc()
105125
if packet == nil {
@@ -167,7 +187,7 @@ func (e *Encoder) Encode(frame *Frame, fn EncoderPacketFn) error {
167187
// Return the codec parameters
168188
func (e *Encoder) Par() *Par {
169189
par := new(Par)
170-
par.timebase = e.stream.TimeBase()
190+
par.timebase = e.ctx.TimeBase()
171191
if err := ff.AVCodec_parameters_from_context(&par.AVCodecParameters, e.ctx); err != nil {
172192
return nil
173193
} else {
@@ -180,9 +200,9 @@ func (e *Encoder) nextPts(frame *Frame) int64 {
180200
next_pts := int64(0)
181201
switch e.ctx.Codec().Type() {
182202
case ff.AVMEDIA_TYPE_AUDIO:
183-
next_pts = ff.AVUtil_rational_rescale_q(int64(frame.NumSamples()), ff.AVUtil_rational(1, frame.SampleRate()), e.stream.TimeBase())
203+
next_pts = ff.AVUtil_rational_rescale_q(int64(frame.NumSamples()), frame.TimeBase(), e.stream.TimeBase())
184204
case ff.AVMEDIA_TYPE_VIDEO:
185-
next_pts = ff.AVUtil_rational_rescale_q(1, ff.AVUtil_rational_invert(e.ctx.Framerate()), e.stream.TimeBase())
205+
next_pts = ff.AVUtil_rational_rescale_q(1, frame.TimeBase(), e.stream.TimeBase())
186206
default:
187207
// Dunno what to do with subtitle and data streams yet
188208
fmt.Println("TODO: next_pts for subtitle and data streams")

pkg/ffmpeg/par.go

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,25 @@ import (
1818

1919
type Par struct {
2020
ff.AVCodecParameters
21+
opts []media.Metadata
2122
timebase ff.AVRational
2223
}
2324

2425
type jsonPar struct {
25-
Par ff.AVCodecParameters `json:"parameters"`
26-
Timebase ff.AVRational `json:"timebase"`
26+
ff.AVCodecParameters
27+
Timebase ff.AVRational `json:"timebase"`
28+
Opts []media.Metadata `json:"options"`
2729
}
2830

2931
///////////////////////////////////////////////////////////////////////////////
3032
// LIFECYCLE
3133

32-
func NewAudioPar(samplefmt string, channellayout string, samplerate int) (*Par, error) {
34+
// Create new audio parameters with sample format, channel layout and sample rate
35+
// plus any additional options which is used for creating a stream
36+
func NewAudioPar(samplefmt string, channellayout string, samplerate int, opts ...media.Metadata) (*Par, error) {
3337
par := new(Par)
3438
par.SetCodecType(ff.AVMEDIA_TYPE_AUDIO)
39+
par.opts = opts
3540

3641
// Sample Format
3742
if samplefmt_ := ff.AVUtil_get_sample_fmt(samplefmt); samplefmt_ == ff.AV_SAMPLE_FMT_NONE {
@@ -59,9 +64,12 @@ func NewAudioPar(samplefmt string, channellayout string, samplerate int) (*Par,
5964
return par, nil
6065
}
6166

62-
func NewVideoPar(pixfmt string, size string, framerate float64) (*Par, error) {
67+
// Create new video parameters with pixel format, frame size, framerate
68+
// plus any additional options which is used for creating a stream
69+
func NewVideoPar(pixfmt string, size string, framerate float64, opts ...media.Metadata) (*Par, error) {
6370
par := new(Par)
6471
par.SetCodecType(ff.AVMEDIA_TYPE_VIDEO)
72+
par.opts = opts
6573

6674
// Pixel Format
6775
if pixfmt_ := ff.AVUtil_get_pix_fmt(pixfmt); pixfmt_ == ff.AV_PIX_FMT_NONE {
@@ -88,37 +96,22 @@ func NewVideoPar(pixfmt string, size string, framerate float64) (*Par, error) {
8896
// Set default sample aspect ratio
8997
par.SetSampleAspectRatio(ff.AVUtil_rational(1, 1))
9098

91-
// TODO: Set profile, codec and bitrate and any other parameters
92-
93-
/* TODO
94-
c->gop_size = 12; // emit one intra frame every twelve frames at most
95-
c->pix_fmt = STREAM_PIX_FMT;
96-
if (c->codec_id == AV_CODEC_ID_MPEG2VIDEO) {
97-
// just for testing, we also add B-frames
98-
c->max_b_frames = 2;
99-
}
100-
if (c->codec_id == AV_CODEC_ID_MPEG1VIDEO) {
101-
// Needed to avoid using macroblocks in which some coeffs overflow.
102-
// This does not happen with normal video, it just happens here as
103-
// the motion of the chroma plane does not match the luma plane.
104-
c->mb_decision = 2;
105-
}
106-
*/
107-
10899
// Return success
109100
return par, nil
110101
}
111102

112-
func AudioPar(samplefmt string, channellayout string, samplerate int) *Par {
113-
if par, err := NewAudioPar(samplefmt, channellayout, samplerate); err != nil {
103+
// Create audio parameters. If there is an error, then this function will panic
104+
func AudioPar(samplefmt string, channellayout string, samplerate int, opts ...media.Metadata) *Par {
105+
if par, err := NewAudioPar(samplefmt, channellayout, samplerate, opts...); err != nil {
114106
panic(err)
115107
} else {
116108
return par
117109
}
118110
}
119111

120-
func VideoPar(pixfmt string, size string, framerate float64) *Par {
121-
if par, err := NewVideoPar(pixfmt, size, framerate); err != nil {
112+
// Create video parameters. If there is an error, then this function will panic
113+
func VideoPar(pixfmt string, size string, framerate float64, opts ...media.Metadata) *Par {
114+
if par, err := NewVideoPar(pixfmt, size, framerate, opts...); err != nil {
122115
panic(err)
123116
} else {
124117
return par
@@ -130,8 +123,9 @@ func VideoPar(pixfmt string, size string, framerate float64) *Par {
130123

131124
func (ctx *Par) MarshalJSON() ([]byte, error) {
132125
return json.Marshal(jsonPar{
133-
Par: ctx.AVCodecParameters,
134-
Timebase: ctx.timebase,
126+
AVCodecParameters: ctx.AVCodecParameters,
127+
Timebase: ctx.timebase,
128+
Opts: ctx.opts,
135129
})
136130
}
137131

@@ -196,6 +190,19 @@ func (ctx *Par) CopyToCodecContext(codec *ff.AVCodecContext) error {
196190
///////////////////////////////////////////////////////////////////////////////
197191
// PRIVATE METHODS
198192

193+
// Return options as a dictionary, which needs to be freed after use
194+
// by the caller method
195+
func (ctx *Par) newOpts() *ff.AVDictionary {
196+
dict := ff.AVUtil_dict_alloc()
197+
for _, opt := range ctx.opts {
198+
if err := ff.AVUtil_dict_set(dict, opt.Key(), opt.Value(), ff.AV_DICT_APPEND); err != nil {
199+
ff.AVUtil_dict_free(dict)
200+
return nil
201+
}
202+
}
203+
return dict
204+
}
205+
199206
func (ctx *Par) copyAudioCodec(codec *ff.AVCodecContext) error {
200207
codec.SetSampleFormat(ctx.SampleFormat())
201208
codec.SetSampleRate(ctx.Samplerate())

0 commit comments

Comments
 (0)