package media_test import ( "bytes" "context" "errors" "io" "testing" "git.netflux.io/rob/clipper/config" "git.netflux.io/rob/clipper/generated/mocks" "git.netflux.io/rob/clipper/generated/store" "git.netflux.io/rob/clipper/media" "github.com/google/uuid" "github.com/jackc/pgx/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.uber.org/zap" ) func TestGetSegment(t *testing.T) { mediaSetID := uuid.MustParse("4c440241-cca9-436f-adb0-be074588cf2b") const inFixturePath = "testdata/tone-44100-stereo-int16-30000ms.raw" t.Run("invalid range", func(t *testing.T) { var mockStore mocks.Store var fileStore mocks.FileStore service := media.NewMediaSetService(&mockStore, nil, &fileStore, nil, config.Config{}, zap.NewNop().Sugar()) stream, err := service.GetAudioSegment(context.Background(), mediaSetID, 1, 0, media.AudioFormatMP3) require.Nil(t, stream) require.EqualError(t, err, "invalid range") }) t.Run("error fetching media set", func(t *testing.T) { var mockStore mocks.Store mockStore.On("GetMediaSet", mock.Anything, mediaSetID).Return(store.MediaSet{}, pgx.ErrNoRows) var fileStore mocks.FileStore service := media.NewMediaSetService(&mockStore, nil, &fileStore, nil, config.Config{}, zap.NewNop().Sugar()) stream, err := service.GetAudioSegment(context.Background(), mediaSetID, 0, 1, media.AudioFormatMP3) require.Nil(t, stream) require.EqualError(t, err, "error getting media set: no rows in result set") }) t.Run("error fetching audio data", func(t *testing.T) { mediaSet := store.MediaSet{ID: mediaSetID, AudioChannels: 2} var mockStore mocks.Store mockStore.On("GetMediaSet", mock.Anything, mediaSetID).Return(mediaSet, nil) var fileStore mocks.FileStore fileStore.On("GetObjectWithRange", mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(nil, errors.New("network error")) service := media.NewMediaSetService(&mockStore, nil, &fileStore, nil, config.Config{}, zap.NewNop().Sugar()) stream, err := service.GetAudioSegment(context.Background(), mediaSetID, 0, 1, media.AudioFormatMP3) require.Nil(t, stream) require.EqualError(t, err, "error getting object from store: network error") }) t.Run("ffmpeg returns non-zero error code", func(t *testing.T) { mediaSet := store.MediaSet{ID: mediaSetID, AudioChannels: 2} var mockStore mocks.Store mockStore.On("GetMediaSet", mock.Anything, mediaSetID).Return(mediaSet, nil) var fileStore mocks.FileStore fileStore.On("GetObjectWithRange", mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(fixtureReader(t, inFixturePath, 1), nil) cmd := helperCommand(t, "", "", "something bad happened", 2) service := media.NewMediaSetService(&mockStore, nil, &fileStore, cmd, config.Config{}, zap.NewNop().Sugar()) stream, err := service.GetAudioSegment(context.Background(), mediaSetID, 0, 1, media.AudioFormatMP3) require.NoError(t, err) _, err = stream.Next(context.Background()) require.EqualError(t, err, "error waiting for ffmpeg: exit status 2, output: something bad happened") }) testCases := []struct { name string audioFormat media.AudioFormat audioChannels int32 inStartFrame, inEndFrame int64 wantStartByte, wantEndByte int64 outFixturePath string wantCommand string wantOutput string }{ { name: "mono to mp3", audioFormat: media.AudioFormatMP3, audioChannels: 1, inStartFrame: 500, inEndFrame: 2_000, wantStartByte: 1_000, wantEndByte: 4_000, outFixturePath: "testdata/fake.mp3", wantCommand: "ffmpeg -hide_banner -loglevel error -f s16le -ac 1 -ar 48000 -i - -f mp3 -", wantOutput: "this is a fake mp3", }, { name: "stereo to mp3", audioFormat: media.AudioFormatMP3, audioChannels: 2, inStartFrame: 0, inEndFrame: 1_323_000, wantStartByte: 0, wantEndByte: 5_292_000, outFixturePath: "testdata/fake.mp3", wantCommand: "ffmpeg -hide_banner -loglevel error -f s16le -ac 2 -ar 48000 -i - -f mp3 -", wantOutput: "this is a fake mp3", }, { name: "mono to wav", audioFormat: media.AudioFormatWAV, audioChannels: 1, inStartFrame: 16_384, inEndFrame: 32_768, wantStartByte: 32_768, wantEndByte: 65_536, outFixturePath: "testdata/fake.wav", wantCommand: "ffmpeg -hide_banner -loglevel error -f s16le -ac 1 -ar 48000 -i - -f wav -", wantOutput: "this is a fake wav", }, { name: "stereo to wav", audioFormat: media.AudioFormatWAV, audioChannels: 2, inStartFrame: 2_048, inEndFrame: 4_096, wantStartByte: 8_192, wantEndByte: 16_384, outFixturePath: "testdata/fake.wav", wantCommand: "ffmpeg -hide_banner -loglevel error -f s16le -ac 2 -ar 48000 -i - -f wav -", wantOutput: "this is a fake wav", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { ctx := context.Background() mediaSet := store.MediaSet{ID: mediaSetID, AudioChannels: tc.audioChannels} var mockStore mocks.Store mockStore.On("GetMediaSet", mock.Anything, mediaSetID).Return(mediaSet, nil) defer mockStore.AssertExpectations(t) var fileStore mocks.FileStore fileStore. On("GetObjectWithRange", mock.Anything, "media_sets/4c440241-cca9-436f-adb0-be074588cf2b/audio.raw", tc.wantStartByte, tc.wantEndByte). Return(fixtureReader(t, inFixturePath, tc.wantEndByte-tc.wantStartByte), nil) defer fileStore.AssertExpectations(t) cmd := helperCommand(t, tc.wantCommand, tc.outFixturePath, "", 0) service := media.NewMediaSetService(&mockStore, nil, &fileStore, cmd, config.Config{}, zap.NewNop().Sugar()) stream, err := service.GetAudioSegment(ctx, mediaSetID, tc.inStartFrame, tc.inEndFrame, tc.audioFormat) require.NoError(t, err) var data bytes.Buffer var lastPercentComplete float32 var progress media.AudioSegmentProgress for { progress, err = stream.Next(ctx) if err == io.EOF { break } require.NoError(t, err) assert.GreaterOrEqual(t, progress.PercentComplete, lastPercentComplete) lastPercentComplete = progress.PercentComplete data.Write(progress.Data) } assert.Equal(t, tc.wantOutput, data.String()) assert.Equal(t, float32(100), lastPercentComplete) }) } }