From 6310a21e37c475e07c80c6fd91e9428ef6fd4b79 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Mon, 15 Mar 2021 10:06:49 +0100 Subject: [PATCH] Updated ffmpeg --- cmd/audioid/app.go | 2 +- media.go | 10 ++-- pkg/media/ffmpeg/audioprofile.go | 89 +++++++++++++++++++++++++++++- pkg/media/ffmpeg/codec.go | 3 +- pkg/media/ffmpeg/frame.go | 4 +- pkg/media/ffmpeg/manager.go | 21 +++++-- pkg/media/ffmpeg/stream.go | 69 +++++++++++++++++++++-- pkg/media/ffmpeg/streammap_test.go | 31 ----------- 8 files changed, 180 insertions(+), 49 deletions(-) diff --git a/cmd/audioid/app.go b/cmd/audioid/app.go index ec4c0de3..22c5b02d 100644 --- a/cmd/audioid/app.go +++ b/cmd/audioid/app.go @@ -60,7 +60,7 @@ func (this *app) Decode(ctx context.Context, file gopi.MediaInput) error { // Decode frames return file.Read(ctx, streams[0:1], func(ctx gopi.MediaDecodeContext, packet gopi.MediaPacket) error { return file.DecodeFrameIterator(ctx, packet, func(frame gopi.MediaFrame) error { - this.Print("Decoded", ctx.Frame(), " => ", frame) + this.Print("Decoded", ctx.Stream(), ctx.Frame(), " => ", frame) return nil }) }) diff --git a/media.go b/media.go index b0610759..5132c6f2 100644 --- a/media.go +++ b/media.go @@ -133,11 +133,13 @@ type MediaCodec interface { //////////////////////////////////////////////////////////////////////////////// // MEDIA STREAMS, PACKETS AND FRAMES -// MediaStream is a stream of packets from a media object +// MediaStream is a stream of packets from a media object, with a defined +// codec and audio or video profile type MediaStream interface { - Index() int // Stream index - Flags() MediaFlag // Flags for the stream (Audio, Video, etc) - Codec() MediaCodec // Return codec and parameters + Index() int // Stream index + Flags() MediaFlag // Flags for the stream (Audio, Video, etc) + Codec() MediaCodec // Return codec and parameters + Profile() MediaProfile // Return audio or video profile for stream } // MediaPacket is a packet of data from a stream diff --git a/pkg/media/ffmpeg/audioprofile.go b/pkg/media/ffmpeg/audioprofile.go index 9e55c58b..516548ec 100644 --- a/pkg/media/ffmpeg/audioprofile.go +++ b/pkg/media/ffmpeg/audioprofile.go @@ -4,6 +4,7 @@ package ffmpeg import ( "fmt" + "sync" gopi "github.com/djthorpe/gopi/v3" ffmpeg "github.com/djthorpe/gopi/v3/pkg/sys/ffmpeg" @@ -13,9 +14,14 @@ import ( // TYPES type AudioProfile struct { + sync.RWMutex + fmt ffmpeg.AVSampleFormat rate uint channels uint + layout ffmpeg.AVChannelLayout + ctx *ffmpeg.SwrContext + frame *ffmpeg.AVFrame } //////////////////////////////////////////////////////////////////////////////// @@ -35,17 +41,98 @@ func NewAudioProfile(fmt gopi.AudioFormat, rate uint, layout gopi.AudioChannelLa this.channels = layout.Channels } + // Return success + return this +} + +func (this *AudioProfile) Dispose() error { + this.RWMutex.Lock() + defer this.RWMutex.Unlock() + + // Free resources + if this.ctx != nil { + this.ctx.Free() + } + if this.frame != nil { + this.frame.Free() + } + + // Release resources + this.ctx = nil + this.frame = nil + // Return success return nil } //////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS +// PROPERTIES func (this *AudioProfile) Flags() gopi.MediaFlag { return gopi.MEDIA_FLAG_AUDIO } +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// Resample returns resampled audio frames which adhere to profile. The returned value +// is either a frame or nil if the resampling operation is still in process. After all +// frames have been called into this method a final call with nil is required +// to flush the last resampled frame. +func (this *AudioProfile) Resample(src *ffmpeg.AVFrame) (*ffmpeg.AVFrame, error) { + this.RWMutex.Lock() + defer this.RWMutex.Unlock() + + // If Resample is called with nil then Flush frame + if src == nil && this.ctx != nil && this.frame != nil { + if err := this.ctx.FlushFrame(this.frame); err != nil { + return nil, err + } else if this.frame.NumSamples() > 0 { + return this.frame, nil + } else { + return nil, nil + } + } + + // Return error if called with nil + if src == nil { + return nil, gopi.ErrBadParameter.WithPrefix("Resample") + } + + // Check incoming frame parameters and create context + if src_fmt := src.SampleFormat(); src_fmt == ffmpeg.AV_SAMPLE_FMT_NONE { + return nil, gopi.ErrBadParameter.WithPrefix("Resample") + } else if this.ctx == nil { + // Initialize frame and context + if dest := ffmpeg.NewAudioFrame(this.fmt, int(this.rate), this.layout); dest == nil { + return nil, gopi.ErrInternalAppError.WithPrefix("Resample") + } else if ctx := ffmpeg.NewSwrContextEx(src_fmt, this.fmt, src.SampleRate(), int(this.rate), src.ChannelLayout(), this.layout); ctx == nil { + dest.Free() + return nil, gopi.ErrInternalAppError.WithPrefix("Resample") + } else if err := this.ctx.ConfigFrame(dest, src); err != nil { + dest.Free() + ctx.Free() + return nil, err + } else if ctx.IsInitialized() == false { + dest.Free() + ctx.Free() + return nil, gopi.ErrUnexpectedResponse.WithPrefix("Resample") + } else { + this.ctx = ctx + this.frame = dest + } + } + + // Resample frame and return the frame if there is data else return nil + if err := this.ctx.ConvertFrame(this.frame, src); err != nil { + return nil, err + } else if this.frame.NumSamples() > 0 { + return this.frame, nil + } else { + return nil, nil + } +} + //////////////////////////////////////////////////////////////////////////////// // STRINGIFY diff --git a/pkg/media/ffmpeg/codec.go b/pkg/media/ffmpeg/codec.go index 39b2f76c..78cf2fcc 100644 --- a/pkg/media/ffmpeg/codec.go +++ b/pkg/media/ffmpeg/codec.go @@ -38,9 +38,10 @@ func NewCodecWithParameters(ctx *ffmpeg.AVCodecParameters) *codec { } } -func (this *codec) Release() { +func (this *codec) Release() error { this.ctx = nil this.codec = nil + return nil } //////////////////////////////////////////////////////////////////////////////// diff --git a/pkg/media/ffmpeg/frame.go b/pkg/media/ffmpeg/frame.go index 346da295..9284a2d0 100644 --- a/pkg/media/ffmpeg/frame.go +++ b/pkg/media/ffmpeg/frame.go @@ -126,10 +126,12 @@ func (this *frame) String() string { str := "" } diff --git a/pkg/media/ffmpeg/manager.go b/pkg/media/ffmpeg/manager.go index 241024d3..c72365a9 100644 --- a/pkg/media/ffmpeg/manager.go +++ b/pkg/media/ffmpeg/manager.go @@ -22,12 +22,13 @@ type Manager struct { gopi.Logger sync.Mutex - in []*inputctx - out []*outputctx + in []*inputctx + out []*outputctx + audioprofile []*AudioProfile } //////////////////////////////////////////////////////////////////////////////// -// NEW +// LIFECYCLE func (this *Manager) New(gopi.Config) error { if this.Logger == nil { @@ -72,6 +73,13 @@ func (this *Manager) Dispose() error { } } + // Free all audio profiles + for _, profile := range this.audioprofile { + if err := profile.Dispose(); err != nil { + result = multierror.Append(result, err) + } + } + // Deinit ffmpeg.AVFormatDeinit() @@ -81,6 +89,7 @@ func (this *Manager) Dispose() error { // Release resources this.in = nil this.out = nil + this.audioprofile = nil // Return any errors return result @@ -248,7 +257,11 @@ func (this *Manager) ListCodecs(name string, flags gopi.MediaFlag) []gopi.MediaC // PUBLIC METHODS - PROFILES func (this *Manager) AudioProfile(fmt gopi.AudioFormat, rate uint, layout gopi.AudioChannelLayout) gopi.MediaProfile { - return NewAudioProfile(fmt, rate, layout) + profile := NewAudioProfile(fmt, rate, layout) + if profile != nil { + this.audioprofile = append(this.audioprofile, profile) + } + return profile } //////////////////////////////////////////////////////////////////////////////// diff --git a/pkg/media/ffmpeg/stream.go b/pkg/media/ffmpeg/stream.go index 28f721d2..541ed10c 100644 --- a/pkg/media/ffmpeg/stream.go +++ b/pkg/media/ffmpeg/stream.go @@ -4,15 +4,20 @@ package ffmpeg import ( "fmt" + "sync" gopi "github.com/djthorpe/gopi/v3" ffmpeg "github.com/djthorpe/gopi/v3/pkg/sys/ffmpeg" + multierror "github.com/hashicorp/go-multierror" ) //////////////////////////////////////////////////////////////////////////////// // TYPES type stream struct { + sync.RWMutex + *AudioProfile + ctx *ffmpeg.AVStream codec *codec } @@ -23,6 +28,8 @@ type stream struct { // NewStream returns a stream object, can optiomally copy codec // parameters from another stream if source is not set to nil func NewStream(ctx *ffmpeg.AVStream, source *stream) *stream { + this := new(stream) + if ctx == nil { return nil } @@ -30,26 +37,55 @@ func NewStream(ctx *ffmpeg.AVStream, source *stream) *stream { if codec := NewCodecWithParameters(ctx.CodecPar()); codec == nil { return nil } else { - return &stream{ctx, codec} + this.ctx = ctx + this.codec = codec } } else { if codec := NewCodecWithParameters(source.ctx.CodecPar()); codec == nil { return nil } else { - return &stream{ctx, codec} + this.ctx = ctx + this.codec = codec } } + + // Return success + return this } -func (this *stream) Release() { +func (this *stream) Release() error { + this.RWMutex.Lock() + defer this.RWMutex.Unlock() + + // Release + var result error + if this.codec != nil { + if err := this.codec.Release(); err != nil { + result = multierror.Append(result, err) + } + } + if this.AudioProfile != nil { + if err := this.AudioProfile.Dispose(); err != nil { + result = multierror.Append(result, err) + } + } + + // Set instance variables to nil this.ctx = nil this.codec = nil + this.AudioProfile = nil + + // Return any errors + return result } //////////////////////////////////////////////////////////////////////////////// // METHODS - STREAM func (this *stream) Index() int { + this.RWMutex.RLock() + defer this.RWMutex.RUnlock() + if this.ctx == nil { return -1 } else { @@ -58,9 +94,11 @@ func (this *stream) Index() int { } func (this *stream) Flags() gopi.MediaFlag { - flags := gopi.MEDIA_FLAG_NONE + this.RWMutex.RLock() + defer this.RWMutex.RUnlock() // Return NONE if released + flags := gopi.MEDIA_FLAG_NONE if this.ctx == nil { return flags } @@ -84,6 +122,9 @@ func (this *stream) Flags() gopi.MediaFlag { } func (this *stream) Codec() gopi.MediaCodec { + this.RWMutex.RLock() + defer this.RWMutex.RUnlock() + if this.ctx == nil { return nil } else { @@ -91,6 +132,17 @@ func (this *stream) Codec() gopi.MediaCodec { } } +func (this *stream) Profile() gopi.MediaProfile { + this.RWMutex.RLock() + defer this.RWMutex.RUnlock() + + if this.AudioProfile != nil { + return this.AudioProfile + } else { + return nil + } +} + func (this *stream) NewContextWithOptions(options *ffmpeg.AVDictionary) *ffmpeg.AVCodecContext { if this.ctx == nil || this.codec == nil { return nil @@ -114,8 +166,13 @@ func (this *stream) String() string { str := "" } diff --git a/pkg/media/ffmpeg/streammap_test.go b/pkg/media/ffmpeg/streammap_test.go index 6b5500f3..edb90da0 100644 --- a/pkg/media/ffmpeg/streammap_test.go +++ b/pkg/media/ffmpeg/streammap_test.go @@ -6,7 +6,6 @@ import ( "testing" ffmpeg "github.com/djthorpe/gopi/v3/pkg/media/ffmpeg" - bindings "github.com/djthorpe/gopi/v3/pkg/sys/ffmpeg" ) func Test_StreamMap_001(t *testing.T) { @@ -14,33 +13,3 @@ func Test_StreamMap_001(t *testing.T) { t.Error("Unexpected nil return") } } - -func Test_StreamMap_002(t *testing.T) { - streammap := ffmpeg.NewStreamMap() - if err := streammap.Set(nil, nil); err == nil { - t.Fatal("Expected error return") - } - ctx := bindings.NewAVFormatContext() - if ctx == nil { - t.Fatal("Unexpected nil return") - } - if err := ctx.OpenInput(SAMPLE_FILE, nil); err != nil { - t.Fatal(err) - } - defer ctx.CloseInput() - if len(ctx.Streams()) == 0 { - t.Fatal("Unexpected zero streams") - } - for _, stream := range ctx.Streams() { - if err := streammap.Add(stream, nil); err != nil { - t.Error(err, stream) - } - if out := streammap.Get(stream); out != nil { - t.Error("Unexpected nil return", stream) - } - streammap.Set(stream, stream) - if out := streammap.Get(stream); out != stream { - t.Error("Unexpected nil return", stream) - } - } -}