package services import ( "context" "fmt" "path" "strings" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/mediaconvert" "github.com/aws/aws-sdk-go-v2/service/mediaconvert/types" ) var MediaConvertClient MediaConvert type MediaConvert interface { QueueEncodingJob(ctx context.Context, key string) (string, error) } // segmentLengthSeconds is how long each HLS segment runs. Six seconds is the // common default: long enough to keep the playlist and the request count down, // short enough that a player can switch rendition without a long stall. const segmentLengthSeconds = 6 type MediaConvertConcrete struct { MediaConvertClient *mediaconvert.Client Role string InputBucket string OutputBucket string } func (svc MediaConvertConcrete) QueueEncodingJob(ctx context.Context, key string) (string, error) { input := fmt.Sprintf("s3://%s/%s", svc.InputBucket, key) // HlsGroupSettings.Destination is a *filename base*, not a directory and // not a finished name: MediaConvert appends each output's NameModifier and // the extensions it needs. Giving each video its own folder keeps the // master playlist, the variant playlists and the segments together, and // keeps two videos from interleaving their segments in one prefix. base := strings.TrimSuffix(key, path.Ext(key)) destination := fmt.Sprintf("s3://%s/%s/index", svc.OutputBucket, base) job, err := svc.MediaConvertClient.CreateJob(ctx, &mediaconvert.CreateJobInput{ Role: &svc.Role, Settings: &types.JobSettings{ Inputs: []types.Input{ { FileInput: &input, AudioSelectors: map[string]types.AudioSelector{ "Audio Selector 1": { DefaultSelection: types.AudioDefaultSelectionDefault, }, }, VideoSelector: &types.VideoSelector{}, }, }, OutputGroups: []types.OutputGroup{ { Name: aws.String("HLS Group"), OutputGroupSettings: &types.OutputGroupSettings{ Type: types.OutputGroupTypeHlsGroupSettings, HlsGroupSettings: &types.HlsGroupSettings{ Destination: &destination, SegmentLength: aws.Int32(segmentLengthSeconds), // 0 lets the last segment be as short as it needs to // be rather than padding the ones before it. MinSegmentLength: aws.Int32(0), }, }, Outputs: []types.Output{ { // One rendition. A ladder is another entry here per // rendition, each with its own NameModifier and // height — the master playlist lists whatever is // present, so nothing downstream changes. NameModifier: aws.String("_720p"), ContainerSettings: &types.ContainerSettings{ Container: types.ContainerTypeM3u8, M3u8Settings: &types.M3u8Settings{}, }, VideoDescription: &types.VideoDescription{ Height: aws.Int32(720), CodecSettings: &types.VideoCodecSettings{ Codec: types.VideoCodecH264, H264Settings: &types.H264Settings{ RateControlMode: types.H264RateControlModeQvbr, MaxBitrate: aws.Int32(3000000), // A segment has to start on a keyframe, // so the GOP is pinned to a divisor of // the segment length. Left to follow the // source, segments come out uneven and // players stall at the joins. GopSizeUnits: types.H264GopSizeUnitsSeconds, GopSize: aws.Float64(2), }, }, }, AudioDescriptions: []types.AudioDescription{ { CodecSettings: &types.AudioCodecSettings{ Codec: types.AudioCodecAac, AacSettings: &types.AacSettings{ Bitrate: aws.Int32(96000), CodingMode: types.AacCodingModeCodingMode20, SampleRate: aws.Int32(48000), }, }, }, }, }, }, }, }, }, }) if err != nil { return "", err } return aws.ToString(job.Job.Id), nil }