使用AWS S3 + Lambda + MediaConvert 实现上传视频文件并自动转码
前言
最近团队在做短视频平台的技术调研,其中有一个环节便是音视频开发,即对用户上传的视频进行自适应转码。自适应的原理其实就是预先将视频转换为几个常用的分辨率,app端根据用户手机分辨率拉取相应分辨率的视频。 目前尝试了两种方案,一种是自己搭建完整的转码服务,利用minio + 桶通知事件 + javacv实现(https://gitee.com/chengzhi2/javacv-demo.git)。 另一种就是使用亚马逊的MediaConvert转码服务(这东西是真的贵啊)
配置S3存储
1、配置源存储桶
创建桶的步骤很简单,参考文档或者提示操作即可。但是这里要着重注意需要配置“跨源资源共享(CORS)”,要不然转码时会读取不到。源目标存储桶可以不提供对外访问权限。
[{"AllowedHeaders": ["*"],"AllowedMethods": ["GET"],"AllowedOrigins": ["*"],"ExposeHeaders": []}
]
2、配置目标存储桶
目标存储桶需要提供对外访问,需要配置存储桶访问策略:
{"Version": "2012-10-17","Statement": [{"Effect": "Allow","Principal": "*","Action": "s3:GetObject","Resource": "arn:aws:s3:::demo-video-out/*"}]
}
配置目标存储桶的目的是为了存储转码后的文件,因此这里不再需要配置跨资源共享了。
配置Lambda
1、创建函数
2、添加Lambda触发器
3、编写代码:
我这里是利用cursor + 调试得出的一个可行的方案:
index.mjs:
import { S3Client, HeadObjectCommand } from "@aws-sdk/client-s3";
import TranscodeService from './TranscodeService.js';const client = new S3Client();
const transcodeService = new TranscodeService('ap-northeast-3');export const handler = async (event, context) => {console.log('上传文件触发了函数')const bucket = event.Records[0].s3.bucket.name;const key = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, ' '));try {// 获取文件对象详细信息const { ContentType } = await client.send(new HeadObjectCommand({Bucket: bucket,Key: key,}));console.log('ContentType:', ContentType);// todo 如果是视频文件并且是上传事件if (ContentType.startsWith('video/')) {console.log('监听到视频文件,自动触发转码任务!!!');// 手动初始化并等待完成console.log('Initializing MediaConvert...');await transcodeService.initializeMediaConvert();console.log('MediaConvert initialized');// 调用转码服务await startTranscoding(bucket, key, ContentType);return {success: true,message: 'Transcoding started',contentType: ContentType};}} catch (err) {console.error('Error:', err);throw err;}
};async function startTranscoding(bucket, s3Key, contentType) {const inputS3Path = `s3://${bucket}/${s3Key}`;const outputS3Path = `s3://demo-video-out`;const transcodeSettings = {hls: true,mp4: true,resolutions: ['1080p', '720p', '480p'],segmentLength: 10};const job = await transcodeService.createTranscodeJob(inputS3Path, outputS3Path, transcodeSettings);console.log('转码任务创建成功!!!')
}
TranscodeService.js
const AWS = require('aws-sdk');
const { v4: uuidv4 } = require('uuid');class TranscodeService {constructor(region = 'ap-northeast-3') {this.region = region;this.mediaConvert = null;this.initialized = false;this.initializing = false;this.initializeMediaConvert();}/*** 初始化MediaConvert客户端*/async initializeMediaConvert() {if (this.initialized) {return;}if (this.initializing) {while (this.initializing) {await new Promise(resolve => setTimeout(resolve, 100));}return;}this.initializing = true;try {console.log('Starting MediaConvert initialization...');const mediaConvert = new AWS.MediaConvert({ region: this.region });const response = await mediaConvert.describeEndpoints().promise();this.mediaConvert = new AWS.MediaConvert({region: this.region,endpoint: response.Endpoints[0].Url});this.initialized = true;console.log('MediaConvert initialized successfully');} catch (error) {console.error('Failed to initialize MediaConvert:', error);throw error;} finally {this.initializing = false;}}/*** 确保MediaConvert已初始化*/async ensureInitialized() {if (!this.initialized) {await this.initializeMediaConvert();}}/*** 创建转码任务*/async createTranscodeJob(inputS3Path, outputS3Path, settings = {}) {try {await this.ensureInitialized();if (!this.mediaConvert) {throw new Error('MediaConvert client is not initialized');}const jobId = uuidv4();const jobSettings = {TimecodeConfig: {Source: "ZEROBASED"},Inputs: [{FileInput: inputS3Path,AudioSelectors: {"Audio Selector 1": {DefaultSelection: "DEFAULT"}}}],OutputGroups: this.buildOutputGroups(outputS3Path, settings)};const params = {Role: 'xxxxxxx', // todo 这里需要替换为MediaConvert角色的arn码UserMetadata: {assetID: jobId,application: 'video-transcode-app',input: inputS3Path,settings: 'custom'},Settings: jobSettings};console.log('Creating MediaConvert job:', JSON.stringify(params, null, 2));const response = await this.mediaConvert.createJob(params).promise();return {jobId: response.Job.Id,status: response.Job.Status,settings: jobSettings};} catch (error) {console.error('Failed to create transcode job:', error);throw error;}}/*** 构建输出组配置*/buildOutputGroups(outputS3Path, settings) {const outputGroups = [];// HLS输出组if (settings.hls !== false) {outputGroups.push({Name: "HLS",OutputGroupSettings: {Type: "HLS_GROUP_SETTINGS",HlsGroupSettings: {SegmentLength: settings.segmentLength || 10,MinSegmentLength: 0, SegmentControl: "SEGMENTED_FILES",Destination: `${outputS3Path}/hls/`,ManifestDurationFormat: "INTEGER"}},Outputs: this.buildHLSOutputs(settings.resolutions || ['1080p', '720p', '480p'])});}// MP4输出组if (settings.mp4 !== false) {outputGroups.push({Name: "MP4",OutputGroupSettings: {Type: "FILE_GROUP_SETTINGS",FileGroupSettings: {Destination: `${outputS3Path}/mp4/`}},Outputs: this.buildMP4Outputs(settings.resolutions || ['1080p', '720p'])});}return outputGroups;}/*** 构建HLS输出配置*/buildHLSOutputs(resolutions) {const resolutionConfigs = {'1080p': { width: 1920, height: 1080, bitrate: 5000000 },'720p': { width: 1280, height: 720, bitrate: 3000000 },'480p': { width: 854, height: 480, bitrate: 1500000 },'360p': { width: 640, height: 360, bitrate: 800000 }};return resolutions.map(resolution => {const config = resolutionConfigs[resolution];if (!config) {throw new Error(`Unsupported resolution: ${resolution}`);}return {NameModifier: `_${resolution}`,VideoDescription: {Width: config.width,Height: config.height,ScalingBehavior: "DEFAULT",CodecSettings: {Codec: "H_264",H264Settings: {MaxBitrate: config.bitrate * 2,HrdBufferSize: config.bitrate * 4, GopSize: 90,GopSizeUnits: "FRAMES",ParControl: "INITIALIZE_FROM_SOURCE",CodecProfile: "HIGH", RateControlMode: "QVBR",QvbrSettings: {QvbrQualityLevel: 8}}}},AudioDescriptions: [{AudioSourceName: "Audio Selector 1",CodecSettings: {Codec: "AAC",AacSettings: {Bitrate: 128000,CodingMode: "CODING_MODE_2_0",SampleRate: 48000,CodecProfile: "LC"}}}],ContainerSettings: {Container: "M3U8",M3u8Settings: {AudioFramesPerPes: 4,PcrControl: "PCR_EVERY_PES_PACKET",PmtPid: 480,PrivateMetadataPid: 503,VideoPid: 481,AudioPids: [482, 483, 484, 485, 486, 487]}}};});}/*** 构建MP4输出配置*/buildMP4Outputs(resolutions) {const resolutionConfigs = {'1080p': { width: 1920, height: 1080, bitrate: 5000000 },'720p': { width: 1280, height: 720, bitrate: 3000000 },'480p': { width: 854, height: 480, bitrate: 1500000 }};return resolutions.map(resolution => {const config = resolutionConfigs[resolution];if (!config) {throw new Error(`Unsupported resolution: ${resolution}`);}return {NameModifier: `_${resolution}`,VideoDescription: {Width: config.width,Height: config.height,ScalingBehavior: "DEFAULT",CodecSettings: {Codec: "H_264",H264Settings: {MaxBitrate: config.bitrate * 2,HrdBufferSize: config.bitrate * 4, GopSize: 90,GopSizeUnits: "FRAMES",ParControl: "INITIALIZE_FROM_SOURCE",CodecProfile: "MAIN", RateControlMode: "QVBR",QvbrSettings: {QvbrQualityLevel: 8}}}},AudioDescriptions: [{AudioSourceName: "Audio Selector 1",CodecSettings: {Codec: "AAC",AacSettings: {Bitrate: 128000,CodingMode: "CODING_MODE_2_0",SampleRate: 48000,CodecProfile: "LC"}}}],ContainerSettings: {Container: "MP4",Mp4Settings: {MoovPlacement: "PROGRESSIVE_DOWNLOAD"}}};});}/*** 查询转码任务状态*/async getJobStatus(jobId) {try {await this.ensureInitialized();const params = { Id: jobId };const response = await this.mediaConvert.getJob(params).promise();return {jobId: response.Job.Id,status: response.Job.Status,progress: response.Job.Status === 'COMPLETE' ? 100 : response.Job.Status === 'ERROR' ? 0 : response.Job.JobPercentComplete || 0,errorMessage: response.Job.ErrorMessage,outputFiles: response.Job.Settings.OutputGroups?.map(group => ({name: group.Name,destination: group.OutputGroupSettings.FileGroupSettings?.Destination ||group.OutputGroupSettings.HlsGroupSettings?.Destination}))};} catch (error) {console.error('Failed to get job status:', error);throw error;}}/*** 取消转码任务*/async cancelJob(jobId) {try {await this.ensureInitialized();const params = { Id: jobId };await this.mediaConvert.cancelJob(params).promise();console.log(`Job ${jobId} cancelled successfully`);} catch (error) {console.error('Failed to cancel job:', error);throw error;}}/*** 批量创建转码任务*/async batchTranscode(inputFiles, outputPath, settings = {}) {const jobs = [];for (const inputFile of inputFiles) {try {const job = await this.createTranscodeJob(inputFile, outputPath, settings);jobs.push(job);} catch (error) {console.error(`Failed to create job for ${inputFile}:`, error);jobs.push({ error: error.message, inputFile });}}return jobs;}/*** 等待转码任务完成*/async waitForJobCompletion(jobId, maxWaitTime = 3600000) { // 默认等待1小时const startTime = Date.now();while (Date.now() - startTime < maxWaitTime) {const status = await this.getJobStatus(jobId);if (status.status === 'COMPLETE') {console.log(`Job ${jobId} completed successfully`);return status;} else if (status.status === 'ERROR') {throw new Error(`Job ${jobId} failed: ${status.errorMessage}`);} else if (status.status === 'CANCELED') {throw new Error(`Job ${jobId} was canceled`);}// 等待30秒后再次检查await new Promise(resolve => setTimeout(resolve, 30000));}throw new Error(`Job ${jobId} timed out after ${maxWaitTime / 1000} seconds`);}/*** 获取转码任务列表*/async listJobs(status = null, maxResults = 20) {try {await this.ensureInitialized();const params = {MaxResults: maxResults};if (status) {params.Status = status;}const response = await this.mediaConvert.listJobs(params).promise();return response.Jobs || [];} catch (error) {console.error('Failed to list jobs:', error);throw error;}}
}module.exports = TranscodeService;
package.json
{"name": "lambda-s3","version": "1.0.0","main": "index.js","scripts": {"test": "echo \"Error: no test specified\" && exit 1"},"author": "","license": "ISC","description": "","dependencies": {"aws-sdk": "^2.1450.0","sharp": "^0.32.6","uuid": "^8.3.2"}
}
代码中需要注意一点,就是角色的arn码,从这里获取:
调试
日志:
日志需要在cloudwatch中对应所属区域下看
上述代码生成的json配置
{"Role": "arn:aws:iam::548259613792:role/service-role/MediaConvert_Default_Role","UserMetadata": {"assetID": "e9cce2b7-c64d-443c-89e8-ac2be2c3bfa7","application": "video-transcode-app","input": "s3://demo-video-source/276985.mp4","settings": "custom"},"Settings": {"TimecodeConfig": {"Source": "ZEROBASED"},"Inputs": [{"FileInput": "s3://demo-video-source/276985.mp4","AudioSelectors": {"Audio Selector 1": {"DefaultSelection": "DEFAULT"}}}],"OutputGroups": [{"Name": "HLS","OutputGroupSettings": {"Type": "HLS_GROUP_SETTINGS","HlsGroupSettings": {"SegmentLength": 10,"MinSegmentLength": 0,"SegmentControl": "SEGMENTED_FILES","Destination": "s3://demo-video-out/hls/","ManifestDurationFormat": "INTEGER"}},"Outputs": [{"NameModifier": "_1080p","VideoDescription": {"Width": 1920,"Height": 1080,"ScalingBehavior": "DEFAULT","CodecSettings": {"Codec": "H_264","H264Settings": {"MaxBitrate": 10000000,"HrdBufferSize": 20000000,"GopSize": 90,"GopSizeUnits": "FRAMES","ParControl": "INITIALIZE_FROM_SOURCE","CodecProfile": "HIGH","RateControlMode": "QVBR","QvbrSettings": {"QvbrQualityLevel": 8}}}},"AudioDescriptions": [{"AudioSourceName": "Audio Selector 1","CodecSettings": {"Codec": "AAC","AacSettings": {"Bitrate": 128000,"CodingMode": "CODING_MODE_2_0","SampleRate": 48000,"CodecProfile": "LC"}}}],"ContainerSettings": {"Container": "M3U8","M3u8Settings": {"AudioFramesPerPes": 4,"PcrControl": "PCR_EVERY_PES_PACKET","PmtPid": 480,"PrivateMetadataPid": 503,"VideoPid": 481,"AudioPids": [482,483,484,485,486,487]}}},{"NameModifier": "_720p","VideoDescription": {"Width": 1280,"Height": 720,"ScalingBehavior": "DEFAULT","CodecSettings": {"Codec": "H_264","H264Settings": {"MaxBitrate": 6000000,"HrdBufferSize": 12000000,"GopSize": 90,"GopSizeUnits": "FRAMES","ParControl": "INITIALIZE_FROM_SOURCE","CodecProfile": "HIGH","RateControlMode": "QVBR","QvbrSettings": {"QvbrQualityLevel": 8}}}},"AudioDescriptions": [{"AudioSourceName": "Audio Selector 1","CodecSettings": {"Codec": "AAC","AacSettings": {"Bitrate": 128000,"CodingMode": "CODING_MODE_2_0","SampleRate": 48000,"CodecProfile": "LC"}}}],"ContainerSettings": {"Container": "M3U8","M3u8Settings": {"AudioFramesPerPes": 4,"PcrControl": "PCR_EVERY_PES_PACKET","PmtPid": 480,"PrivateMetadataPid": 503,"VideoPid": 481,"AudioPids": [482,483,484,485,486,487]}}},{"NameModifier": "_480p","VideoDescription": {"Width": 854,"Height": 480,"ScalingBehavior": "DEFAULT","CodecSettings": {"Codec": "H_264","H264Settings": {"MaxBitrate": 3000000,"HrdBufferSize": 6000000,"GopSize": 90,"GopSizeUnits": "FRAMES","ParControl": "INITIALIZE_FROM_SOURCE","CodecProfile": "HIGH","RateControlMode": "QVBR","QvbrSettings": {"QvbrQualityLevel": 8}}}},"AudioDescriptions": [{"AudioSourceName": "Audio Selector 1","CodecSettings": {"Codec": "AAC","AacSettings": {"Bitrate": 128000,"CodingMode": "CODING_MODE_2_0","SampleRate": 48000,"CodecProfile": "LC"}}}],"ContainerSettings": {"Container": "M3U8","M3u8Settings": {"AudioFramesPerPes": 4,"PcrControl": "PCR_EVERY_PES_PACKET","PmtPid": 480,"PrivateMetadataPid": 503,"VideoPid": 481,"AudioPids": [482,483,484,485,486,487]}}}]},{"Name": "MP4","OutputGroupSettings": {"Type": "FILE_GROUP_SETTINGS","FileGroupSettings": {"Destination": "s3://demo-video-out/mp4/"}},"Outputs": [{"NameModifier": "_1080p","VideoDescription": {"Width": 1920,"Height": 1080,"ScalingBehavior": "DEFAULT","CodecSettings": {"Codec": "H_264","H264Settings": {"MaxBitrate": 10000000,"HrdBufferSize": 20000000,"GopSize": 90,"GopSizeUnits": "FRAMES","ParControl": "INITIALIZE_FROM_SOURCE","CodecProfile": "MAIN","RateControlMode": "QVBR","QvbrSettings": {"QvbrQualityLevel": 8}}}},"AudioDescriptions": [{"AudioSourceName": "Audio Selector 1","CodecSettings": {"Codec": "AAC","AacSettings": {"Bitrate": 128000,"CodingMode": "CODING_MODE_2_0","SampleRate": 48000,"CodecProfile": "LC"}}}],"ContainerSettings": {"Container": "MP4","Mp4Settings": {"MoovPlacement": "PROGRESSIVE_DOWNLOAD"}}},{"NameModifier": "_720p","VideoDescription": {"Width": 1280,"Height": 720,"ScalingBehavior": "DEFAULT","CodecSettings": {"Codec": "H_264","H264Settings": {"MaxBitrate": 6000000,"HrdBufferSize": 12000000,"GopSize": 90,"GopSizeUnits": "FRAMES","ParControl": "INITIALIZE_FROM_SOURCE","CodecProfile": "MAIN","RateControlMode": "QVBR","QvbrSettings": {"QvbrQualityLevel": 8}}}},"AudioDescriptions": [{"AudioSourceName": "Audio Selector 1","CodecSettings": {"Codec": "AAC","AacSettings": {"Bitrate": 128000,"CodingMode": "CODING_MODE_2_0","SampleRate": 48000,"CodecProfile": "LC"}}}],"ContainerSettings": {"Container": "MP4","Mp4Settings": {"MoovPlacement": "PROGRESSIVE_DOWNLOAD"}}},{"NameModifier": "_480p","VideoDescription": {"Width": 854,"Height": 480,"ScalingBehavior": "DEFAULT","CodecSettings": {"Codec": "H_264","H264Settings": {"MaxBitrate": 3000000,"HrdBufferSize": 6000000,"GopSize": 90,"GopSizeUnits": "FRAMES","ParControl": "INITIALIZE_FROM_SOURCE","CodecProfile": "MAIN","RateControlMode": "QVBR","QvbrSettings": {"QvbrQualityLevel": 8}}}},"AudioDescriptions": [{"AudioSourceName": "Audio Selector 1","CodecSettings": {"Codec": "AAC","AacSettings": {"Bitrate": 128000,"CodingMode": "CODING_MODE_2_0","SampleRate": 48000,"CodecProfile": "LC"}}}],"ContainerSettings": {"Container": "MP4","Mp4Settings": {"MoovPlacement": "PROGRESSIVE_DOWNLOAD"}}}]}]}
}
参考转码配置:
{"Queue": "arn:aws:mediaconvert:us-east-1:533267335205:queues/Default","UserMetadata": {},"Role": "arn:aws:iam::533267335205:role/MediaConvertRole","Settings": {"TimecodeConfig": {"Source": "EMBEDDED"},"OutputGroups": [{"Name": "HLS","Outputs": [{"ContainerSettings": {"Container": "M3U8","M3u8Settings": {"AudioFramesPerPes": 4,"PcrControl": "PCR_EVERY_PES_PACKET","PmtPid": 480,"PrivateMetadataPid": 503,"ProgramNumber": 1,"PatInterval": 0,"PmtInterval": 0,"Scte35Source": "NONE","Scte35Pid": 500,"TimedMetadata": "NONE","TimedMetadataPid": 502,"TransportStreamId": 1,"VideoPid": 481}},"VideoDescription": {"Width": 1920,"ScalingBehavior": "DEFAULT","Height": 1080,"TimecodeInsertion": "DISABLED","AntiAlias": "ENABLED","Sharpness": 50,"CodecSettings": {"Codec": "H_264","H264Settings": {"InterlaceMode": "PROGRESSIVE","NumberReferenceFrames": 3,"Syntax": "DEFAULT","Softness": 0,"GopClosedCadence": 1,"GopSize": 90,"Slices": 1,"GopBReference": "DISABLED","SlowPal": "DISABLED","SpatialAdaptiveQuantization": "ENABLED","TemporalAdaptiveQuantization": "ENABLED","FlickerAdaptiveQuantization": "DISABLED","EntropyEncoding": "CABAC","Bitrate": 5000000,"FramerateControl": "INITIALIZE_FROM_SOURCE","RateControlMode": "CBR","CodecProfile": "MAIN","Telecine": "NONE","MinIInterval": 0,"AdaptiveQuantization": "HIGH","CodecLevel": "AUTO","FieldEncoding": "PAFF","SceneChangeDetect": "ENABLED","QualityTuningLevel": "SINGLE_PASS","FramerateConversionAlgorithm": "DUPLICATE_DROP","UnregisteredSeiTimecode": "DISABLED","GopSizeUnits": "FRAMES","ParControl": "INITIALIZE_FROM_SOURCE","NumberBFramesBetweenReferenceFrames": 2,"RepeatPps": "DISABLED"}},"AfdSignaling": "NONE","DropFrameTimecode": "ENABLED","RespondToAfd": "NONE","ColorMetadata": "INSERT"},"AudioDescriptions": [{"AudioTypeControl": "FOLLOW_INPUT","CodecSettings": {"Codec": "AAC","AacSettings": {"AudioDescriptionBroadcasterMix": "NORMAL","Bitrate": 96000,"RateControlMode": "CBR","CodecProfile": "LC","CodingMode": "CODING_MODE_2_0","RawFormat": "NONE","SampleRate": 48000,"Specification": "MPEG4"}},"LanguageCodeControl": "FOLLOW_INPUT"}],"NameModifier": "_1080p"},{"ContainerSettings": {"Container": "M3U8","M3u8Settings": {"AudioFramesPerPes": 4,"PcrControl": "PCR_EVERY_PES_PACKET","PmtPid": 480,"PrivateMetadataPid": 503,"ProgramNumber": 1,"PatInterval": 0,"PmtInterval": 0,"Scte35Source": "NONE","Scte35Pid": 500,"TimedMetadata": "NONE","TimedMetadataPid": 502,"TransportStreamId": 1,"VideoPid": 481}},"VideoDescription": {"Width": 1280,"ScalingBehavior": "DEFAULT","Height": 720,"TimecodeInsertion": "DISABLED","AntiAlias": "ENABLED","Sharpness": 50,"CodecSettings": {"Codec": "H_264","H264Settings": {"InterlaceMode": "PROGRESSIVE","NumberReferenceFrames": 3,"Syntax": "DEFAULT","Softness": 0,"GopClosedCadence": 1,"GopSize": 90,"Slices": 1,"GopBReference": "DISABLED","SlowPal": "DISABLED","SpatialAdaptiveQuantization": "ENABLED","TemporalAdaptiveQuantization": "ENABLED","FlickerAdaptiveQuantization": "DISABLED","EntropyEncoding": "CABAC","Bitrate": 2500000,"FramerateControl": "INITIALIZE_FROM_SOURCE","RateControlMode": "CBR","CodecProfile": "MAIN","Telecine": "NONE","MinIInterval": 0,"AdaptiveQuantization": "HIGH","CodecLevel": "AUTO","FieldEncoding": "PAFF","SceneChangeDetect": "ENABLED","QualityTuningLevel": "SINGLE_PASS","FramerateConversionAlgorithm": "DUPLICATE_DROP","UnregisteredSeiTimecode": "DISABLED","GopSizeUnits": "FRAMES","ParControl": "INITIALIZE_FROM_SOURCE","NumberBFramesBetweenReferenceFrames": 2,"RepeatPps": "DISABLED"}},"AfdSignaling": "NONE","DropFrameTimecode": "ENABLED","RespondToAfd": "NONE","ColorMetadata": "INSERT"},"AudioDescriptions": [{"AudioTypeControl": "FOLLOW_INPUT","CodecSettings": {"Codec": "AAC","AacSettings": {"AudioDescriptionBroadcasterMix": "NORMAL","Bitrate": 96000,"RateControlMode": "CBR","CodecProfile": "LC","CodingMode": "CODING_MODE_2_0","RawFormat": "NONE","SampleRate": 48000,"Specification": "MPEG4"}},"LanguageCodeControl": "FOLLOW_INPUT"}],"NameModifier": "_720p"},{"ContainerSettings": {"Container": "M3U8","M3u8Settings": {"AudioFramesPerPes": 4,"PcrControl": "PCR_EVERY_PES_PACKET","PmtPid": 480,"PrivateMetadataPid": 503,"ProgramNumber": 1,"PatInterval": 0,"PmtInterval": 0,"Scte35Source": "NONE","Scte35Pid": 500,"TimedMetadata": "NONE","TimedMetadataPid": 502,"TransportStreamId": 1,"VideoPid": 481}},"VideoDescription": {"Width": 640,"ScalingBehavior": "DEFAULT","Height": 360,"TimecodeInsertion": "DISABLED","AntiAlias": "ENABLED","Sharpness": 50,"CodecSettings": {"Codec": "H_264","H264Settings": {"InterlaceMode": "PROGRESSIVE","NumberReferenceFrames": 3,"Syntax": "DEFAULT","Softness": 0,"GopClosedCadence": 1,"GopSize": 90,"Slices": 1,"GopBReference": "DISABLED","SlowPal": "DISABLED","SpatialAdaptiveQuantization": "ENABLED","TemporalAdaptiveQuantization": "ENABLED","FlickerAdaptiveQuantization": "DISABLED","EntropyEncoding": "CABAC","Bitrate": 800000,"FramerateControl": "INITIALIZE_FROM_SOURCE","RateControlMode": "CBR","CodecProfile": "MAIN","Telecine": "NONE","MinIInterval": 0,"AdaptiveQuantization": "HIGH","CodecLevel": "AUTO","FieldEncoding": "PAFF","SceneChangeDetect": "ENABLED","QualityTuningLevel": "SINGLE_PASS","FramerateConversionAlgorithm": "DUPLICATE_DROP","UnregisteredSeiTimecode": "DISABLED","GopSizeUnits": "FRAMES","ParControl": "INITIALIZE_FROM_SOURCE","NumberBFramesBetweenReferenceFrames": 2,"RepeatPps": "DISABLED"}},"AfdSignaling": "NONE","DropFrameTimecode": "ENABLED","RespondToAfd": "NONE","ColorMetadata": "INSERT"},"AudioDescriptions": [{"AudioTypeControl": "FOLLOW_INPUT","CodecSettings": {"Codec": "AAC","AacSettings": {"AudioDescriptionBroadcasterMix": "NORMAL","Bitrate": 96000,"RateControlMode": "CBR","CodecProfile": "LC","CodingMode": "CODING_MODE_2_0","RawFormat": "NONE","SampleRate": 48000,"Specification": "MPEG4"}},"LanguageCodeControl": "FOLLOW_INPUT"}],"NameModifier": "_360p"}],"OutputGroupSettings": {"Type": "HLS_GROUP_SETTINGS","HlsGroupSettings": {"ManifestDurationFormat": "INTEGER","SegmentLength": 10,"TimedMetadataId3Period": 10,"CaptionLanguageSetting": "OMIT","Destination": "s3://wyz-mediaconvert-bucket-virginia/output/hls/","TimedMetadataId3Frame": "PRIV","CodecSpecification": "RFC_4281","OutputSelection": "MANIFESTS_AND_SEGMENTS","ProgramDateTimePeriod": 600,"MinSegmentLength": 0,"DirectoryStructure": "SINGLE_DIRECTORY","ProgramDateTime": "EXCLUDE","SegmentControl": "SEGMENTED_FILES","ManifestCompression": "NONE","ClientCache": "ENABLED","StreamInfResolution": "INCLUDE"}}}],"AdAvailOffset": 0,"Inputs": [{"AudioSelectors": {"Audio Selector 1": {"Offset": 0,"DefaultSelection": "DEFAULT","ProgramSelection": 1}},"VideoSelector": {"ColorSpace": "FOLLOW"},"FilterEnable": "AUTO","PsiControl": "USE_PSI","FilterStrength": 0,"DeblockFilter": "DISABLED","DenoiseFilter": "DISABLED","TimecodeSource": "EMBEDDED","FileInput": "s3://wyz-mediaconvert-bucket-virginia/input/4ktest.mp4"}]},"BillingTagsSource": "JOB","AccelerationSettings": {"Mode": "DISABLED"},"StatusUpdateInterval": "SECONDS_60","Priority": 0
}
参考文档:
https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/userguide/tutorial-s3-batchops-lambda-mediaconvert-video.html
https://docs.aws.amazon.com/zh_cn/mediaconvert/latest/ug/example-job-settings.html
https://docs.aws.amazon.com/zh_cn/lambda/latest/dg/with-s3-example.html