Skip to content

Commit d6d76ad

Browse files
committed
[kernel/encoder] Fix the InputChanged error
1 parent fe2032f commit d6d76ad

6 files changed

Lines changed: 239 additions & 7 deletions

File tree

codec/encoder.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func (pcmFmt PCMAudioFormat) Equal(other PCMAudioFormat) bool {
7272
return pcmFmt.SampleFormat == other.SampleFormat &&
7373
pcmFmt.SampleRate == other.SampleRate &&
7474
channelLayoutEqual &&
75-
pcmFmt.ChunkSize == other.ChunkSize
75+
(pcmFmt.ChunkSize == other.ChunkSize || pcmFmt.ChunkSize == 0 || other.ChunkSize == 0)
7676
}
7777

7878
type ErrNotDummy struct{}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module github.com/xaionaro-go/avpipeline
22

33
go 1.24.4
44

5-
replace github.com/asticode/go-astiav v0.36.0 => github.com/xaionaro-go/astiav v0.0.0-20251213191451-08d78322abe4
5+
replace github.com/asticode/go-astiav v0.36.0 => github.com/xaionaro-go/astiav v0.0.0-20251214204411-73e53d79a9c8
66

77
require (
88
github.com/anthonynsimon/bild v0.14.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
9393
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
9494
github.com/xaionaro-go/androidetc v0.0.0-20250824193302-b7ecebb3b825 h1:4f2P9/JASUXx2GWhQ6wyGlwOtr+nIfM6PUq9wTHqHmM=
9595
github.com/xaionaro-go/androidetc v0.0.0-20250824193302-b7ecebb3b825/go.mod h1:IvRt8Hto32EGtBEf7A94HVxiYEt7hGXLWabjdnUYmAE=
96-
github.com/xaionaro-go/astiav v0.0.0-20251213191451-08d78322abe4 h1:s9nQJd9HwGXvXoEd7aKUnbWWNUB0mGNv5jeCKIawAhU=
97-
github.com/xaionaro-go/astiav v0.0.0-20251213191451-08d78322abe4/go.mod h1:txVrlvD3ErCtyacQ6WBMarn9+oD1V8vk33dWmVCGQhM=
96+
github.com/xaionaro-go/astiav v0.0.0-20251214204411-73e53d79a9c8 h1:LmYDu/KfW+mEa3m+jDkoDU4w98rm/HMVWeXBX7D5URo=
97+
github.com/xaionaro-go/astiav v0.0.0-20251214204411-73e53d79a9c8/go.mod h1:txVrlvD3ErCtyacQ6WBMarn9+oD1V8vk33dWmVCGQhM=
9898
github.com/xaionaro-go/audio v0.0.0-20250426140416-6a9b3f1c8737 h1:82T1ghLMySGzft1c11Go+GtSQI67Ixk6Mq+nZ52CnnI=
9999
github.com/xaionaro-go/audio v0.0.0-20250426140416-6a9b3f1c8737/go.mod h1:m1JTs/Kqw+iPlQ081sfXA2JPBIylezwe5c3w//oV3Gs=
100100
github.com/xaionaro-go/avcommon v0.0.0-20250823173020-6a2bb1e1f59d h1:++uR850Cqo5qXGTfAq60p2sIfq7oSYK0x0KoKL8B9Ts=

kernel/encoder.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -759,7 +759,7 @@ func (e *streamEncoder) fitFrameForEncoding(
759759
if encPCMFmt.Equal(*inPCMFmt) {
760760
return []*astiav.Frame{input.Frame}, nil
761761
}
762-
resampledFrames, err := e.getResampledFrames(ctx, input.Frame, *inPCMFmt, *encPCMFmt)
762+
resampledFrames, err := e.getResampledFrames(ctx, input.Frame, *encPCMFmt)
763763
if err != nil {
764764
return nil, fmt.Errorf("unable to resample the audio frame: %w", err)
765765
}
@@ -788,15 +788,20 @@ func getPCMAudioFormatFromFrame(
788788
func (e *streamEncoder) getResampledFrames(
789789
ctx context.Context,
790790
inputFrame *astiav.Frame,
791-
inPCMFmt codec.PCMAudioFormat,
792791
outPCMFmt codec.PCMAudioFormat,
793792
) (resampledFrames []*astiav.Frame, _err error) {
793+
// making channels ordered, otherwise resampler may fail
794+
switch inputFrame.ChannelLayout().Channels() {
795+
case 2:
796+
inputFrame.SetChannelLayout(astiav.ChannelLayoutStereo)
797+
}
798+
inPCMFmt := getPCMAudioFormatFromFrame(ctx, inputFrame)
794799
logger.Tracef(ctx, "getResampledFrames: in:%v; out:%v", inPCMFmt, outPCMFmt)
795800
defer func() {
796801
logger.Tracef(ctx, "/getResampledFrames: in:%v; out:%v: %v %v", inPCMFmt, outPCMFmt, resampledFrames, _err)
797802
}()
798803

799-
err := e.prepareResampler(ctx, inPCMFmt, outPCMFmt)
804+
err := e.prepareResampler(ctx, *inPCMFmt, outPCMFmt)
800805
if err != nil {
801806
return nil, fmt.Errorf("unable to prepare the resampler: %w", err)
802807
}

resampler/resampler.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
type Resampler struct {
1414
AudioFifo *astiav.AudioFifo
1515
SoftwareResampleContext *astiav.SoftwareResampleContext
16+
FormatInput *codec.PCMAudioFormat
1617
FormatOutput codec.PCMAudioFormat
1718
ResampledFrame *astiav.Frame
1819
}
@@ -106,6 +107,20 @@ func (r *Resampler) SendFrame(
106107
logger.Tracef(ctx, "SendFrame: %d", in.NbSamples())
107108
defer func() { logger.Tracef(ctx, "/SendFrame: %d: %v", in.NbSamples(), _err) }()
108109

110+
if in == nil {
111+
return fmt.Errorf("input frame is nil")
112+
}
113+
114+
inFormat := getPCMFormatFromFrame(ctx, in)
115+
if r.FormatInput != nil {
116+
if !r.FormatInput.Equal(*inFormat) {
117+
return fmt.Errorf("input frame format changed: was %#+v, now %#+v", *r.FormatInput, *inFormat)
118+
}
119+
} else {
120+
logger.Debugf(ctx, "input frame format: (%d, %s, %s)", inFormat.SampleRate, inFormat.SampleFormat, inFormat.ChannelLayout)
121+
}
122+
r.FormatInput = inFormat
123+
109124
if err := r.SoftwareResampleContext.ConvertFrame(in, r.ResampledFrame); err != nil {
110125
return fmt.Errorf("cannot convert frame: %w", err)
111126
}
@@ -121,6 +136,21 @@ func (r *Resampler) SendFrame(
121136
return nil
122137
}
123138

139+
func getPCMFormatFromFrame(
140+
_ context.Context,
141+
frame *astiav.Frame,
142+
) *codec.PCMAudioFormat {
143+
if frame == nil {
144+
return nil
145+
}
146+
return &codec.PCMAudioFormat{
147+
SampleFormat: frame.SampleFormat(),
148+
SampleRate: frame.SampleRate(),
149+
ChannelLayout: frame.ChannelLayout(),
150+
ChunkSize: frame.NbSamples(),
151+
}
152+
}
153+
124154
func (r *Resampler) receiveFrame(
125155
ctx context.Context,
126156
outputFrame *astiav.Frame,

resampler/resampler_test.go

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package resampler
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/asticode/go-astiav"
8+
"github.com/stretchr/testify/require"
9+
"github.com/xaionaro-go/avpipeline/codec"
10+
"github.com/xaionaro-go/avpipeline/frame"
11+
)
12+
13+
func defaultPCMFormat() codec.PCMAudioFormat {
14+
return codec.PCMAudioFormat{
15+
SampleFormat: astiav.SampleFormatFlt,
16+
SampleRate: 48000,
17+
ChannelLayout: astiav.ChannelLayoutStereo,
18+
ChunkSize: 8,
19+
}
20+
}
21+
22+
func TestGetPCMFormatFromFrame(t *testing.T) {
23+
t.Run("NilFrame", func(t *testing.T) {
24+
t.Parallel()
25+
require.Nil(t, getPCMFormatFromFrame(context.Background(), nil))
26+
})
27+
28+
t.Run("PopulatedFrame", func(t *testing.T) {
29+
t.Parallel()
30+
31+
fr := frame.Pool.Get()
32+
defer frame.Pool.Put(fr)
33+
fr.Unref()
34+
fr.SetSampleFormat(astiav.SampleFormatS16)
35+
fr.SetSampleRate(44100)
36+
fr.SetChannelLayout(astiav.ChannelLayoutMono)
37+
fr.SetNbSamples(123)
38+
39+
got := getPCMFormatFromFrame(context.Background(), fr)
40+
require.NotNil(t, got)
41+
42+
expected := codec.PCMAudioFormat{
43+
SampleFormat: astiav.SampleFormatS16,
44+
SampleRate: 44100,
45+
ChannelLayout: astiav.ChannelLayoutMono,
46+
ChunkSize: 123,
47+
}
48+
require.True(t, expected.Equal(*got))
49+
})
50+
}
51+
52+
func TestResamplerAllocateOutputFrame(t *testing.T) {
53+
t.Parallel()
54+
55+
ctx := context.Background()
56+
fmt := defaultPCMFormat()
57+
r, err := New(ctx, fmt)
58+
require.NoError(t, err)
59+
t.Cleanup(func() { require.NoError(t, r.Close(ctx)) })
60+
61+
out, err := r.AllocateOutputFrame(ctx)
62+
require.NoError(t, err)
63+
t.Cleanup(func() { frame.Pool.Put(out) })
64+
65+
require.Equal(t, fmt.ChunkSize, out.NbSamples())
66+
require.Equal(t, fmt.ChannelLayout, out.ChannelLayout())
67+
require.Equal(t, fmt.SampleFormat, out.SampleFormat())
68+
require.Equal(t, fmt.SampleRate, out.SampleRate())
69+
}
70+
71+
func TestResamplerReceiveFrameFlow(t *testing.T) {
72+
t.Parallel()
73+
74+
ctx := context.Background()
75+
fmt := defaultPCMFormat()
76+
r, err := New(ctx, fmt)
77+
require.NoError(t, err)
78+
t.Cleanup(func() { require.NoError(t, r.Close(ctx)) })
79+
80+
out, err := r.AllocateOutputFrame(ctx)
81+
require.NoError(t, err)
82+
defer frame.Pool.Put(out)
83+
84+
require.ErrorIs(t, r.ReceiveFrame(ctx, out), astiav.ErrEof)
85+
86+
writeSamples(t, r, fmt.ChunkSize/2)
87+
require.ErrorIs(t, r.ReceiveFrame(ctx, out), astiav.ErrEagain)
88+
89+
writeSamples(t, r, fmt.ChunkSize)
90+
require.NoError(t, r.ReceiveFrame(ctx, out))
91+
require.Equal(t, fmt.ChunkSize, out.NbSamples())
92+
93+
flushFrame, err := r.AllocateOutputFrame(ctx)
94+
require.NoError(t, err)
95+
defer frame.Pool.Put(flushFrame)
96+
require.NoError(t, r.Flush(ctx, flushFrame))
97+
require.Equal(t, fmt.ChunkSize/2, flushFrame.NbSamples())
98+
99+
require.ErrorIs(t, r.ReceiveFrame(ctx, out), astiav.ErrEof)
100+
}
101+
102+
func TestResamplerSendFrameValidations(t *testing.T) {
103+
t.Parallel()
104+
105+
ctx := context.Background()
106+
fmt := defaultPCMFormat()
107+
r, err := New(ctx, fmt)
108+
require.NoError(t, err)
109+
t.Cleanup(func() { require.NoError(t, r.Close(ctx)) })
110+
111+
firstFormat := codec.PCMAudioFormat{
112+
SampleFormat: astiav.SampleFormatS16,
113+
SampleRate: 44100,
114+
ChannelLayout: astiav.ChannelLayoutMono,
115+
ChunkSize: 4,
116+
}
117+
118+
secondFormat := codec.PCMAudioFormat{
119+
SampleFormat: astiav.SampleFormatS16,
120+
SampleRate: 48000,
121+
ChannelLayout: astiav.ChannelLayoutMono,
122+
ChunkSize: 4,
123+
}
124+
125+
firstFrame := buildPCMFrame(t, firstFormat)
126+
defer frame.Pool.Put(firstFrame)
127+
128+
secondFrame := buildPCMFrame(t, secondFormat)
129+
defer frame.Pool.Put(secondFrame)
130+
131+
err = r.SendFrame(ctx, firstFrame)
132+
require.NoError(t, err)
133+
require.NotNil(t, r.FormatInput)
134+
require.True(t, firstFormat.Equal(*r.FormatInput))
135+
136+
err = r.SendFrame(ctx, secondFrame)
137+
require.Error(t, err)
138+
require.Contains(t, err.Error(), "input frame format changed")
139+
}
140+
141+
func TestResamplerUnspecifiedChannelOrder(t *testing.T) {
142+
t.Parallel()
143+
144+
ctx := context.Background()
145+
fmtOut := defaultPCMFormat()
146+
r, err := New(ctx, fmtOut)
147+
require.NoError(t, err)
148+
t.Cleanup(func() { require.NoError(t, r.Close(ctx)) })
149+
150+
inputFormat := codec.PCMAudioFormat{
151+
SampleFormat: astiav.SampleFormatS16,
152+
SampleRate: 44100,
153+
ChannelLayout: astiav.ChannelLayoutStereo,
154+
ChunkSize: fmtOut.ChunkSize,
155+
}
156+
inputFormat.ChannelLayout.SetOrder(astiav.ChannelOrderUnspecified)
157+
158+
firstFrame := buildPCMFrame(t, inputFormat)
159+
defer frame.Pool.Put(firstFrame)
160+
161+
secondFrame := buildPCMFrame(t, inputFormat)
162+
defer frame.Pool.Put(secondFrame)
163+
164+
require.NoError(t, r.SendFrame(ctx, firstFrame))
165+
require.NotNil(t, r.FormatInput)
166+
167+
err = r.SendFrame(ctx, secondFrame)
168+
require.Error(t, err)
169+
require.ErrorIs(t, err, astiav.ErrInputChanged)
170+
}
171+
172+
func writeSamples(t *testing.T, r *Resampler, samples int) {
173+
t.Helper()
174+
pcmFrame := buildPCMFrame(t, codec.PCMAudioFormat{
175+
SampleFormat: r.FormatOutput.SampleFormat,
176+
SampleRate: r.FormatOutput.SampleRate,
177+
ChannelLayout: r.FormatOutput.ChannelLayout,
178+
ChunkSize: samples,
179+
})
180+
defer func() {
181+
frame.Pool.Put(pcmFrame)
182+
}()
183+
_, err := r.AudioFifo.Write(pcmFrame)
184+
require.NoError(t, err)
185+
}
186+
187+
func buildPCMFrame(t *testing.T, fmt codec.PCMAudioFormat) *astiav.Frame {
188+
t.Helper()
189+
fr := frame.Pool.Get()
190+
fr.Unref()
191+
fr.SetSampleFormat(fmt.SampleFormat)
192+
fr.SetSampleRate(fmt.SampleRate)
193+
fr.SetChannelLayout(fmt.ChannelLayout)
194+
fr.SetNbSamples(fmt.ChunkSize)
195+
require.NoError(t, fr.AllocBuffer(0))
196+
return fr
197+
}

0 commit comments

Comments
 (0)