122 lines
3.9 KiB
Go
122 lines
3.9 KiB
Go
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
|
|
}
|