Skip to content

Commit

Permalink
Fixed timebases
Browse files Browse the repository at this point in the history
  • Loading branch information
djthorpe committed Jul 6, 2024
1 parent 36dd616 commit c142e8e
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 79 deletions.
33 changes: 33 additions & 0 deletions cmd/examples/encode/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package main

import (
"context"
"os"
"os/signal"
)

///////////////////////////////////////////////////////////////////////////////
// PUBLIC METHODS

// ContextForSignal returns a context object which is cancelled when a signal
// is received. It returns nil if no signal parameter is provided
func ContextForSignal(signals ...os.Signal) context.Context {
if len(signals) == 0 {
return nil
}

ch := make(chan os.Signal, 1)
ctx, cancel := context.WithCancel(context.Background())

// Send message on channel when signal received
signal.Notify(ch, signals...)

// When any signal received, call cancel
go func() {
<-ch
cancel()
}()

// Return success
return ctx
}
19 changes: 15 additions & 4 deletions cmd/examples/encode/main.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package main

import (
"context"
"errors"
"fmt"
"io"
"log"
"os"
"syscall"

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

// This example encodes an audio an video stream to a file
func main() {
// Check we have a filename
if len(os.Args) != 2 {
log.Fatal("Usage: encode filename")
}

// Create a new file with an audio and video stream
file, err := ffmpeg.Create(os.Args[1],
ffmpeg.OptStream(1, ffmpeg.VideoPar("yuv420p", "1280x720", 30)),
ffmpeg.OptStream(1, ffmpeg.VideoPar("yuv420p", "1280x720", 25, ffmpeg.NewMetadata("crf", 2))),
ffmpeg.OptStream(2, ffmpeg.AudioPar("fltp", "mono", 22050)),
)
if err != nil {
Expand All @@ -39,10 +47,13 @@ func main() {
}
defer audio.Close()

// Bail out when we receive a signal
ctx := ContextForSignal(os.Interrupt, syscall.SIGQUIT)

// Write 90 seconds, passing video and audio frames to the encoder
// and returning io.EOF when the duration is reached
duration := float64(90)
err = file.Encode(func(stream int) (*ffmpeg.Frame, error) {
err = file.Encode(ctx, func(stream int) (*ffmpeg.Frame, error) {
var frame *ffmpeg.Frame
switch stream {
case 1:
Expand All @@ -51,12 +62,12 @@ func main() {
frame = audio.Frame()
}
if frame != nil && frame.Ts() < duration {
fmt.Print(".")
fmt.Println(stream, frame.Ts())
return frame, nil
}
return nil, io.EOF
}, nil)
if err != nil {
if err != nil && !errors.Is(err, context.Canceled) {
log.Fatal(err)
}
fmt.Print("\n")
Expand Down
34 changes: 27 additions & 7 deletions pkg/ffmpeg/encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func NewEncoder(ctx *ff.AVFormatContext, stream int, par *Par) (*Encoder, error)
}

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

// Get the options
opts := par.newOpts()
if opts == nil {
ff.AVCodec_free_context(encoder.ctx)
return nil, ErrInternalAppError.With("could not allocate options dictionary")
}
defer ff.AVUtil_dict_free(opts)

// Open it
if err := ff.AVCodec_open(encoder.ctx, codec, nil); err != nil {
if err := ff.AVCodec_open(encoder.ctx, codec, opts); err != nil {
ff.AVCodec_free_context(encoder.ctx)
return nil, ErrInternalAppError.Withf("codec_open: %v", err)
}

// If there are any non-consumed options, then error
var result error
for _, key := range ff.AVUtil_dict_keys(opts) {
result = errors.Join(result, ErrBadParameter.Withf("Stream %d: invalid codec option %q", stream, key))
}
if result != nil {
ff.AVCodec_free_context(encoder.ctx)
return nil, result
}

// Copy parameters to stream
if err := ff.AVCodec_parameters_from_context(encoder.stream.CodecPar(), encoder.ctx); err != nil {
ff.AVCodec_free_context(encoder.ctx)
return nil, err
} else {
encoder.stream.SetTimeBase(par.timebase)
}

// Hint what timebase we want to encode at. This will change when writing the
// headers for the encoding process
encoder.stream.SetTimeBase(par.timebase)

// Create a packet
packet := ff.AVCodec_packet_alloc()
if packet == nil {
Expand Down Expand Up @@ -167,7 +187,7 @@ func (e *Encoder) Encode(frame *Frame, fn EncoderPacketFn) error {
// Return the codec parameters
func (e *Encoder) Par() *Par {
par := new(Par)
par.timebase = e.stream.TimeBase()
par.timebase = e.ctx.TimeBase()
if err := ff.AVCodec_parameters_from_context(&par.AVCodecParameters, e.ctx); err != nil {
return nil
} else {
Expand All @@ -180,9 +200,9 @@ func (e *Encoder) nextPts(frame *Frame) int64 {
next_pts := int64(0)
switch e.ctx.Codec().Type() {
case ff.AVMEDIA_TYPE_AUDIO:
next_pts = ff.AVUtil_rational_rescale_q(int64(frame.NumSamples()), ff.AVUtil_rational(1, frame.SampleRate()), e.stream.TimeBase())
next_pts = ff.AVUtil_rational_rescale_q(int64(frame.NumSamples()), frame.TimeBase(), e.stream.TimeBase())
case ff.AVMEDIA_TYPE_VIDEO:
next_pts = ff.AVUtil_rational_rescale_q(1, ff.AVUtil_rational_invert(e.ctx.Framerate()), e.stream.TimeBase())
next_pts = ff.AVUtil_rational_rescale_q(1, frame.TimeBase(), e.stream.TimeBase())
default:
// Dunno what to do with subtitle and data streams yet
fmt.Println("TODO: next_pts for subtitle and data streams")
Expand Down
61 changes: 34 additions & 27 deletions pkg/ffmpeg/par.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,25 @@ import (

type Par struct {
ff.AVCodecParameters
opts []media.Metadata
timebase ff.AVRational
}

type jsonPar struct {
Par ff.AVCodecParameters `json:"parameters"`
Timebase ff.AVRational `json:"timebase"`
ff.AVCodecParameters
Timebase ff.AVRational `json:"timebase"`
Opts []media.Metadata `json:"options"`
}

///////////////////////////////////////////////////////////////////////////////
// LIFECYCLE

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

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

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

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

// TODO: Set profile, codec and bitrate and any other parameters

/* TODO
c->gop_size = 12; // emit one intra frame every twelve frames at most
c->pix_fmt = STREAM_PIX_FMT;
if (c->codec_id == AV_CODEC_ID_MPEG2VIDEO) {
// just for testing, we also add B-frames
c->max_b_frames = 2;
}
if (c->codec_id == AV_CODEC_ID_MPEG1VIDEO) {
// Needed to avoid using macroblocks in which some coeffs overflow.
// This does not happen with normal video, it just happens here as
// the motion of the chroma plane does not match the luma plane.
c->mb_decision = 2;
}
*/

// Return success
return par, nil
}

func AudioPar(samplefmt string, channellayout string, samplerate int) *Par {
if par, err := NewAudioPar(samplefmt, channellayout, samplerate); err != nil {
// Create audio parameters. If there is an error, then this function will panic
func AudioPar(samplefmt string, channellayout string, samplerate int, opts ...media.Metadata) *Par {
if par, err := NewAudioPar(samplefmt, channellayout, samplerate, opts...); err != nil {
panic(err)
} else {
return par
}
}

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

func (ctx *Par) MarshalJSON() ([]byte, error) {
return json.Marshal(jsonPar{
Par: ctx.AVCodecParameters,
Timebase: ctx.timebase,
AVCodecParameters: ctx.AVCodecParameters,
Timebase: ctx.timebase,
Opts: ctx.opts,
})
}

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

// Return options as a dictionary, which needs to be freed after use
// by the caller method
func (ctx *Par) newOpts() *ff.AVDictionary {
dict := ff.AVUtil_dict_alloc()
for _, opt := range ctx.opts {
if err := ff.AVUtil_dict_set(dict, opt.Key(), opt.Value(), ff.AV_DICT_APPEND); err != nil {
ff.AVUtil_dict_free(dict)
return nil
}
}
return dict
}

func (ctx *Par) copyAudioCodec(codec *ff.AVCodecContext) error {
codec.SetSampleFormat(ctx.SampleFormat())
codec.SetSampleRate(ctx.Samplerate())
Expand Down
Loading

0 comments on commit c142e8e

Please sign in to comment.