diff --git a/backend/media/fetch.go b/backend/media/fetch.go index 4f6f35d..086110b 100644 --- a/backend/media/fetch.go +++ b/backend/media/fetch.go @@ -79,7 +79,7 @@ func (s *FetchMediaSetService) Fetch(ctx context.Context, id string) (*MediaSet, // grab an audio stream from youtube // TODO: avoid possible panic - format := sortAudio(video.Formats)[0] + format := SortYoutubeAudio(video.Formats)[0] sampleRate, err := strconv.Atoi(format.AudioSampleRate) if err != nil { @@ -109,10 +109,7 @@ func (s *FetchMediaSetService) Fetch(ctx context.Context, id string) (*MediaSet, return &mediaSet, nil } -// FetchAudio fetches the audio stream from Youtube, pipes it through FFMPEG to -// extract the raw audio samples, and uploads them to S3. It -// returns a FetchAudioProgressReader. This reader must be read until -// completion - it will return any error which occurs during the fetch process. +// FetchAudio fetches the audio part of a MediaSet. func (s *FetchMediaSetService) FetchAudio(ctx context.Context, id string) (FetchAudioProgressReader, error) { mediaSet := NewMediaSet(id) if !mediaSet.Exists() { @@ -134,7 +131,7 @@ func (s *FetchMediaSetService) FetchAudio(ctx context.Context, id string) (Fetch } // TODO: avoid possible panic - format := sortAudio(video.Formats)[0] + format := SortYoutubeAudio(video.Formats)[0] stream, _, err := s.youtube.GetStreamContext(ctx, video, &format) if err != nil { @@ -184,8 +181,6 @@ type fetchAudioState struct { uploader *multipartUploadWriter } -// run copies the audio data from ffmpeg, waits for termination and then cleans -// up appropriately. func (s *fetchAudioState) run(ctx context.Context) { mw := io.MultiWriter(s, s.uploader) done := make(chan error) diff --git a/backend/media/youtube.go b/backend/media/youtube.go index 824a934..a82a2df 100644 --- a/backend/media/youtube.go +++ b/backend/media/youtube.go @@ -7,10 +7,10 @@ import ( youtubev2 "github.com/kkdai/youtube/v2" ) -// sortAudio returns the provided formats ordered in descending preferred +// SortYoutubeAudio returns the provided formats ordered in descending preferred // order. The ideal candidate is opus-encoded stereo audio in a webm container, // with the lowest available bitrate. -func sortAudio(inFormats youtubev2.FormatList) youtubev2.FormatList { +func SortYoutubeAudio(inFormats youtubev2.FormatList) youtubev2.FormatList { var formats youtubev2.FormatList for _, format := range inFormats { if format.FPS == 0 && format.AudioChannels > 0 { @@ -32,3 +32,25 @@ func sortAudio(inFormats youtubev2.FormatList) youtubev2.FormatList { }) return formats } + +// SortYoutubeVideo returns the provided formats ordered in descending preferred +// order. The ideal candidate is video in an mp4 container with a low +// bitrate, with audio channels (needed to allow synced playback on the +// website). +func SortYoutubeVideo(inFormats youtubev2.FormatList) youtubev2.FormatList { + var formats youtubev2.FormatList + for _, format := range inFormats { + if format.FPS > 0 && format.ContentLength > 0 { + formats = append(formats, format) + } + } + sort.SliceStable(formats, func(i, j int) bool { + isMP4I := strings.Contains(formats[i].MimeType, "mp4") + isMP4J := strings.Contains(formats[j].MimeType, "mp4") + if isMP4I && isMP4J { + return formats[i].ContentLength < formats[j].ContentLength + } + return isMP4I + }) + return formats +} diff --git a/backend/youtube/helpers_test.go b/backend/media/youtube_test.go similarity index 59% rename from backend/youtube/helpers_test.go rename to backend/media/youtube_test.go index f50233d..c687f54 100644 --- a/backend/youtube/helpers_test.go +++ b/backend/media/youtube_test.go @@ -1,9 +1,9 @@ -package youtube_test +package media_test import ( "testing" - "git.netflux.io/rob/clipper/youtube" + "git.netflux.io/rob/clipper/media" youtubev2 "github.com/kkdai/youtube/v2" "github.com/stretchr/testify/assert" ) @@ -12,70 +12,77 @@ func TestSortAudio(t *testing.T) { formats := []youtubev2.Format{ { MimeType: `audio/webm; codecs="opus"`, - Bitrate: 350_000, - AudioChannels: 2, + ContentLength: 38573, + AudioChannels: 1, AudioSampleRate: "16000", }, { MimeType: `audio/webm; codecs="opus"`, - Bitrate: 350_000, + ContentLength: 39458, AudioChannels: 2, - AudioSampleRate: "48000", + AudioSampleRate: "16000", }, { MimeType: `audio/mp4; codecs="mp4a.40.2"`, - Bitrate: 250_000, + ContentLength: 118394, + AudioChannels: 1, + AudioSampleRate: "48000", + }, + { + MimeType: `audio/webm; codecs="opus"`, + ContentLength: 127393, AudioChannels: 2, AudioSampleRate: "48000", }, { MimeType: `audio/webm; codecs="opus"`, - Bitrate: 125_000, + ContentLength: 123245, AudioChannels: 2, AudioSampleRate: "48000", }, } - sortedFormats := youtube.SortAudio(formats) + sortedFormats := media.SortYoutubeAudio(formats) assert.Equal(t, formats[1], sortedFormats[0]) - assert.Equal(t, formats[3], sortedFormats[1]) - assert.Equal(t, formats[0], sortedFormats[2]) - assert.Equal(t, formats[2], sortedFormats[3]) + assert.Equal(t, formats[4], sortedFormats[1]) + assert.Equal(t, formats[3], sortedFormats[2]) + assert.Equal(t, formats[0], sortedFormats[3]) + assert.Equal(t, formats[2], sortedFormats[4]) } func TestSortVideo(t *testing.T) { formats := []youtubev2.Format{ + { + MimeType: `audio/webm; codecs="opus"`, + QualityLabel: "120p", + FPS: 30, + ContentLength: 39402, + }, { MimeType: `video/mp4; codecs="avc1.42001E, mp4a.40.2"`, QualityLabel: "240p", FPS: 30, - AudioChannels: 2, - }, - { - MimeType: `audio/webm; codecs="opus"`, - QualityLabel: "", - FPS: 0, - AudioChannels: 2, + ContentLength: 40353, }, { MimeType: `video/mp4; codecs="avc1.42001E, mp4a.40.2"`, QualityLabel: "720p", FPS: 30, - AudioChannels: 2, + ContentLength: 393103, }, { MimeType: `video/mp4; codecs="avc1.42001E, mp4a.40.2"`, QualityLabel: "360p", - FPS: 30, - AudioChannels: 2, + FPS: 0, + ContentLength: 20403, }, } - sortedFormats := youtube.SortVideo(formats) + sortedFormats := media.SortYoutubeVideo(formats) assert.Len(t, sortedFormats, 3) - assert.Equal(t, formats[3], sortedFormats[0]) - assert.Equal(t, formats[0], sortedFormats[1]) - assert.Equal(t, formats[2], sortedFormats[2]) + assert.Equal(t, formats[1], sortedFormats[0]) + assert.Equal(t, formats[2], sortedFormats[1]) + assert.Equal(t, formats[0], sortedFormats[2]) }