├── .gitignore ├── .npmignore ├── README.md ├── bin └── event-replay-engine.ts ├── cdk.json ├── config └── encoding-profiles │ ├── hd-1080p.json │ ├── hd-720p.json │ ├── mc-job-template.json │ └── sd-540p.json ├── frontend ├── .gitignore ├── README.md ├── amplify │ ├── .config │ │ └── project-config.json │ ├── README.md │ ├── backend │ │ ├── backend-config.json │ │ └── tags.json │ ├── cli.json │ ├── hooks │ │ └── README.md │ └── team-provider-info.json ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.module.css │ ├── App.test.tsx │ ├── App.tsx │ ├── cdk-exports.json │ ├── components │ │ ├── Celebrities │ │ │ ├── Celebrities.module.css │ │ │ └── Celebrities.tsx │ │ ├── NavBar │ │ │ ├── NarBar.module.css │ │ │ └── NavBar.tsx │ │ ├── Preview │ │ │ ├── Preview.module.css │ │ │ └── Preview.tsx │ │ ├── Streaming │ │ │ ├── Streaming.module.css │ │ │ └── Streaming.tsx │ │ ├── Video │ │ │ ├── Video.module.css │ │ │ └── Video.tsx │ │ └── Vod │ │ │ ├── Vod.module.css │ │ │ └── Vod.tsx │ ├── global.d.ts │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── reportWebVitals.ts │ └── setupTests.ts └── tsconfig.json ├── images ├── 2022-12-28-20-38-37.png ├── 2023-01-04-20-56-15.png ├── 2023-01-05-10-30-21.png ├── 2023-01-05-11-14-52.png ├── 2023-03-24-22-34-49.png └── demo.gif ├── jest.config.js ├── lib ├── amplify.ts ├── cloudfront-streaming.ts ├── cloudfront-vod.ts ├── event-replay-engine.ts ├── media_live.ts ├── media_package.ts ├── mediaconvert-rekognition.ts ├── s3lambda-to-sqs.ts ├── secrets_mediapackage.ts └── sqs-to-mediaconvert.ts ├── package-lock.json ├── package.json ├── resources ├── getData.ts ├── mediaconvert.ts ├── rekognition.ts └── s3upload.ts ├── samples ├── medialive_configs.json ├── rekognition-event.json └── s3-upload-event.json ├── static └── index.html └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | cdk-export.json 6 | note.md 7 | !frontend/src/global.d.ts 8 | 9 | # CDK asset staging directory 10 | .cdk.staging 11 | cdk.out 12 | .env 13 | .DS_Store 14 | .vscode 15 | cleanup.sh -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless Smart Media Streaming Engine with AI/ML Backend 2 | 3 | ## Overview 4 | 5 | This demo shows how you can use CDK and amplify to build a streaming service with a smart backend that analyze the streaming media using a AI/ML service. In this particular demo, it plays Reacher from Prime Video and generate short VoD contents for each actors. 6 | 7 | - Elemental Media Services for streaming and vod contents 8 | - Serverless Event-Driven Architecture 9 | - All infra-structure resources as a code using CDK 10 | - Simple React frontend using Amplify 11 | - All code written in typescript 12 | 13 | ![](images/demo.gif) 14 | 15 | ## Architecture 16 | 17 | ![img](images/2022-12-28-20-38-37.png) 18 | 19 | ### Live Streaming 20 | 21 | The streaming feature leverages Amazon Elemental Services. [Amazon Elemental MediaLive](https://aws.amazon.com/medialive/?nc2=type_a) provides live video processing service and [Amazon Elemental MediaPackage](https://aws.amazon.com/mediapackage/?nc2=type_a) provides packaging and distributions. And MediaPackage is integrated with the [Amazon CloudFront](https://aws.amazon.com/cloudfront/?nc2=type_a) to deliver live stream to viewers with low latency and high transfer speeds. 22 | 23 | ### VoD Generation Backend with Rekognition 24 | 25 | This demo shows a good demonstration on how you can use various types of events when developing serverless architecture. Amazon Elemental MediaLive has two output groups, one for MediaPackage for live stream and another for S3 Archive. The S3 Archive output group archives live videos into [Amazon S3](https://aws.amazon.com/s3/?nc2=type_a) bucket which becomes the first event of Event-Driven Serverless backend. 26 | When a media file is uploaded to S3 Archive Bucket, a [Lambda](https://aws.amazon.com/lambda/?nc2=type_a) is invoked to put a message including the object information into [SQS](https://aws.amazon.com/sqs/?nc2=type_a) queue. Then another lambda is called via SQS target, to call [Amazon Elemental MediaConvert](https://aws.amazon.com/mediaconvert/?nc2=type_a) to convert the file. 27 | MediaConvert also has two output groups. One output group converts the file into HLS so it can be served as VoD Content. The other output group converts the file into MP4, because Amazon Rekognition can only analyze MP4 files. 28 | [Amazon EventBridge](https://aws.amazon.com/eventbridge/?nc2=type_a) watches the jobs of MediaConvert, and it triggers an event when it completes and calls the third lambda. The third lambda calles [Amazon Rekognition](https://aws.amazon.com/rekognition/?nc2=type_a) to analyze who appears in the video clip. And if a certain celebrity is found, it puts the metadata in [Amazon DynamoDB](https://aws.amazon.com/dynamodb/?nc2=type_a). 29 | 30 | ### Frontend Integration 31 | 32 | For ease of configuration and deployment, this demo uses [AWS Amplify](https://aws.amazon.com/amplify/?nc2=type_a) for the frontend and [AWS CDK](https://aws.amazon.com/cdk/?nc2=type_a) for backend. To keep the infrastructure management simple and consistent, all the resources are defined and deployed using AWS CDK. And the resource information is exported into a file in JSON format, and the frontend imports that file to configure AWS Amplify resources. Detailed instruction will be provided in the later chapter. 33 | 34 | ## Prerequisites 35 | 36 | - aws account 37 | - aws-cli 38 | - typescript >= 4.8 39 | - aws-cdk >= 2.54 40 | - node >= 16.17.0 41 | - npm >= 8.15.0 42 | - docker 43 | - git 44 | 45 | ## Installation 46 | 47 | 1. Download the code 48 | 49 | ```sh 50 | git clone https://github.com/atheanchu/MediaReplayEngine.git 51 | ``` 52 | 53 | 2. Install Packages for CDK backend 54 | 55 | ```sh 56 | npm install 57 | ``` 58 | 59 | 3. Export AWS CLI 60 | 61 | ```sh 62 | export AWS_PROFILE= 63 | ``` 64 | 65 | 4. Create `.env` file and set `REGION` environment variable to your region 66 | 67 | ``` 68 | REGION= 69 | ``` 70 | 71 | 5. Bootstrap and synthesize CDK 72 | 73 | ```sh 74 | cdk bootstrap 75 | cdk synth 76 | ``` 77 | 78 | 5. Deploy CDK - export the output to the output file in frontend directory. 79 | 80 | ```sh 81 | cdk deploy -O ./frontend/src/cdk-exports.json 82 | ``` 83 | 84 | 6. Install packages for frontend React application 85 | 86 | ```sh 87 | cd ./frontend 88 | npm install 89 | ``` 90 | 91 | ## Testing the demo 92 | 93 | > For detailed instructions on how to use OBS to stream your local video to MediaLive, please check this [link](https://aws.amazon.com/blogs/media/connecting-obs-studio-to-aws-media-services-in-the-cloud/). 94 | 95 | 1. Install [Open Broadcaster Software(OBS)](https://obsproject.com/ko/download) 96 | 2. Start MediaLive Channel 97 | ![](./images/2023-01-05-10-30-21.png) 98 | 3. Run OBS 99 | 4. Click Settings and Streams. Choose Custom for Service. 100 | 5. Copy `MyMediaLiveChannelDestPri` from `./frontend/cdk-exports.json`. The value must be in the format of `rtmp://:/`. For Server, input `rtmp://:/` and for Stream Key, put ``. For instance, if `MyMediaLiveChannelDestPri` is `rtmp://555.555.555.555:9999/streamkey`, 101 | 102 | ``` 103 | - Server: rtmp://555.555.555.555:9999/ 104 | - Stream Key: streamkey 105 | ``` 106 | 107 | ![](images/2023-03-24-22-34-49.png) 108 | 109 | 6. Play a video that Son Heung-min is in. It could be a local file or youtube video. 110 | 111 | 7. From Source, click `+` button and choose Window Capture. Select the window you want to start streaming to MediaLive input and click the button `Start Streaming` 112 | 113 | 8. Run the frontend application 114 | 115 | ```sh 116 | cd ./frontend 117 | npm start 118 | ``` 119 | 120 | 8. It will show the streaming video at the top of the page. It takes a few minutes for the backend to generate clips and analyze each clips which is 60 seconds long. Click `Celebrity` menu at the top right corner, and it will show buttons for each actor in the video. When you click the button, it will show all the shorts content for the actor. 121 | 122 | ## Clean Up 123 | 124 | 1. Stop Streaming from OBS by clicking button `Stop Streaming`. 125 | 2. Stop MediaLive Channel by clicking `Stop Channel` button. 126 | ![](./images/2023-01-05-11-14-52.png) 127 | 3. Since all the resources are deployed using AWS CDK, you can clean up simply by running `cdk destroy`. Although this demo uses AWS Amplify, we don't have to run `amplify delete` because no resources are created using through Amplify. 128 | 129 | ```sh 130 | cdk destroy 131 | ``` 132 | 133 | ## Resources 134 | 135 | - [React Player](https://github.com/cookpete/react-player) 136 | - [Fort Awesome](https://fortawesome.com/) 137 | 138 | ## License 139 | 140 | This sample code is made available under MIT-0 license. See the LICENSE file. 141 | -------------------------------------------------------------------------------- /bin/event-replay-engine.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "source-map-support/register"; 3 | import * as cdk from "aws-cdk-lib"; 4 | import { Aws } from "aws-cdk-lib"; 5 | import { App, Aspects } from "aws-cdk-lib"; 6 | 7 | import { AwsSolutionsChecks } from "cdk-nag"; 8 | 9 | import { EventReplayEngine } from "../lib/event-replay-engine"; 10 | const stackNameValue = "EventReplayEngine"; 11 | const description = 12 | "Event Replay Engine with AI/ML. This uses Elemental Services and backend \ 13 | serverless services to automatically generate the highlight clips of a streaming event"; 14 | 15 | const app = new cdk.App(); 16 | //Aspects.of(app).add(new AwsSolutionsChecks()); 17 | new EventReplayEngine(app, "EventReplayEngineStack", { 18 | stackName: stackNameValue, 19 | env: { 20 | region: `${Aws.REGION}`, 21 | account: `${Aws.ACCOUNT_ID}`, 22 | }, 23 | description, 24 | }); 25 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/event-replay-engine.ts", 3 | "watch": { 4 | "include": ["**"], 5 | "exclude": [ 6 | "README.md", 7 | "cdk*.json", 8 | "**/*.d.ts", 9 | "**/*.js", 10 | "tsconfig.json", 11 | "package*.json", 12 | "yarn.lock", 13 | "node_modules", 14 | "test" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 19 | "@aws-cdk/core:stackRelativeExports": true, 20 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 21 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 22 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 23 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 24 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 25 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 26 | "@aws-cdk/core:checkSecretUsage": true, 27 | "@aws-cdk/aws-iam:minimizePolicies": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 30 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 31 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 32 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 33 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 34 | "@aws-cdk/core:enablePartitionLiterals": true, 35 | "@aws-cdk/core:target-partitions": ["aws", "aws-cn"] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /config/encoding-profiles/hd-1080p.json: -------------------------------------------------------------------------------- 1 | { 2 | "audioDescriptions": [ 3 | { 4 | "codecSettings": { 5 | "aacSettings": { 6 | "inputType": "NORMAL", 7 | "bitrate": 96000, 8 | "codingMode": "CODING_MODE_2_0", 9 | "rawFormat": "NONE", 10 | "spec": "MPEG4", 11 | "profile": "LC", 12 | "rateControlMode": "CBR", 13 | "sampleRate": 48000 14 | } 15 | }, 16 | "audioTypeControl": "FOLLOW_INPUT", 17 | "languageCodeControl": "FOLLOW_INPUT", 18 | "auldioSelectorName": "default", 19 | "name": "audio_j8tr8" 20 | }, 21 | { 22 | "codecSettings": { 23 | "aacSettings": { 24 | "inputType": "NORMAL", 25 | "bitrate": 96000, 26 | "codingMode": "CODING_MODE_2_0", 27 | "rawFormat": "NONE", 28 | "spec": "MPEG4", 29 | "profile": "LC", 30 | "rateControlMode": "CBR", 31 | "sampleRate": 48000 32 | } 33 | }, 34 | "audioTypeControl": "FOLLOW_INPUT", 35 | "languageCodeControl": "FOLLOW_INPUT", 36 | "audioSelectorName": "default", 37 | "name": "audio_6ht2vm" 38 | }, 39 | { 40 | "codecSettings": { 41 | "aacSettings": { 42 | "inputType": "NORMAL", 43 | "bitrate": 96000, 44 | "codingMode": "CODING_MODE_2_0", 45 | "rawFormat": "NONE", 46 | "spec": "MPEG4", 47 | "profile": "LC", 48 | "rateControlMode": "CBR", 49 | "sampleRate": 48000 50 | } 51 | }, 52 | "audioTypeControl": "FOLLOW_INPUT", 53 | "languageCodeControl": "FOLLOW_INPUT", 54 | "audioSelectorName": "default", 55 | "name": "audio_s90hue" 56 | }, 57 | { 58 | "codecSettings": { 59 | "aacSettings": { 60 | "inputType": "NORMAL", 61 | "bitrate": 96000, 62 | "codingMode": "CODING_MODE_2_0", 63 | "rawFormat": "NONE", 64 | "spec": "MPEG4", 65 | "profile": "LC", 66 | "rateControlMode": "CBR", 67 | "sampleRate": 48000 68 | } 69 | }, 70 | "audioTypeControl": "FOLLOW_INPUT", 71 | "languageCodeControl": "FOLLOW_INPUT", 72 | "audioSelectorName": "default", 73 | "name": "audio_i3rm19" 74 | }, 75 | { 76 | "codecSettings": { 77 | "aacSettings": { 78 | "inputType": "NORMAL", 79 | "bitrate": 96000, 80 | "codingMode": "CODING_MODE_2_0", 81 | "rawFormat": "NONE", 82 | "spec": "MPEG4", 83 | "profile": "LC", 84 | "rateControlMode": "CBR", 85 | "sampleRate": 48000 86 | } 87 | }, 88 | "audioTypeControl": "FOLLOW_INPUT", 89 | "languageCodeControl": "FOLLOW_INPUT", 90 | "audioSelectorName": "default", 91 | "name": "audio_ze3rtr" 92 | }, 93 | { 94 | "codecSettings": { 95 | "aacSettings": { 96 | "inputType": "NORMAL", 97 | "bitrate": 96000, 98 | "codingMode": "CODING_MODE_2_0", 99 | "rawFormat": "NONE", 100 | "spec": "MPEG4", 101 | "profile": "LC", 102 | "rateControlMode": "CBR", 103 | "sampleRate": 48000 104 | } 105 | }, 106 | "audioTypeControl": "FOLLOW_INPUT", 107 | "languageCodeControl": "FOLLOW_INPUT", 108 | "audioSelectorName": "default", 109 | "name": "audio_5l3zhb" 110 | } 111 | ], 112 | "captionDescriptions": [], 113 | "outputGroups": [ 114 | { 115 | "outputGroupSettings": { 116 | "mediaPackageGroupSettings": { 117 | "destination": { 118 | "destinationRefId": "media-destination" 119 | } 120 | } 121 | }, 122 | "name": "HLS HD", 123 | "outputs": [ 124 | { 125 | "audioDescriptionNames": ["audio_j8tr8"], 126 | "captionDescriptionNames": [], 127 | "outputName": "_512x288", 128 | "outputSettings": { 129 | "mediaPackageOutputSettings": {} 130 | }, 131 | "videoDescriptionName": "_512x288" 132 | }, 133 | { 134 | "audioDescriptionNames": ["audio_6ht2vm"], 135 | "captionDescriptionNames": [], 136 | "outputName": "_640x360", 137 | "outputSettings": { 138 | "mediaPackageOutputSettings": {} 139 | }, 140 | "videoDescriptionName": "_640x360" 141 | }, 142 | { 143 | "audioDescriptionNames": ["audio_s90hue"], 144 | "captionDescriptionNames": [], 145 | "outputName": "_764x432", 146 | "outputSettings": { 147 | "mediaPackageOutputSettings": {} 148 | }, 149 | "videoDescriptionName": "_768x432" 150 | }, 151 | { 152 | "audioDescriptionNames": ["audio_i3rm19"], 153 | "captionDescriptionNames": [], 154 | "outputName": "_960x540", 155 | "outputSettings": { 156 | "mediaPackageOutputSettings": {} 157 | }, 158 | "videoDescriptionName": "_960x540" 159 | }, 160 | { 161 | "audioDescriptionNames": ["audio_ze3rtr"], 162 | "captionDescriptionNames": [], 163 | "outputName": "_1280x720", 164 | "outputSettings": { 165 | "mediaPackageOutputSettings": {} 166 | }, 167 | "videoDescriptionName": "_1280x720" 168 | }, 169 | { 170 | "audioDescriptionNames": ["audio_5l3zhb"], 171 | "captionDescriptionNames": [], 172 | "outputName": "_1920x1080", 173 | "outputSettings": { 174 | "mediaPackageOutputSettings": {} 175 | }, 176 | "videoDescriptionName": "_1920x1080" 177 | } 178 | ] 179 | }, 180 | { 181 | "outputGroupSettings": { 182 | "archiveGroupSettings": { 183 | "destination": { 184 | "destinationRefId": "s3-archive" 185 | }, 186 | "rolloverInterval": 60 187 | } 188 | }, 189 | "name": "S3 Archive", 190 | "outputs": [ 191 | { 192 | "outputSettings": { 193 | "archiveOutputSettings": { 194 | "nameModifier": "$dt$", 195 | "extension": "ts", 196 | "containerSettings": { 197 | "m2TsSettings": { 198 | "ccDescriptor": "DISABLED", 199 | "ebif": "NONE", 200 | "nielsenId3Behavior": "NO_PASSTHROUGH", 201 | "programNum": 1, 202 | "patInterval": 100, 203 | "pmtInterval": 100, 204 | "pcrControl": "PCR_EVERY_PES_PACKET", 205 | "pcrPeriod": 40, 206 | "timedMetadataBehavior": "NO_PASSTHROUGH", 207 | "bufferModel": "MULTIPLEX", 208 | "rateMode": "CBR", 209 | "audioBufferModel": "ATSC", 210 | "audioStreamType": "DVB", 211 | "audioFramesPerPes": 2, 212 | "segmentationStyle": "MAINTAIN_CADENCE", 213 | "segmentationMarkers": "NONE", 214 | "ebpPlacement": "VIDEO_AND_AUDIO_PIDS", 215 | "ebpAudioInterval": "VIDEO_INTERVAL", 216 | "esRateInPes": "EXCLUDE", 217 | "arib": "DISABLED", 218 | "aribCaptionsPidControl": "AUTO", 219 | "absentInputAudioBehavior": "ENCODE_SILENCE", 220 | "pmtPid": "480", 221 | "videoPid": "481", 222 | "audioPids": "482-498", 223 | "dvbTeletextPid": "499", 224 | "dvbSubPids": "460-479", 225 | "scte27Pids": "450-459", 226 | "scte35Pid": "500", 227 | "scte35Control": "NONE", 228 | "klv": "NONE", 229 | "klvDataPids": "501", 230 | "timedMetadataPid": "502", 231 | "etvPlatformPid": "504", 232 | "etvSignalPid": "505", 233 | "aribCaptionsPid": "507" 234 | } 235 | } 236 | } 237 | }, 238 | "outputName": "ykm19e", 239 | "videoDescriptionName": "video_xu3olh", 240 | "audioDescriptionNames": ["audio_uge9rm"], 241 | "captionDescriptionNames": [] 242 | } 243 | ] 244 | } 245 | ], 246 | "timecodeConfig": { 247 | "source": "EMBEDDED" 248 | }, 249 | "videoDescriptions": [ 250 | { 251 | "codecSettings": { 252 | "h264Settings": { 253 | "afdSignaling": "NONE", 254 | "colorMetadata": "INSERT", 255 | "adaptiveQuantization": "HIGH", 256 | "bitrate": 400000, 257 | "bufSize": 800000, 258 | "bufFillPct": 90, 259 | "entropyEncoding": "CAVLC", 260 | "flickerAq": "ENABLED", 261 | "framerateControl": "SPECIFIED", 262 | "framerateNumerator": 15, 263 | "framerateDenominator": 1, 264 | "gopBReference": "ENABLED", 265 | "gopClosedCadence": 1, 266 | "gopNumBFrames": 3, 267 | "gopSize": 2, 268 | "gopSizeUnits": "SECONDS", 269 | "subgopLength": "DYNAMIC", 270 | "scanType": "PROGRESSIVE", 271 | "level": "H264_LEVEL_AUTO", 272 | "lookAheadRateControl": "HIGH", 273 | "maxBitrate": 400000, 274 | "numRefFrames": 5, 275 | "parControl": "SPECIFIED", 276 | "parDenominator": 1, 277 | "parNumerator": 1, 278 | "profile": "MAIN", 279 | "rateControlMode": "QVBR", 280 | "qvbrQualityLevel": 6, 281 | "syntax": "DEFAULT", 282 | "sceneChangeDetect": "ENABLED", 283 | "spatialAq": "ENABLED", 284 | "temporalAq": "ENABLED", 285 | "timecodeInsertion": "DISABLED" 286 | } 287 | }, 288 | "height": 288, 289 | "name": "_512x288", 290 | "respondToAfd": "NONE", 291 | "sharpness": 100, 292 | "scalingBehavior": "DEFAULT", 293 | "width": 512 294 | }, 295 | { 296 | "codecSettings": { 297 | "h264Settings": { 298 | "afdSignaling": "NONE", 299 | "colorMetadata": "INSERT", 300 | "adaptiveQuantization": "HIGH", 301 | "bitrate": 800000, 302 | "bufSize": 1600000, 303 | "bufFillPct": 90, 304 | "entropyEncoding": "CAVLC", 305 | "flickerAq": "ENABLED", 306 | "framerateControl": "SPECIFIED", 307 | "framerateNumerator": 30, 308 | "framerateDenominator": 1, 309 | "gopBReference": "ENABLED", 310 | "gopClosedCadence": 1, 311 | "gopNumBFrames": 3, 312 | "gopSize": 2, 313 | "gopSizeUnits": "SECONDS", 314 | "subgopLength": "DYNAMIC", 315 | "scanType": "PROGRESSIVE", 316 | "level": "H264_LEVEL_AUTO", 317 | "lookAheadRateControl": "HIGH", 318 | "maxBitrate": 800000, 319 | "numRefFrames": 5, 320 | "parControl": "SPECIFIED", 321 | "parDenominator": 1, 322 | "parNumerator": 1, 323 | "profile": "MAIN", 324 | "rateControlMode": "QVBR", 325 | "qvbrQualityLevel": 7, 326 | "syntax": "DEFAULT", 327 | "sceneChangeDetect": "ENABLED", 328 | "spatialAq": "ENABLED", 329 | "temporalAq": "ENABLED", 330 | "timecodeInsertion": "DISABLED" 331 | } 332 | }, 333 | "height": 360, 334 | "name": "_640x360", 335 | "respondToAfd": "NONE", 336 | "sharpness": 100, 337 | "scalingBehavior": "DEFAULT", 338 | "width": 640 339 | }, 340 | { 341 | "codecSettings": { 342 | "h264Settings": { 343 | "afdSignaling": "NONE", 344 | "colorMetadata": "INSERT", 345 | "adaptiveQuantization": "HIGH", 346 | "bitrate": 1200000, 347 | "bufSize": 2400000, 348 | "bufFillPct": 90, 349 | "entropyEncoding": "CAVLC", 350 | "flickerAq": "ENABLED", 351 | "framerateControl": "SPECIFIED", 352 | "framerateNumerator": 30, 353 | "framerateDenominator": 1, 354 | "gopBReference": "ENABLED", 355 | "gopClosedCadence": 1, 356 | "gopNumBFrames": 3, 357 | "gopSize": 2, 358 | "gopSizeUnits": "SECONDS", 359 | "subgopLength": "DYNAMIC", 360 | "scanType": "PROGRESSIVE", 361 | "level": "H264_LEVEL_AUTO", 362 | "lookAheadRateControl": "HIGH", 363 | "maxBitrate": 1200000, 364 | "numRefFrames": 5, 365 | "parControl": "SPECIFIED", 366 | "parDenominator": 1, 367 | "parNumerator": 1, 368 | "profile": "MAIN", 369 | "rateControlMode": "QVBR", 370 | "qvbrQualityLevel": 7, 371 | "syntax": "DEFAULT", 372 | "sceneChangeDetect": "ENABLED", 373 | "spatialAq": "ENABLED", 374 | "temporalAq": "ENABLED", 375 | "timecodeInsertion": "DISABLED" 376 | } 377 | }, 378 | "height": 432, 379 | "name": "_768x432", 380 | "respondToAfd": "NONE", 381 | "sharpness": 100, 382 | "scalingBehavior": "DEFAULT", 383 | "width": 768 384 | }, 385 | { 386 | "codecSettings": { 387 | "h264Settings": { 388 | "afdSignaling": "NONE", 389 | "colorMetadata": "INSERT", 390 | "adaptiveQuantization": "HIGH", 391 | "bitrate": 1800000, 392 | "bufSize": 3600000, 393 | "bufFillPct": 90, 394 | "entropyEncoding": "CAVLC", 395 | "flickerAq": "ENABLED", 396 | "framerateControl": "SPECIFIED", 397 | "framerateNumerator": 30, 398 | "framerateDenominator": 1, 399 | "gopBReference": "ENABLED", 400 | "gopClosedCadence": 1, 401 | "gopNumBFrames": 3, 402 | "gopSize": 2, 403 | "gopSizeUnits": "SECONDS", 404 | "subgopLength": "DYNAMIC", 405 | "scanType": "PROGRESSIVE", 406 | "level": "H264_LEVEL_AUTO", 407 | "lookAheadRateControl": "HIGH", 408 | "maxBitrate": 1800000, 409 | "numRefFrames": 5, 410 | "parControl": "SPECIFIED", 411 | "parDenominator": 1, 412 | "parNumerator": 1, 413 | "profile": "MAIN", 414 | "rateControlMode": "QVBR", 415 | "qvbrQualityLevel": 7, 416 | "syntax": "DEFAULT", 417 | "sceneChangeDetect": "ENABLED", 418 | "spatialAq": "ENABLED", 419 | "temporalAq": "ENABLED", 420 | "timecodeInsertion": "DISABLED" 421 | } 422 | }, 423 | "height": 540, 424 | "name": "_960x540", 425 | "respondToAfd": "NONE", 426 | "sharpness": 100, 427 | "scalingBehavior": "DEFAULT", 428 | "width": 960 429 | }, 430 | { 431 | "codecSettings": { 432 | "h264Settings": { 433 | "afdSignaling": "NONE", 434 | "colorMetadata": "INSERT", 435 | "adaptiveQuantization": "HIGH", 436 | "bitrate": 2700000, 437 | "bufSize": 5400000, 438 | "bufFillPct": 90, 439 | "entropyEncoding": "CAVLC", 440 | "flickerAq": "ENABLED", 441 | "framerateControl": "SPECIFIED", 442 | "framerateNumerator": 30, 443 | "framerateDenominator": 1, 444 | "gopBReference": "ENABLED", 445 | "gopClosedCadence": 1, 446 | "gopNumBFrames": 3, 447 | "gopSize": 2, 448 | "gopSizeUnits": "SECONDS", 449 | "subgopLength": "DYNAMIC", 450 | "scanType": "PROGRESSIVE", 451 | "level": "H264_LEVEL_AUTO", 452 | "lookAheadRateControl": "HIGH", 453 | "maxBitrate": 2700000, 454 | "numRefFrames": 5, 455 | "parControl": "SPECIFIED", 456 | "parDenominator": 1, 457 | "parNumerator": 1, 458 | "profile": "MAIN", 459 | "rateControlMode": "QVBR", 460 | "qvbrQualityLevel": 8, 461 | "syntax": "DEFAULT", 462 | "sceneChangeDetect": "ENABLED", 463 | "spatialAq": "ENABLED", 464 | "temporalAq": "ENABLED", 465 | "timecodeInsertion": "DISABLED" 466 | } 467 | }, 468 | "height": 720, 469 | "name": "_1280x720", 470 | "respondToAfd": "NONE", 471 | "sharpness": 100, 472 | "scalingBehavior": "DEFAULT", 473 | "width": 1280 474 | }, 475 | { 476 | "codecSettings": { 477 | "h264Settings": { 478 | "afdSignaling": "NONE", 479 | "colorMetadata": "INSERT", 480 | "adaptiveQuantization": "HIGH", 481 | "bitrate": 4100000, 482 | "bufSize": 8200000, 483 | "bufFillPct": 90, 484 | "entropyEncoding": "CAVLC", 485 | "flickerAq": "ENABLED", 486 | "framerateControl": "SPECIFIED", 487 | "framerateNumerator": 30, 488 | "framerateDenominator": 1, 489 | "gopBReference": "ENABLED", 490 | "gopClosedCadence": 1, 491 | "gopNumBFrames": 3, 492 | "gopSize": 2, 493 | "gopSizeUnits": "SECONDS", 494 | "subgopLength": "DYNAMIC", 495 | "scanType": "PROGRESSIVE", 496 | "level": "H264_LEVEL_AUTO", 497 | "lookAheadRateControl": "HIGH", 498 | "maxBitrate": 4100000, 499 | "numRefFrames": 5, 500 | "parControl": "SPECIFIED", 501 | "parDenominator": 1, 502 | "parNumerator": 1, 503 | "profile": "HIGH", 504 | "rateControlMode": "QVBR", 505 | "qvbrQualityLevel": 8, 506 | "syntax": "DEFAULT", 507 | "sceneChangeDetect": "ENABLED", 508 | "spatialAq": "ENABLED", 509 | "temporalAq": "ENABLED", 510 | "timecodeInsertion": "DISABLED" 511 | } 512 | }, 513 | "height": 1080, 514 | "name": "_1920x1080", 515 | "respondToAfd": "NONE", 516 | "sharpness": 100, 517 | "scalingBehavior": "DEFAULT", 518 | "width": 1920 519 | } 520 | ], 521 | "availConfiguration": { 522 | "availSettings": { 523 | "scte35SpliceInsert": { 524 | "webDeliveryAllowedFlag": "FOLLOW", 525 | "noRegionalBlackoutFlag": "FOLLOW" 526 | } 527 | } 528 | } 529 | } 530 | -------------------------------------------------------------------------------- /config/encoding-profiles/hd-720p.json: -------------------------------------------------------------------------------- 1 | { 2 | "audioDescriptions": [ 3 | { 4 | "codecSettings": { 5 | "aacSettings": { 6 | "inputType": "NORMAL", 7 | "bitrate": 96000, 8 | "codingMode": "CODING_MODE_2_0", 9 | "rawFormat": "NONE", 10 | "spec": "MPEG4", 11 | "profile": "LC", 12 | "rateControlMode": "CBR", 13 | "sampleRate": 48000 14 | } 15 | }, 16 | "audioTypeControl": "FOLLOW_INPUT", 17 | "languageCodeControl": "FOLLOW_INPUT", 18 | "audioSelectorName": "default", 19 | "name": "audio_ze3rtr" 20 | } 21 | ], 22 | "captionDescriptions": [], 23 | "outputGroups": [ 24 | { 25 | "outputGroupSettings": { 26 | "mediaPackageGroupSettings": { 27 | "destination": { 28 | "destinationRefId": "media-destination" 29 | } 30 | } 31 | }, 32 | "name": "HLS HD", 33 | "outputs": [ 34 | { 35 | "captionDescriptionNames": [], 36 | "outputName": "_512x288", 37 | "outputSettings": { 38 | "mediaPackageOutputSettings": {} 39 | }, 40 | "videoDescriptionName": "_512x288" 41 | }, 42 | { 43 | "captionDescriptionNames": [], 44 | "outputName": "_640x360", 45 | "outputSettings": { 46 | "mediaPackageOutputSettings": {} 47 | }, 48 | "videoDescriptionName": "_640x360" 49 | }, 50 | { 51 | "captionDescriptionNames": [], 52 | "outputName": "_960x540", 53 | "outputSettings": { 54 | "mediaPackageOutputSettings": {} 55 | }, 56 | "videoDescriptionName": "_960x540" 57 | }, 58 | { 59 | "audioDescriptionNames": ["audio_ze3rtr"], 60 | "captionDescriptionNames": [], 61 | "outputName": "_1280x720", 62 | "outputSettings": { 63 | "mediaPackageOutputSettings": {} 64 | }, 65 | "videoDescriptionName": "_1280x720" 66 | } 67 | ] 68 | }, 69 | { 70 | "outputGroupSettings": { 71 | "archiveGroupSettings": { 72 | "destination": { 73 | "destinationRefId": "s3-archive" 74 | }, 75 | "rolloverInterval": 60 76 | } 77 | }, 78 | "name": "S3 Archive", 79 | "outputs": [ 80 | { 81 | "outputSettings": { 82 | "archiveOutputSettings": { 83 | "nameModifier": "$dt$", 84 | "extension": "ts", 85 | "containerSettings": { 86 | "m2TsSettings": { 87 | "ccDescriptor": "DISABLED", 88 | "ebif": "NONE", 89 | "nielsenId3Behavior": "NO_PASSTHROUGH", 90 | "programNum": 1, 91 | "patInterval": 100, 92 | "pmtInterval": 100, 93 | "pcrControl": "PCR_EVERY_PES_PACKET", 94 | "pcrPeriod": 40, 95 | "timedMetadataBehavior": "NO_PASSTHROUGH", 96 | "bufferModel": "MULTIPLEX", 97 | "rateMode": "CBR", 98 | "audioBufferModel": "ATSC", 99 | "audioStreamType": "DVB", 100 | "audioFramesPerPes": 2, 101 | "segmentationStyle": "MAINTAIN_CADENCE", 102 | "segmentationMarkers": "NONE", 103 | "ebpPlacement": "VIDEO_AND_AUDIO_PIDS", 104 | "ebpAudioInterval": "VIDEO_INTERVAL", 105 | "esRateInPes": "EXCLUDE", 106 | "arib": "DISABLED", 107 | "aribCaptionsPidControl": "AUTO", 108 | "absentInputAudioBehavior": "ENCODE_SILENCE", 109 | "pmtPid": "480", 110 | "videoPid": "481", 111 | "audioPids": "482-498", 112 | "dvbTeletextPid": "499", 113 | "dvbSubPids": "460-479", 114 | "scte27Pids": "450-459", 115 | "scte35Pid": "500", 116 | "scte35Control": "NONE", 117 | "klv": "NONE", 118 | "klvDataPids": "501", 119 | "timedMetadataPid": "502", 120 | "etvPlatformPid": "504", 121 | "etvSignalPid": "505", 122 | "aribCaptionsPid": "507" 123 | } 124 | } 125 | } 126 | }, 127 | "outputName": "ykm19e", 128 | "videoDescriptionName": "video_xu3olh", 129 | "audioDescriptionNames": ["audio_ze3rtr"], 130 | "captionDescriptionNames": [] 131 | } 132 | ] 133 | } 134 | ], 135 | "timecodeConfig": { 136 | "source": "EMBEDDED" 137 | }, 138 | "videoDescriptions": [ 139 | { 140 | "codecSettings": { 141 | "h264Settings": { 142 | "afdSignaling": "NONE", 143 | "colorMetadata": "INSERT", 144 | "adaptiveQuantization": "HIGH", 145 | "bitrate": 400000, 146 | "bufSize": 800000, 147 | "bufFillPct": 90, 148 | "entropyEncoding": "CAVLC", 149 | "flickerAq": "ENABLED", 150 | "forceFieldPictures": "DISABLED", 151 | "framerateControl": "SPECIFIED", 152 | "framerateNumerator": 15, 153 | "framerateDenominator": 1, 154 | "gopBReference": "DISABLED", 155 | "gopClosedCadence": 1, 156 | "gopNumBFrames": 0, 157 | "gopSize": 2, 158 | "gopSizeUnits": "SECONDS", 159 | "subgopLength": "FIXED", 160 | "scanType": "PROGRESSIVE", 161 | "level": "H264_LEVEL_AUTO", 162 | "lookAheadRateControl": "HIGH", 163 | "maxBitrate": 400000, 164 | "numRefFrames": 5, 165 | "parControl": "SPECIFIED", 166 | "parNumerator": 1, 167 | "parDenominator": 1, 168 | "profile": "BASELINE", 169 | "rateControlMode": "QVBR", 170 | "qvbrQualityLevel": 6, 171 | "syntax": "DEFAULT", 172 | "sceneChangeDetect": "DISABLED", 173 | "spatialAq": "ENABLED", 174 | "temporalAq": "ENABLED", 175 | "timecodeInsertion": "DISABLED" 176 | } 177 | }, 178 | "height": 288, 179 | "name": "_512x288", 180 | "respondToAfd": "NONE", 181 | "sharpness": 100, 182 | "scalingBehavior": "DEFAULT", 183 | "width": 512 184 | }, 185 | { 186 | "codecSettings": { 187 | "h264Settings": { 188 | "afdSignaling": "NONE", 189 | "colorMetadata": "INSERT", 190 | "adaptiveQuantization": "HIGH", 191 | "bitrate": 800000, 192 | "bufSize": 1600000, 193 | "bufFillPct": 90, 194 | "entropyEncoding": "CAVLC", 195 | "flickerAq": "ENABLED", 196 | "forceFieldPictures": "DISABLED", 197 | "framerateControl": "SPECIFIED", 198 | "framerateNumerator": 25, 199 | "framerateDenominator": 1, 200 | "gopBReference": "DISABLED", 201 | "gopClosedCadence": 1, 202 | "gopNumBFrames": 0, 203 | "gopSize": 2, 204 | "gopSizeUnits": "SECONDS", 205 | "subgopLength": "FIXED", 206 | "scanType": "PROGRESSIVE", 207 | "level": "H264_LEVEL_AUTO", 208 | "lookAheadRateControl": "HIGH", 209 | "maxBitrate": 800000, 210 | "numRefFrames": 5, 211 | "parControl": "SPECIFIED", 212 | "parNumerator": 1, 213 | "parDenominator": 1, 214 | "profile": "BASELINE", 215 | "rateControlMode": "QVBR", 216 | "qvbrQualityLevel": 7, 217 | "syntax": "DEFAULT", 218 | "sceneChangeDetect": "DISABLED", 219 | "spatialAq": "ENABLED", 220 | "temporalAq": "ENABLED", 221 | "timecodeInsertion": "DISABLED" 222 | } 223 | }, 224 | "height": 360, 225 | "name": "_640x360", 226 | "respondToAfd": "NONE", 227 | "sharpness": 100, 228 | "scalingBehavior": "DEFAULT", 229 | "width": 640 230 | }, 231 | { 232 | "codecSettings": { 233 | "h264Settings": { 234 | "afdSignaling": "NONE", 235 | "colorMetadata": "INSERT", 236 | "adaptiveQuantization": "HIGH", 237 | "bitrate": 1800000, 238 | "bufSize": 3600000, 239 | "bufFillPct": 90, 240 | "entropyEncoding": "CABAC", 241 | "flickerAq": "ENABLED", 242 | "forceFieldPictures": "DISABLED", 243 | "framerateControl": "SPECIFIED", 244 | "framerateNumerator": 25, 245 | "framerateDenominator": 1, 246 | "gopBReference": "ENABLED", 247 | "gopClosedCadence": 1, 248 | "gopNumBFrames": 3, 249 | "gopSize": 2, 250 | "gopSizeUnits": "SECONDS", 251 | "subgopLength": "FIXED", 252 | "scanType": "PROGRESSIVE", 253 | "level": "H264_LEVEL_AUTO", 254 | "lookAheadRateControl": "HIGH", 255 | "maxBitrate": 1800000, 256 | "numRefFrames": 5, 257 | "parControl": "SPECIFIED", 258 | "parNumerator": 1, 259 | "parDenominator": 1, 260 | "profile": "MAIN", 261 | "rateControlMode": "QVBR", 262 | "qvbrQualityLevel": 7, 263 | "syntax": "DEFAULT", 264 | "sceneChangeDetect": "DISABLED", 265 | "spatialAq": "ENABLED", 266 | "temporalAq": "ENABLED", 267 | "timecodeInsertion": "DISABLED" 268 | } 269 | }, 270 | "height": 540, 271 | "name": "_960x540", 272 | "respondToAfd": "NONE", 273 | "sharpness": 100, 274 | "scalingBehavior": "DEFAULT", 275 | "width": 960 276 | }, 277 | { 278 | "codecSettings": { 279 | "h264Settings": { 280 | "afdSignaling": "NONE", 281 | "colorMetadata": "INSERT", 282 | "adaptiveQuantization": "HIGH", 283 | "bitrate": 2700000, 284 | "bufSize": 5400000, 285 | "bufFillPct": 90, 286 | "entropyEncoding": "CABAC", 287 | "flickerAq": "ENABLED", 288 | "forceFieldPictures": "DISABLED", 289 | "framerateControl": "SPECIFIED", 290 | "framerateNumerator": 25, 291 | "framerateDenominator": 1, 292 | "gopBReference": "ENABLED", 293 | "gopClosedCadence": 1, 294 | "gopNumBFrames": 3, 295 | "gopSize": 2, 296 | "gopSizeUnits": "SECONDS", 297 | "subgopLength": "FIXED", 298 | "scanType": "PROGRESSIVE", 299 | "level": "H264_LEVEL_AUTO", 300 | "lookAheadRateControl": "HIGH", 301 | "maxBitrate": 2700000, 302 | "numRefFrames": 5, 303 | "parControl": "SPECIFIED", 304 | "parNumerator": 1, 305 | "parDenominator": 1, 306 | "profile": "HIGH", 307 | "rateControlMode": "QVBR", 308 | "qvbrQualityLevel": 8, 309 | "syntax": "DEFAULT", 310 | "sceneChangeDetect": "DISABLED", 311 | "spatialAq": "ENABLED", 312 | "temporalAq": "ENABLED", 313 | "timecodeInsertion": "DISABLED" 314 | } 315 | }, 316 | "height": 720, 317 | "name": "_1280x720", 318 | "respondToAfd": "NONE", 319 | "sharpness": 100, 320 | "scalingBehavior": "DEFAULT", 321 | "width": 1280 322 | }, 323 | { 324 | "codecSettings": { 325 | "h264Settings": { 326 | "afdSignaling": "NONE", 327 | "colorMetadata": "INSERT", 328 | "adaptiveQuantization": "AUTO", 329 | "entropyEncoding": "CABAC", 330 | "flickerAq": "ENABLED", 331 | "forceFieldPictures": "DISABLED", 332 | "framerateControl": "INITIALIZE_FROM_SOURCE", 333 | "gopBReference": "DISABLED", 334 | "gopClosedCadence": 1, 335 | "gopSize": 90, 336 | "gopSizeUnits": "FRAMES", 337 | "subgopLength": "FIXED", 338 | "scanType": "PROGRESSIVE", 339 | "level": "H264_LEVEL_AUTO", 340 | "lookAheadRateControl": "MEDIUM", 341 | "numRefFrames": 1, 342 | "parControl": "INITIALIZE_FROM_SOURCE", 343 | "profile": "MAIN", 344 | "rateControlMode": "CBR", 345 | "syntax": "DEFAULT", 346 | "sceneChangeDetect": "ENABLED", 347 | "spatialAq": "ENABLED", 348 | "temporalAq": "ENABLED", 349 | "timecodeInsertion": "DISABLED" 350 | } 351 | }, 352 | "name": "video_xu3olh", 353 | "respondToAfd": "NONE", 354 | "sharpness": 50, 355 | "scalingBehavior": "DEFAULT" 356 | } 357 | ], 358 | "availConfiguration": { 359 | "availSettings": { 360 | "scte35SpliceInsert": { 361 | "webDeliveryAllowedFlag": "FOLLOW", 362 | "nNoRegionalBlackoutFlag": "FOLLOW" 363 | } 364 | } 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /config/encoding-profiles/mc-job-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "OutputGroups": [ 3 | { 4 | "Name": "Apple HLS", 5 | "Outputs": [ 6 | { 7 | "Preset": "System-Ott_Hls_Ts_Avc_Aac_16x9_480x270p_15Hz_0.4Mbps", 8 | "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_480x270p_15Hz_400Kbps" 9 | }, 10 | { 11 | "Preset": "System-Ott_Hls_Ts_Avc_Aac_16x9_640x360p_30Hz_0.6Mbps", 12 | "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_640x360p_30Hz_600Kbps" 13 | }, 14 | { 15 | "Preset": "System-Ott_Hls_Ts_Avc_Aac_16x9_640x360p_30Hz_1.2Mbps", 16 | "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_640x360p_30Hz_1200Kbps" 17 | }, 18 | { 19 | "Preset": "System-Ott_Hls_Ts_Avc_Aac_16x9_960x540p_30Hz_3.5Mbps", 20 | "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_960x540p_30Hz_3500Kbps" 21 | }, 22 | { 23 | "Preset": "System-Ott_Hls_Ts_Avc_Aac_16x9_1280x720p_30Hz_3.5Mbps", 24 | "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_1280x720p_30Hz_3500Kbps" 25 | }, 26 | { 27 | "Preset": "System-Ott_Hls_Ts_Avc_Aac_16x9_1280x720p_30Hz_5.0Mbps", 28 | "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_1280x720p_30Hz_5000Kbps" 29 | }, 30 | { 31 | "Preset": "System-Ott_Hls_Ts_Avc_Aac_16x9_1280x720p_30Hz_6.5Mbps", 32 | "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_1280x720p_30Hz_6500Kbps" 33 | }, 34 | { 35 | "Preset": "System-Ott_Hls_Ts_Avc_Aac_16x9_1920x1080p_30Hz_8.5Mbps", 36 | "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_1920x1080p_30Hz_8500Kbps" 37 | } 38 | ], 39 | "OutputGroupSettings": { 40 | "Type": "HLS_GROUP_SETTINGS", 41 | "HlsGroupSettings": { 42 | "ManifestDurationFormat": "INTEGER", 43 | "SegmentLength": 3, 44 | "TimedMetadataId3Period": 10, 45 | "CaptionLanguageSetting": "OMIT", 46 | "Destination": "", 47 | "TimedMetadataId3Frame": "PRIV", 48 | "CodecSpecification": "RFC_4281", 49 | "OutputSelection": "MANIFESTS_AND_SEGMENTS", 50 | "ProgramDateTimePeriod": 600, 51 | "MinSegmentLength": 0, 52 | "DirectoryStructure": "SINGLE_DIRECTORY", 53 | "ProgramDateTime": "EXCLUDE", 54 | "SegmentControl": "SEGMENTED_FILES", 55 | "ManifestCompression": "NONE", 56 | "ClientCache": "ENABLED", 57 | "StreamInfResolution": "INCLUDE" 58 | } 59 | } 60 | }, 61 | { 62 | "CustomName": "mp4-for-rekognition", 63 | "Name": "File Group", 64 | "Outputs": [ 65 | { 66 | "ContainerSettings": { 67 | "Container": "MP4", 68 | "Mp4Settings": {} 69 | }, 70 | "VideoDescription": { 71 | "CodecSettings": { 72 | "Codec": "H_264", 73 | "H264Settings": { 74 | "MaxBitrate": 10000000, 75 | "RateControlMode": "QVBR", 76 | "SceneChangeDetect": "TRANSITION_DETECTION" 77 | } 78 | } 79 | }, 80 | "AudioDescriptions": [ 81 | { 82 | "CodecSettings": { 83 | "Codec": "AAC", 84 | "AacSettings": { 85 | "Bitrate": 96000, 86 | "CodingMode": "CODING_MODE_2_0", 87 | "SampleRate": 48000 88 | } 89 | } 90 | } 91 | ] 92 | } 93 | ], 94 | "OutputGroupSettings": { 95 | "Type": "FILE_GROUP_SETTINGS", 96 | "FileGroupSettings": { 97 | "Destination": "" 98 | } 99 | } 100 | } 101 | ], 102 | "AdAvailOffset": 0 103 | } 104 | -------------------------------------------------------------------------------- /config/encoding-profiles/sd-540p.json: -------------------------------------------------------------------------------- 1 | { 2 | "audioDescriptions": [ 3 | { 4 | "codecSettings": { 5 | "aacSettings": { 6 | "inputType": "NORMAL", 7 | "bitrate": 96000, 8 | "codingMode": "CODING_MODE_2_0", 9 | "rawFormat": "NONE", 10 | "spec": "MPEG4", 11 | "profile": "LC", 12 | "rateControlMode": "CBR", 13 | "sampleRate": 48000 14 | } 15 | }, 16 | "audioTypeControl": "FOLLOW_INPUT", 17 | "languageCodeControl": "FOLLOW_INPUT", 18 | "audioSelectorName": "default", 19 | "name": "audio_j8tr8" 20 | }, 21 | { 22 | "codecSettings": { 23 | "aacSettings": { 24 | "inputType": "NORMAL", 25 | "bitrate": 96000, 26 | "codingMode": "CODING_MODE_2_0", 27 | "rawFormat": "NONE", 28 | "spec": "MPEG4", 29 | "profile": "LC", 30 | "rateControlMode": "CBR", 31 | "sampleRate": 48000 32 | } 33 | }, 34 | "audioTypeControl": "FOLLOW_INPUT", 35 | "languageCodeControl": "FOLLOW_INPUT", 36 | "audioSelectorName": "default", 37 | "name": "audio_6ht2vm" 38 | }, 39 | { 40 | "codecSettings": { 41 | "aacSettings": { 42 | "inputType": "NORMAL", 43 | "bitrate": 96000, 44 | "codingMode": "CODING_MODE_2_0", 45 | "rawFormat": "NONE", 46 | "spec": "MPEG4", 47 | "profile": "LC", 48 | "rateControlMode": "CBR", 49 | "sampleRate": 48000 50 | } 51 | }, 52 | "audioTypeControl": "FOLLOW_INPUT", 53 | "languageCodeControl": "FOLLOW_INPUT", 54 | "audioSelectorName": "default", 55 | "name": "audio_s90hue" 56 | }, 57 | { 58 | "codecSettings": { 59 | "aacSettings": { 60 | "inputType": "NORMAL", 61 | "bitrate": 96000, 62 | "codingMode": "CODING_MODE_2_0", 63 | "rawFormat": "NONE", 64 | "spec": "MPEG4", 65 | "profile": "LC", 66 | "rateControlMode": "CBR", 67 | "sampleRate": 48000 68 | } 69 | }, 70 | "audioTypeControl": "FOLLOW_INPUT", 71 | "languageCodeControl": "FOLLOW_INPUT", 72 | "audioSelectorName": "default", 73 | "name": "audio_i3rm19" 74 | } 75 | ], 76 | "captionDescriptions": [], 77 | "outputGroups": [ 78 | { 79 | "outputGroupSettings": { 80 | "mediaPackageGroupSettings": { 81 | "destination": { 82 | "destinationRefId": "media-destination" 83 | } 84 | } 85 | }, 86 | "name": "HLS HD", 87 | "outputs": [ 88 | { 89 | "audioDescriptionNames": ["audio_j8tr8"], 90 | "captionDescriptionNames": [], 91 | "outputName": "_512x288", 92 | "outputSettings": { 93 | "mediaPackageOutputSettings": {} 94 | }, 95 | "videoDescriptionName": "_512x288" 96 | }, 97 | { 98 | "audioDescriptionNames": ["audio_6ht2vm"], 99 | "captionDescriptionNames": [], 100 | "outputName": "_640x360", 101 | "outputSettings": { 102 | "mediaPackageOutputSettings": {} 103 | }, 104 | "videoDescriptionName": "_640x360" 105 | }, 106 | { 107 | "audioDescriptionNames": ["audio_s90hue"], 108 | "captionDescriptionNames": [], 109 | "outputName": "_764x432", 110 | "outputSettings": { 111 | "mediaPackageOutputSettings": {} 112 | }, 113 | "videoDescriptionName": "_768x432" 114 | }, 115 | { 116 | "audioDescriptionNames": ["audio_i3rm19"], 117 | "captionDescriptionNames": [], 118 | "outputName": "_960x540", 119 | "outputSettings": { 120 | "mediaPackageOutputSettings": {} 121 | }, 122 | "videoDescriptionName": "_960x540" 123 | } 124 | ] 125 | }, 126 | { 127 | "outputGroupSettings": { 128 | "archiveGroupSettings": { 129 | "destination": { 130 | "destinationRefId": "s3-archive" 131 | }, 132 | "rolloverInterval": 60 133 | } 134 | }, 135 | "name": "S3 Archive", 136 | "outputs": [ 137 | { 138 | "outputSettings": { 139 | "archiveOutputSettings": { 140 | "nameModifier": "$dt$", 141 | "extension": "ts", 142 | "containerSettings": { 143 | "m2TsSettings": { 144 | "ccDescriptor": "DISABLED", 145 | "ebif": "NONE", 146 | "nielsenId3Behavior": "NO_PASSTHROUGH", 147 | "programNum": 1, 148 | "patInterval": 100, 149 | "pmtInterval": 100, 150 | "pcrControl": "PCR_EVERY_PES_PACKET", 151 | "pcrPeriod": 40, 152 | "timedMetadataBehavior": "NO_PASSTHROUGH", 153 | "bufferModel": "MULTIPLEX", 154 | "rateMode": "CBR", 155 | "audioBufferModel": "ATSC", 156 | "audioStreamType": "DVB", 157 | "audioFramesPerPes": 2, 158 | "segmentationStyle": "MAINTAIN_CADENCE", 159 | "segmentationMarkers": "NONE", 160 | "ebpPlacement": "VIDEO_AND_AUDIO_PIDS", 161 | "ebpAudioInterval": "VIDEO_INTERVAL", 162 | "esRateInPes": "EXCLUDE", 163 | "arib": "DISABLED", 164 | "aribCaptionsPidControl": "AUTO", 165 | "absentInputAudioBehavior": "ENCODE_SILENCE", 166 | "pmtPid": "480", 167 | "videoPid": "481", 168 | "audioPids": "482-498", 169 | "dvbTeletextPid": "499", 170 | "dvbSubPids": "460-479", 171 | "scte27Pids": "450-459", 172 | "scte35Pid": "500", 173 | "scte35Control": "NONE", 174 | "klv": "NONE", 175 | "klvDataPids": "501", 176 | "timedMetadataPid": "502", 177 | "etvPlatformPid": "504", 178 | "etvSignalPid": "505", 179 | "aribCaptionsPid": "507" 180 | } 181 | } 182 | } 183 | }, 184 | "outputName": "ykm19e", 185 | "videoDescriptionName": "video_xu3olh", 186 | "audioDescriptionNames": ["audio_uge9rm"], 187 | "captionDescriptionNames": [] 188 | } 189 | ] 190 | } 191 | ], 192 | "timecodeConfig": { 193 | "source": "EMBEDDED" 194 | }, 195 | "videoDescriptions": [ 196 | { 197 | "codecSettings": { 198 | "h264Settings": { 199 | "afdSignaling": "NONE", 200 | "colorMetadata": "INSERT", 201 | "adaptiveQuantization": "HIGH", 202 | "bitrate": 400000, 203 | "bufSize": 800000, 204 | "bufFillPct": 90, 205 | "entropyEncoding": "CAVLC", 206 | "flickerAq": "ENABLED", 207 | "framerateControl": "SPECIFIED", 208 | "framerateNumerator": 15, 209 | "framerateDenominator": 1, 210 | "gopBReference": "ENABLED", 211 | "gopClosedCadence": 1, 212 | "gopNumBFrames": 3, 213 | "gopSize": 2, 214 | "gopSizeUnits": "SECONDS", 215 | "subgopLength": "DYNAMIC", 216 | "scanType": "PROGRESSIVE", 217 | "level": "H264_LEVEL_AUTO", 218 | "lookAheadRateControl": "HIGH", 219 | "maxBitrate": 400000, 220 | "numRefFrames": 5, 221 | "parControl": "SPECIFIED", 222 | "parDenominator": 1, 223 | "parNumerator": 1, 224 | "profile": "MAIN", 225 | "rateControlMode": "QVBR", 226 | "qvbrQualityLevel": 6, 227 | "syntax": "DEFAULT", 228 | "sceneChangeDetect": "ENABLED", 229 | "spatialAq": "ENABLED", 230 | "temporalAq": "ENABLED", 231 | "timecodeInsertion": "DISABLED" 232 | } 233 | }, 234 | "height": 288, 235 | "name": "_512x288", 236 | "respondToAfd": "NONE", 237 | "sharpness": 100, 238 | "scalingBehavior": "DEFAULT", 239 | "width": 512 240 | }, 241 | { 242 | "codecSettings": { 243 | "h264Settings": { 244 | "afdSignaling": "NONE", 245 | "colorMetadata": "INSERT", 246 | "adaptiveQuantization": "HIGH", 247 | "bitrate": 800000, 248 | "bufSize": 1600000, 249 | "bufFillPct": 90, 250 | "entropyEncoding": "CAVLC", 251 | "flickerAq": "ENABLED", 252 | "framerateControl": "SPECIFIED", 253 | "framerateNumerator": 30, 254 | "framerateDenominator": 1, 255 | "gopBReference": "ENABLED", 256 | "gopClosedCadence": 1, 257 | "gopNumBFrames": 3, 258 | "gopSize": 2, 259 | "gopSizeUnits": "SECONDS", 260 | "subgopLength": "DYNAMIC", 261 | "scanType": "PROGRESSIVE", 262 | "level": "H264_LEVEL_AUTO", 263 | "lookAheadRateControl": "HIGH", 264 | "maxBitrate": 800000, 265 | "numRefFrames": 5, 266 | "parControl": "SPECIFIED", 267 | "parDenominator": 1, 268 | "parNumerator": 1, 269 | "profile": "MAIN", 270 | "rateControlMode": "QVBR", 271 | "qvbrQualityLevel": 7, 272 | "syntax": "DEFAULT", 273 | "sceneChangeDetect": "ENABLED", 274 | "spatialAq": "ENABLED", 275 | "temporalAq": "ENABLED", 276 | "timecodeInsertion": "DISABLED" 277 | } 278 | }, 279 | "height": 360, 280 | "name": "_640x360", 281 | "respondToAfd": "NONE", 282 | "sharpness": 100, 283 | "scalingBehavior": "DEFAULT", 284 | "width": 640 285 | }, 286 | { 287 | "codecSettings": { 288 | "h264Settings": { 289 | "afdSignaling": "NONE", 290 | "colorMetadata": "INSERT", 291 | "adaptiveQuantization": "HIGH", 292 | "bitrate": 1200000, 293 | "bufSize": 2400000, 294 | "bufFillPct": 90, 295 | "entropyEncoding": "CAVLC", 296 | "flickerAq": "ENABLED", 297 | "framerateControl": "SPECIFIED", 298 | "framerateNumerator": 30, 299 | "framerateDenominator": 1, 300 | "gopBReference": "ENABLED", 301 | "gopClosedCadence": 1, 302 | "gopNumBFrames": 3, 303 | "gopSize": 2, 304 | "gopSizeUnits": "SECONDS", 305 | "subgopLength": "DYNAMIC", 306 | "scanType": "PROGRESSIVE", 307 | "level": "H264_LEVEL_AUTO", 308 | "lookAheadRateControl": "HIGH", 309 | "maxBitrate": 1200000, 310 | "numRefFrames": 5, 311 | "parControl": "SPECIFIED", 312 | "parDenominator": 1, 313 | "parNumerator": 1, 314 | "profile": "MAIN", 315 | "rateControlMode": "QVBR", 316 | "qvbrQualityLevel": 7, 317 | "syntax": "DEFAULT", 318 | "sceneChangeDetect": "ENABLED", 319 | "spatialAq": "ENABLED", 320 | "temporalAq": "ENABLED", 321 | "timecodeInsertion": "DISABLED" 322 | } 323 | }, 324 | "height": 432, 325 | "name": "_768x432", 326 | "respondToAfd": "NONE", 327 | "sharpness": 100, 328 | "scalingBehavior": "DEFAULT", 329 | "width": 768 330 | }, 331 | { 332 | "codecSettings": { 333 | "h264Settings": { 334 | "afdSignaling": "NONE", 335 | "colorMetadata": "INSERT", 336 | "adaptiveQuantization": "HIGH", 337 | "bitrate": 1800000, 338 | "bufSize": 3600000, 339 | "bufFillPct": 90, 340 | "entropyEncoding": "CAVLC", 341 | "flickerAq": "ENABLED", 342 | "framerateControl": "SPECIFIED", 343 | "framerateNumerator": 30, 344 | "framerateDenominator": 1, 345 | "gopBReference": "ENABLED", 346 | "gopClosedCadence": 1, 347 | "gopNumBFrames": 3, 348 | "gopSize": 2, 349 | "gopSizeUnits": "SECONDS", 350 | "subgopLength": "DYNAMIC", 351 | "scanType": "PROGRESSIVE", 352 | "level": "H264_LEVEL_AUTO", 353 | "lookAheadRateControl": "HIGH", 354 | "maxBitrate": 1800000, 355 | "numRefFrames": 5, 356 | "parControl": "SPECIFIED", 357 | "parDenominator": 1, 358 | "parNumerator": 1, 359 | "profile": "MAIN", 360 | "rateControlMode": "QVBR", 361 | "qvbrQualityLevel": 7, 362 | "syntax": "DEFAULT", 363 | "sceneChangeDetect": "ENABLED", 364 | "spatialAq": "ENABLED", 365 | "temporalAq": "ENABLED", 366 | "timecodeInsertion": "DISABLED" 367 | } 368 | }, 369 | "height": 540, 370 | "name": "_960x540", 371 | "respondToAfd": "NONE", 372 | "sharpness": 100, 373 | "scalingBehavior": "DEFAULT", 374 | "width": 960 375 | } 376 | ], 377 | "availConfiguration": { 378 | "availSettings": { 379 | "scte35SpliceInsert": { 380 | "webDeliveryAllowedFlag": "FOLLOW", 381 | "noRegionalBlackoutFlag": "FOLLOW" 382 | } 383 | } 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | #amplify-do-not-edit-begin 26 | amplify/\#current-cloud-backend 27 | amplify/.config/local-* 28 | amplify/logs 29 | amplify/mock-data 30 | amplify/mock-api-resources 31 | amplify/backend/amplify-meta.json 32 | amplify/backend/.temp 33 | build/ 34 | dist/ 35 | node_modules/ 36 | aws-exports.js 37 | awsconfiguration.json 38 | amplifyconfiguration.json 39 | amplifyconfiguration.dart 40 | amplify-build-config.json 41 | amplify-gradle-config.json 42 | amplifytools.xcconfig 43 | .secret-* 44 | **.sample 45 | #amplify-do-not-edit-end 46 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /frontend/amplify/.config/project-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "frontend", 3 | "version": "3.1", 4 | "frontend": "javascript", 5 | "javascript": { 6 | "framework": "react", 7 | "config": { 8 | "SourceDir": "src", 9 | "DistributionDir": "build", 10 | "BuildCommand": "npm run-script build", 11 | "StartCommand": "npm run-script start" 12 | } 13 | }, 14 | "providers": [ 15 | "awscloudformation" 16 | ] 17 | } -------------------------------------------------------------------------------- /frontend/amplify/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Amplify CLI 2 | This directory was generated by [Amplify CLI](https://docs.amplify.aws/cli). 3 | 4 | Helpful resources: 5 | - Amplify documentation: https://docs.amplify.aws 6 | - Amplify CLI documentation: https://docs.amplify.aws/cli 7 | - More details on this folder & generated files: https://docs.amplify.aws/cli/reference/files 8 | - Join Amplify's community: https://amplify.aws/community/ 9 | -------------------------------------------------------------------------------- /frontend/amplify/backend/backend-config.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /frontend/amplify/backend/tags.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Key": "user:Stack", 4 | "Value": "{project-env}" 5 | }, 6 | { 7 | "Key": "user:Application", 8 | "Value": "{project-name}" 9 | } 10 | ] -------------------------------------------------------------------------------- /frontend/amplify/cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": { 3 | "graphqltransformer": { 4 | "addmissingownerfields": true, 5 | "improvepluralization": false, 6 | "validatetypenamereservedwords": true, 7 | "useexperimentalpipelinedtransformer": true, 8 | "enableiterativegsiupdates": true, 9 | "secondarykeyasgsi": true, 10 | "skipoverridemutationinputtypes": true, 11 | "transformerversion": 2, 12 | "suppressschemamigrationprompt": true, 13 | "securityenhancementnotification": false, 14 | "showfieldauthnotification": false, 15 | "usesubusernamefordefaultidentityclaim": true, 16 | "usefieldnameforprimarykeyconnectionfield": false, 17 | "enableautoindexquerynames": true, 18 | "respectprimarykeyattributesonconnectionfield": true, 19 | "shoulddeepmergedirectiveconfigdefaults": false, 20 | "populateownerfieldforstaticgroupauth": true 21 | }, 22 | "frontend-ios": { 23 | "enablexcodeintegration": true 24 | }, 25 | "auth": { 26 | "enablecaseinsensitivity": true, 27 | "useinclusiveterminology": true, 28 | "breakcirculardependency": true, 29 | "forcealiasattributes": false, 30 | "useenabledmfas": true 31 | }, 32 | "codegen": { 33 | "useappsyncmodelgenplugin": true, 34 | "usedocsgeneratorplugin": true, 35 | "usetypesgeneratorplugin": true, 36 | "cleangeneratedmodelsdirectory": true, 37 | "retaincasestyle": true, 38 | "addtimestampfields": true, 39 | "handlelistnullabilitytransparently": true, 40 | "emitauthprovider": true, 41 | "generateindexrules": true, 42 | "enabledartnullsafety": true 43 | }, 44 | "appsync": { 45 | "generategraphqlpermissions": true 46 | }, 47 | "latestregionsupport": { 48 | "pinpoint": 1, 49 | "translate": 1, 50 | "transcribe": 1, 51 | "rekognition": 1, 52 | "textract": 1, 53 | "comprehend": 1 54 | }, 55 | "project": { 56 | "overrides": true 57 | } 58 | }, 59 | "debug": { 60 | "shareProjectConfig": false 61 | } 62 | } -------------------------------------------------------------------------------- /frontend/amplify/hooks/README.md: -------------------------------------------------------------------------------- 1 | # Command Hooks 2 | 3 | Command hooks can be used to run custom scripts upon Amplify CLI lifecycle events like pre-push, post-add-function, etc. 4 | 5 | To get started, add your script files based on the expected naming convention in this directory. 6 | 7 | Learn more about the script file naming convention, hook parameters, third party dependencies, and advanced configurations at https://docs.amplify.aws/cli/usage/command-hooks 8 | -------------------------------------------------------------------------------- /frontend/amplify/team-provider-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "dev": { 3 | "awscloudformation": { 4 | "AuthRoleName": "amplify-frontend-dev-03222-authRole", 5 | "UnauthRoleArn": "arn:aws:iam::236241703319:role/amplify-frontend-dev-03222-unauthRole", 6 | "AuthRoleArn": "arn:aws:iam::236241703319:role/amplify-frontend-dev-03222-authRole", 7 | "Region": "ap-northeast-2", 8 | "DeploymentBucketName": "amplify-frontend-dev-03222-deployment", 9 | "UnauthRoleName": "amplify-frontend-dev-03222-unauthRole", 10 | "StackName": "amplify-frontend-dev-03222", 11 | "StackId": "arn:aws:cloudformation:ap-northeast-2:236241703319:stack/amplify-frontend-dev-03222/ff7fc810-82d6-11ed-80ab-0aeb07658bfc", 12 | "AmplifyAppId": "d3abguv487yeci" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@fortawesome/free-solid-svg-icons": "^6.2.1", 7 | "@fortawesome/react-fontawesome": "^0.2.0", 8 | "@testing-library/jest-dom": "^5.16.5", 9 | "@testing-library/react": "^13.4.0", 10 | "@testing-library/user-event": "^13.5.0", 11 | "@types/jest": "^27.5.2", 12 | "@types/node": "^16.18.10", 13 | "@types/react": "^18.0.26", 14 | "@types/react-dom": "^18.0.9", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-player": "^2.11.0", 18 | "react-router-dom": "^6.9.0", 19 | "react-scripts": "5.0.1", 20 | "typescript": "^4.9.4", 21 | "web-vitals": "^2.1.4" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "css-loader": "^6.8.1", 49 | "postcss-loader": "^7.3.3", 50 | "style-loader": "^3.3.3" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-smart-streaming-engine/f5b480fab7bd145c81761b17f8c7e71de5578886/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-smart-streaming-engine/f5b480fab7bd145c81761b17f8c7e71de5578886/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-smart-streaming-engine/f5b480fab7bd145c81761b17f8c7e71de5578886/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/App.module.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: center; 6 | } 7 | 8 | .mainArea { 9 | /* background-color: #424242; */ 10 | } 11 | 12 | .wrapper { 13 | display: flex; 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import NavBar from "./components/NavBar/NavBar"; 2 | import { Outlet } from "react-router-dom"; 3 | 4 | function App() { 5 | return ( 6 | <> 7 | {/*
*/} 8 | 9 | {/*
10 |
11 | 12 | 13 |
14 |
*/} 15 | 16 | {/*
*/} 17 | 18 | ); 19 | } 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /frontend/src/cdk-exports.json: -------------------------------------------------------------------------------- 1 | { 2 | "EventReplayEngine": { 3 | "MyMediaConvertBucket": "eventreplayengine-mediaconvertbucket0ffa08e9-8cmng59ampbw", 4 | "MyStreamingCloudFrontForVod": "d31vfq9383bzce.cloudfront.net", 5 | "MyMediaPackageChannelName": "EventReplayEngine_EMP-CDK", 6 | "MyMediaLiveChannelS3ArchivePath": "s3ssl://eventreplayengine-medialivearchivebucketadf8d49c-1nedgalslm1pd/medialiveArchive/", 7 | "MyMediaLiveChannelInputName": "EventReplayEngine_RTMP_PUSH_MediaLiveInput", 8 | "AmplifyFrontendApiGatewayEndpoint6D255223": "https://h62gx7u9r2.execute-api.ap-northeast-2.amazonaws.com/prod/", 9 | "MyStreamingCloudFrontS3LogBucket": "eventreplayengine-mycloudfrontdistributionlogsbuc-8qjlhqzqmbqy", 10 | "MyMediaLiveArchiveBucket": "eventreplayengine-medialivearchivebucketadf8d49c-1nedgalslm1pd", 11 | "FrontPageURL": "http://d33tc4poer1ctl.cloudfront.net", 12 | "MyMediaLiveChannelArn": "arn:aws:medialive:ap-northeast-2:236241703319:channel:6033864", 13 | "MyStreamingloudFrontDashEndpoint": "https://d3pspjsqfkpjwd.cloudfront.net/out/v1/2ce58adc61084de5b68e2554a4e8c3b6/index.mpd", 14 | "MyRekognitionBucket": "eventreplayengine-rekognitionbucketb1399c89-6thjer0hfba0", 15 | "MyMediaPackageChannelMyMediaPackageChannelRole864BA9E9": "arn:aws:iam::236241703319:role/EventReplayEngine-MyMediaPackageChannelMyMediaPack-S8NM8SWFBGHD", 16 | "MyMediaLiveChannelDestPri": "rtmp://13.209.181.30:1935/live/primary", 17 | "APIGatewayURLforAmplify": "https://h62gx7u9r2.execute-api.ap-northeast-2.amazonaws.com/prod/", 18 | "MyStreamingCloudFrontHlsEndpoint": "https://d3pspjsqfkpjwd.cloudfront.net/out/v1/d4e0040d353d42ee9acb2e39b863682b/index.m3u8" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/components/Celebrities/Celebrities.module.css: -------------------------------------------------------------------------------- 1 | /* CSS */ 2 | .button { 3 | align-items: center; 4 | background-clip: padding-box; 5 | background-color: #7b5297; 6 | border: 1px solid transparent; 7 | border-radius: 0.25rem; 8 | box-shadow: rgba(0, 0, 0, 0.02) 0 1px 3px 0; 9 | box-sizing: border-box; 10 | color: #fff; 11 | cursor: pointer; 12 | display: inline-flex; 13 | font-family: system-ui, -apple-system, system-ui, "Helvetica Neue", Helvetica, 14 | Arial, sans-serif; 15 | font-size: 16px; 16 | font-weight: 600; 17 | justify-content: center; 18 | line-height: 1.25; 19 | margin: 0; 20 | min-height: 3rem; 21 | padding: calc(0.875rem - 1px) calc(1.5rem - 1px); 22 | position: relative; 23 | text-decoration: none; 24 | transition: all 250ms; 25 | user-select: none; 26 | -webkit-user-select: none; 27 | touch-action: manipulation; 28 | vertical-align: baseline; 29 | width: auto; 30 | margin: 1rem; 31 | } 32 | 33 | .button:hover, 34 | .button:focus { 35 | background-color: #fb8332; 36 | box-shadow: rgba(0, 0, 0, 0.1) 0 4px 12px; 37 | } 38 | 39 | .button:hover { 40 | transform: translateY(-1px); 41 | } 42 | 43 | .button:active { 44 | background-color: #c85000; 45 | box-shadow: rgba(0, 0, 0, 0.06) 0 2px 4px; 46 | transform: translateY(0); 47 | } 48 | 49 | .buttonGroup { 50 | display: flex; 51 | justify-content: space-between; 52 | } 53 | -------------------------------------------------------------------------------- /frontend/src/components/Celebrities/Celebrities.tsx: -------------------------------------------------------------------------------- 1 | import { API } from "aws-amplify"; 2 | import { useEffect, useState, MouseEvent } from "react"; 3 | import { ReactElement } from "react"; 4 | import styles from "./Celebrities.module.css"; 5 | import { useNavigate } from "react-router-dom"; 6 | 7 | export default function Celebrities() { 8 | const [celebs, setCelebs] = useState([]); 9 | const navigate = useNavigate(); 10 | 11 | useEffect(() => { 12 | let list: ReactElement[] = []; 13 | const set = new Set(); 14 | 15 | const apiName = "GetVideoList"; 16 | const apiPath = "/vod"; 17 | const testEvent = { 18 | message: "Hello, this is the test event from the amplify", 19 | }; 20 | 21 | const onClickHander = (e: MouseEvent) => { 22 | e.preventDefault(); 23 | console.log(e.currentTarget.name); 24 | navigate(`/shorts/${e.currentTarget.name}`); 25 | }; 26 | 27 | API.get(apiName, apiPath, testEvent) 28 | .then((response) => { 29 | // console.log(response); 30 | for (const res of response) { 31 | set.add(res.celebName); 32 | } 33 | const temp = [...set]; 34 | for (const elem of temp) { 35 | list.push( 36 | 44 | ); 45 | } 46 | 47 | setCelebs(list); 48 | }) 49 | .catch((error) => { 50 | console.error(error.response); 51 | }); 52 | }, [navigate]); 53 | return
{celebs}
; 54 | } 55 | -------------------------------------------------------------------------------- /frontend/src/components/NavBar/NarBar.module.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | display: flex; 3 | background-color: #fa6400; 4 | align-items: center; 5 | } 6 | 7 | .navbar h2 { 8 | padding: 0px; 9 | margin: 10px; 10 | margin-left: 2rem; 11 | color: whitesmoke; 12 | } 13 | 14 | .link { 15 | text-decoration: none; 16 | } 17 | 18 | .toCelebButton__container { 19 | display: flex; 20 | margin-left: auto; 21 | } 22 | 23 | .toCelebButton { 24 | padding: 0px; 25 | margin-right: 5rem; 26 | text-decoration: none; 27 | color: whitesmoke; 28 | font-weight: bolder; 29 | font-size: 1.2rem; 30 | } 31 | 32 | .toCelebButton:hover, 33 | .toCelebButton:focus { 34 | font-size: 1.3rem; 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/components/NavBar/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./NarBar.module.css"; 3 | import { Link } from "react-router-dom"; 4 | 5 | const NavBar = () => { 6 | return ( 7 |
8 | 9 |

AnyCompany Shorts Generator

10 | 11 |
12 | 13 | Celebrities 14 | 15 |
16 |
17 | ); 18 | }; 19 | 20 | export default NavBar; 21 | -------------------------------------------------------------------------------- /frontend/src/components/Preview/Preview.module.css: -------------------------------------------------------------------------------- 1 | .previewPlayer { 2 | margin: 1rem; 3 | } 4 | 5 | .previewPlayer .previewItem { 6 | margin-bottom: 10px; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/components/Preview/Preview.tsx: -------------------------------------------------------------------------------- 1 | import ReactPlayer from "react-player"; 2 | import styles from "./Preview.module.css"; 3 | 4 | type Video = { 5 | url: string; 6 | title: string; 7 | }; 8 | 9 | const Preview = ({ url, title }: Video) => { 10 | const splitTitle = title.split("/").at(-1); 11 | return ( 12 |
13 | 20 | {splitTitle} 21 |
22 | ); 23 | }; 24 | 25 | export default Preview; 26 | -------------------------------------------------------------------------------- /frontend/src/components/Streaming/Streaming.module.css: -------------------------------------------------------------------------------- 1 | .streaming { 2 | margin: 1rem 3rem; 3 | box-shadow: rgba(6, 24, 44, 0.4) 0px 0px 0px 2px, 4 | rgba(6, 24, 44, 0.65) 0px 4px 6px -1px, 5 | rgba(255, 255, 255, 0.08) 0px 1px 0px inset; 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/components/Streaming/Streaming.tsx: -------------------------------------------------------------------------------- 1 | import Video from "../Video/Video"; 2 | import style from "./Streaming.module.css"; 3 | 4 | const Streaming = () => { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | }; 11 | 12 | export default Streaming; 13 | -------------------------------------------------------------------------------- /frontend/src/components/Video/Video.module.css: -------------------------------------------------------------------------------- 1 | .videoPlayer { 2 | position: relative; 3 | left: 50%; 4 | transform: translate(-50%, 0); 5 | padding-top: 56.25%; 6 | /* max-width: 1280px; */ 7 | /* max-width: 1080px; */ 8 | } 9 | 10 | .reactPlayerLive { 11 | position: absolute; 12 | top: 0; 13 | left: 0; 14 | } 15 | 16 | @media (min-width: 1280px) { 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/components/Video/Video.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactPlayer from "react-player"; 3 | import styles from "./Video.module.css"; 4 | import cdk from "../../cdk-exports.json"; 5 | 6 | const Video = () => { 7 | // const videoURL = process.env.REACT_APP_LIVE_STREAMING_CF_URL; 8 | const videoURL = cdk.EventReplayEngine.MyStreamingCloudFrontHlsEndpoint; 9 | console.log(videoURL); 10 | 11 | return ( 12 |
13 | 20 |
21 | ); 22 | }; 23 | 24 | export default Video; 25 | -------------------------------------------------------------------------------- /frontend/src/components/Vod/Vod.module.css: -------------------------------------------------------------------------------- 1 | .vod h1 { 2 | color: white; 3 | } 4 | 5 | .vod ul { 6 | list-style: none; 7 | display: flex; 8 | width: 100%; 9 | flex-wrap: wrap; 10 | /* justify-content: space-around; */ 11 | position: relative; 12 | 13 | padding: 0px; 14 | margin: 0px; 15 | } 16 | 17 | .vodBar { 18 | display: flex; 19 | justify-content: space-between; 20 | margin: 1rem; 21 | } 22 | 23 | .vodRefreshIcon:hover { 24 | margin: 0; 25 | padding: 0; 26 | border: 1px solid black; 27 | font-size: 30px; 28 | } 29 | 30 | .vodRefreshIcon:active { 31 | background-color: darkgray; 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/components/Vod/Vod.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useEffect } from "react"; 2 | import Preview from "../Preview/Preview"; 3 | import { API } from "aws-amplify"; 4 | import { useState } from "react"; 5 | import styles from "./Vod.module.css"; 6 | import { useParams } from "react-router-dom"; 7 | 8 | const Vod = () => { 9 | const [playlist, setPlaylist] = useState([]); 10 | const { keyword } = useParams(); 11 | 12 | useEffect(() => { 13 | const apiName = "GetVideoList"; 14 | const apiPath = "/vod"; 15 | const testEvent = { 16 | message: "Hello, this is the test event from the amplify", 17 | }; 18 | 19 | const list: ReactElement[] = []; 20 | 21 | // temporary array and set to remove duplicate 22 | let tempArray: string[] = []; 23 | const tempSet: Set = new Set(); 24 | 25 | API.get(apiName, apiPath, testEvent) 26 | .then((response) => { 27 | for (const item of response) { 28 | if (!keyword) tempSet.add(item.url); 29 | else { 30 | if (keyword === item.celebName) { 31 | console.log(`${keyword} provided`); 32 | tempSet.add(item.url); 33 | } 34 | } 35 | } 36 | 37 | tempArray = [...tempSet]; 38 | 39 | for (const elem of tempArray) { 40 | list.push( 41 |
  • 42 | 43 |
  • 44 | ); 45 | } 46 | 47 | setPlaylist(list); 48 | }) 49 | .catch((error) => { 50 | // console.log(error.response); 51 | }); 52 | }, [keyword]); 53 | 54 | return ( 55 |
    56 |
    57 | {/* 58 |  Shorts */} 59 |
    60 |
      {playlist}
    61 |
    62 | ); 63 | }; 64 | 65 | export default Vod; 66 | -------------------------------------------------------------------------------- /frontend/src/global.d.ts: -------------------------------------------------------------------------------- 1 | // TypeScript가 css 파일을 모듈로 인식하지 못해서 에러가 발생 2 | // css에 대한 모듈 형식을 선언해주는 것으로 문제를 해결 3 | declare module "*.css" { 4 | const content: { [className: string]: string }; 5 | export = content; 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import reportWebVitals from "./reportWebVitals"; 6 | import { Amplify } from "aws-amplify"; 7 | import cdk from "./cdk-exports.json"; 8 | import { createBrowserRouter, RouterProvider } from "react-router-dom"; 9 | import Streaming from "./components/Streaming/Streaming"; 10 | import Vod from "./components/Vod/Vod"; 11 | import Celebrities from "./components/Celebrities/Celebrities"; 12 | 13 | Amplify.configure({ 14 | API: { 15 | endpoints: [ 16 | { 17 | name: "GetVideoList", 18 | endpoint: cdk.EventReplayEngine.APIGatewayURLforAmplify, 19 | }, 20 | ], 21 | }, 22 | }); 23 | 24 | const router = createBrowserRouter([ 25 | { 26 | path: "/", 27 | element: , 28 | children: [ 29 | { index: true, element: }, 30 | { path: "celebs", element: }, 31 | { path: "shorts", element: }, 32 | { path: "shorts/:keyword", element: }, 33 | ], 34 | }, 35 | ]); 36 | 37 | const root = ReactDOM.createRoot( 38 | document.getElementById("root") as HTMLElement 39 | ); 40 | 41 | root.render( 42 | 43 | {/* */} 44 | 45 | 46 | ); 47 | 48 | // If you want to start measuring performance in your app, pass a function 49 | // to log results (for example: reportWebVitals(console.log)) 50 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 51 | reportWebVitals(); 52 | -------------------------------------------------------------------------------- /frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /frontend/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /images/2022-12-28-20-38-37.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-smart-streaming-engine/f5b480fab7bd145c81761b17f8c7e71de5578886/images/2022-12-28-20-38-37.png -------------------------------------------------------------------------------- /images/2023-01-04-20-56-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-smart-streaming-engine/f5b480fab7bd145c81761b17f8c7e71de5578886/images/2023-01-04-20-56-15.png -------------------------------------------------------------------------------- /images/2023-01-05-10-30-21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-smart-streaming-engine/f5b480fab7bd145c81761b17f8c7e71de5578886/images/2023-01-05-10-30-21.png -------------------------------------------------------------------------------- /images/2023-01-05-11-14-52.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-smart-streaming-engine/f5b480fab7bd145c81761b17f8c7e71de5578886/images/2023-01-05-11-14-52.png -------------------------------------------------------------------------------- /images/2023-03-24-22-34-49.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-smart-streaming-engine/f5b480fab7bd145c81761b17f8c7e71de5578886/images/2023-03-24-22-34-49.png -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-smart-streaming-engine/f5b480fab7bd145c81761b17f8c7e71de5578886/images/demo.gif -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /lib/amplify.ts: -------------------------------------------------------------------------------- 1 | import { 2 | aws_dynamodb as dynamodb, 3 | aws_apigateway as agw, 4 | aws_s3_deployment as deployment, 5 | aws_cloudfront_origins as origins, 6 | aws_iam as iam, 7 | aws_s3 as s3, 8 | aws_cloudfront as cf, 9 | aws_cloudfront as cloudfront, 10 | } from "aws-cdk-lib"; 11 | import { Construct } from "constructs"; 12 | import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs"; 13 | import * as path from "path"; 14 | 15 | export class AmplifyStack extends Construct { 16 | public readonly frontPage: string; 17 | public readonly agwUrl: string; 18 | constructor(scope: Construct, id: string, region: string, table: dynamodb.Table, cfDomain: string) { 19 | super(scope, id); 20 | 21 | // Step 1: Lambda that scans the dynamoDB table 22 | const lamdbaRole = new iam.Role(this, "RoleForRekognitionLambad", { 23 | assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), 24 | managedPolicies: [ 25 | iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonDynamoDBFullAccess"), 26 | iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole"), 27 | ], 28 | }); 29 | 30 | const myLambda = new NodejsFunction(this, "RekognitionInvoker", { 31 | role: lamdbaRole, 32 | entry: path.join(__dirname, `/../resources/getData.ts`), 33 | handler: "handler", 34 | environment: { 35 | REGION: region, 36 | DDB_TABLE: table.tableName!, 37 | CF_URL: cfDomain, 38 | }, 39 | }); 40 | 41 | // Step 2: API Gateway 42 | const apiGateway = new agw.LambdaRestApi(this, "ApiGateway", { handler: myLambda, proxy: false }); 43 | const vod = apiGateway.root.addResource("vod"); // GET /vod 44 | vod.addMethod("GET"); 45 | 46 | this.agwUrl = apiGateway.url; 47 | 48 | // S3 bucket for static files(html, css, react) 49 | const websiteBucket = new s3.Bucket(this, "WebsiteBucket"); 50 | 51 | const websiteDeployment = new deployment.BucketDeployment(this, "WebsiteDeployment", { 52 | sources: [deployment.Source.asset("./static")], 53 | destinationBucket: websiteBucket, 54 | }); 55 | 56 | const originAccessIdentity = new cloudfront.OriginAccessIdentity(this, "OAIforWebsiteBucket"); 57 | const s3Origin = new origins.S3Origin(websiteBucket, { originAccessIdentity }); 58 | 59 | const cfDistribution = new cloudfront.Distribution(this, "DistributionForWebsiteBucket", { 60 | defaultRootObject: "index.html", 61 | defaultBehavior: { 62 | origin: s3Origin, 63 | cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED, 64 | allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD, 65 | viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.ALLOW_ALL, 66 | originRequestPolicy: cloudfront.OriginRequestPolicy.CORS_S3_ORIGIN, 67 | responseHeadersPolicy: cloudfront.ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT, 68 | }, 69 | }); 70 | 71 | this.frontPage = cfDistribution.domainName; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/cloudfront-streaming.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Aws, 3 | aws_mediapackage as mediapackage, 4 | aws_cloudfront as cloudfront, 5 | aws_cloudfront_origins as origins, 6 | aws_s3 as s3, 7 | CfnOutput, 8 | Fn, 9 | } from "aws-cdk-lib"; 10 | import { Construct } from "constructs"; 11 | import { Secrets } from "./secrets_mediapackage"; 12 | import * as cdk from "aws-cdk-lib"; 13 | import { NagSuppressions } from "cdk-nag"; 14 | 15 | export class CloudFrontForStreaming extends Construct { 16 | //👇 Defining public variables to export on CloudFormation 17 | public readonly channel: mediapackage.CfnChannel; 18 | public readonly hlsPlayback: string; 19 | public readonly dashPlayback: string; 20 | public readonly s3LogBucket: s3.Bucket; 21 | 22 | //Defining private Variable 23 | private readonly CDNHEADER = "MediaPackageCDNIdentifier"; 24 | private readonly DESCRIPTIONDISTRIBUTION = " - CDK deployment Live Streaming Distribution"; 25 | 26 | constructor(scope: Construct, id: string, hlsEndpoint: string, dashEndpoint: string) { 27 | super(scope, id); 28 | 29 | /* 30 | * First step: Create S3 bucket for logs 👇 31 | */ 32 | //👇 1. Creating S3 Buckets for logs and demo website 33 | const s3Logs = new s3.Bucket(this, "LogsBucket", { 34 | removalPolicy: cdk.RemovalPolicy.DESTROY, 35 | autoDeleteObjects: true, 36 | encryption: s3.BucketEncryption.S3_MANAGED, 37 | enforceSSL: true, 38 | blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, 39 | accessControl: s3.BucketAccessControl.BUCKET_OWNER_FULL_CONTROL, 40 | objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_PREFERRED 41 | }); 42 | NagSuppressions.addResourceSuppressions(s3Logs, [ 43 | { 44 | id: "AwsSolutions-S1", 45 | reason: "Remediated through property override.", 46 | }, 47 | ]); 48 | 49 | /* 50 | * Second step: Create CloudFront Policies and Origins👇 51 | */ 52 | //👇 2. Prepare for CloudFront distribution 53 | //2.1 Creating Origin Request Policies for MediaTailor & MediaPackage Origin 54 | const mediaPackageHostname = Fn.select(2, Fn.split("/", hlsEndpoint)); 55 | 56 | const myOriginRequestPolicy = new cloudfront.OriginRequestPolicy(this, "OriginRequestPolicy", { 57 | originRequestPolicyName: Aws.STACK_NAME + "Viewer-Country-City", 58 | comment: "Policy to FWD CloudFront headers", 59 | headerBehavior: cloudfront.OriginRequestHeaderBehavior.allowList( 60 | "CloudFront-Viewer-Address", 61 | "CloudFront-Viewer-Country", 62 | "CloudFront-Viewer-City", 63 | "Referer", 64 | "User-Agent", 65 | "Access-Control-Request-Method", 66 | "Access-Control-Request-Headers" 67 | ), 68 | queryStringBehavior: cloudfront.OriginRequestQueryStringBehavior.all(), 69 | }); 70 | 71 | const mediaPackageOrigin = new origins.HttpOrigin(mediaPackageHostname, { 72 | originSslProtocols: [cloudfront.OriginSslPolicy.TLS_V1_2], 73 | protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY, 74 | }); 75 | 76 | /* 77 | * Third step: Create CloudFront Distributions 👇 78 | */ 79 | //👇 3. Creating CloudFront distributions 80 | 81 | //3.1. Distribution for media Live distribution 82 | const distribution = new cloudfront.Distribution(this, "DistributionForStreaming", { 83 | comment: Aws.STACK_NAME + this.DESCRIPTIONDISTRIBUTION, 84 | sslSupportMethod: cloudfront.SSLMethod.VIP, 85 | enableLogging: true, 86 | logBucket: s3Logs, 87 | logFilePrefix: "distribution-access-logs/", 88 | defaultRootObject: "", 89 | minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_1_2016, 90 | defaultBehavior: { 91 | origin: mediaPackageOrigin, 92 | cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED, 93 | allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD, 94 | viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 95 | originRequestPolicy: myOriginRequestPolicy, 96 | responseHeadersPolicy: cloudfront.ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT, 97 | }, 98 | additionalBehaviors: { 99 | "*.m3u8": { 100 | origin: mediaPackageOrigin, 101 | viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 102 | cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, 103 | originRequestPolicy: myOriginRequestPolicy, 104 | responseHeadersPolicy: cloudfront.ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT, 105 | }, 106 | "*.ts": { 107 | origin: mediaPackageOrigin, 108 | viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 109 | cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED, 110 | originRequestPolicy: myOriginRequestPolicy, 111 | responseHeadersPolicy: cloudfront.ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT, 112 | }, 113 | "*.mpd": { 114 | origin: mediaPackageOrigin, 115 | viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 116 | cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, 117 | originRequestPolicy: myOriginRequestPolicy, 118 | responseHeadersPolicy: cloudfront.ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT, 119 | }, 120 | "*.mp4": { 121 | origin: mediaPackageOrigin, 122 | viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 123 | cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED, 124 | originRequestPolicy: myOriginRequestPolicy, 125 | responseHeadersPolicy: cloudfront.ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT, 126 | }, 127 | }, 128 | }); 129 | NagSuppressions.addResourceSuppressions(distribution, [ 130 | { 131 | id: " AwsSolutions-CFR4", 132 | reason: "Remediated through property override.", 133 | }, 134 | ]); 135 | /* 136 | * Final step: Exporting Varibales for Cfn Outputs 👇 137 | */ 138 | this.s3LogBucket = s3Logs; 139 | 140 | //👇 Getting the path from EMP (/out/v1/) endpoint 141 | const hlsPathEMP = Fn.select(1, Fn.split("/out/", hlsEndpoint)); 142 | const dashPathEMP = Fn.select(1, Fn.split("/out/", dashEndpoint)); 143 | 144 | this.hlsPlayback = "https://" + distribution.domainName + "/out/" + hlsPathEMP; 145 | this.dashPlayback = "https://" + distribution.domainName + "/out/" + dashPathEMP; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /lib/cloudfront-vod.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from "constructs"; 2 | import { 3 | Aws, 4 | aws_cloudfront as cloudfront, 5 | aws_cloudfront_origins as origins, 6 | aws_s3 as s3, 7 | CfnOutput, 8 | Fn, 9 | } from "aws-cdk-lib"; 10 | 11 | export class CloudFrontForVod extends Construct { 12 | public readonly domainName: string; 13 | constructor(scope: Construct, id: string, vodBucket: s3.Bucket) { 14 | super(scope, id); 15 | 16 | const s3Origin = new origins.S3Origin(vodBucket); 17 | 18 | const distribution = new cloudfront.Distribution(this, "DistributionForVod", { 19 | defaultBehavior: { 20 | origin: s3Origin, 21 | cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED, 22 | allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD, 23 | viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.ALLOW_ALL, 24 | originRequestPolicy: cloudfront.OriginRequestPolicy.CORS_S3_ORIGIN, 25 | responseHeadersPolicy: cloudfront.ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT, 26 | }, 27 | additionalBehaviors: { 28 | "*.m3u8": { 29 | origin: s3Origin, 30 | viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.ALLOW_ALL, 31 | cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, 32 | originRequestPolicy: cloudfront.OriginRequestPolicy.CORS_S3_ORIGIN, 33 | responseHeadersPolicy: cloudfront.ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT, 34 | }, 35 | "*.ts": { 36 | origin: s3Origin, 37 | viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.ALLOW_ALL, 38 | cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED, 39 | originRequestPolicy: cloudfront.OriginRequestPolicy.CORS_S3_ORIGIN, 40 | responseHeadersPolicy: cloudfront.ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT, 41 | }, 42 | "*.mpd": { 43 | origin: s3Origin, 44 | viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.ALLOW_ALL, 45 | cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, 46 | originRequestPolicy: cloudfront.OriginRequestPolicy.CORS_S3_ORIGIN, 47 | responseHeadersPolicy: cloudfront.ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT, 48 | }, 49 | "*.mp4": { 50 | origin: s3Origin, 51 | viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 52 | cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED, 53 | originRequestPolicy: cloudfront.OriginRequestPolicy.CORS_S3_ORIGIN, 54 | responseHeadersPolicy: cloudfront.ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT, 55 | }, 56 | }, 57 | }); 58 | 59 | this.domainName = distribution.domainName; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/event-replay-engine.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CfnOutput, 3 | Fn, 4 | Aws, 5 | aws_s3 as s3, 6 | aws_sqs as sqs, 7 | aws_dynamodb as dynamodb, 8 | Duration, 9 | aws_cloudfront as cloudfront, 10 | } from "aws-cdk-lib"; 11 | import * as cdk from "aws-cdk-lib"; 12 | import { Construct } from "constructs"; 13 | import { MediaPackageCdnAuth } from "./media_package"; 14 | import { MediaLive } from "./media_live"; 15 | import { CloudFrontForStreaming } from "./cloudfront-streaming"; 16 | import { CloudFrontForVod } from "./cloudfront-vod"; 17 | import { S3LambdaToSQS } from "./s3lambda-to-sqs"; 18 | import { mediaConvertLambda } from "./sqs-to-mediaconvert"; 19 | import { MediaConvertToRekognition } from "./mediaconvert-rekognition"; 20 | import { AmplifyStack } from "./amplify"; 21 | import * as dotenv from "dotenv"; 22 | import { table } from "console"; 23 | 24 | dotenv.config(); 25 | 26 | const configurationMediaLive = { 27 | streamName: "live", 28 | channelClass: "SINGLE_PIPELINE", 29 | inputType: "RTMP_PUSH", 30 | sourceEndBehavior: "LOOP", 31 | codec: "AVC", 32 | encodingProfile: "HD-720p", 33 | priLink: "", 34 | secLink: "", 35 | inputCidr: "0.0.0.0/0", 36 | priUrl: "", 37 | secUrl: "", 38 | priFlow: "", 39 | secFlow: "", 40 | }; 41 | 42 | const configurationMediaPackage = { 43 | ad_markers: "PASSTHROUGH", 44 | cdn_authorization: false, 45 | hls_segment_duration_seconds: 4, 46 | hls_playlist_window_seconds: 60, 47 | hls_max_video_bits_per_second: 2147483647, 48 | hls_min_video_bits_per_second: 0, 49 | hls_stream_order: "ORIGINAL", 50 | hls_include_I_frame: false, 51 | hls_audio_rendition_group: false, 52 | hls_program_date_interval: 60, 53 | dash_period_triggers: "ADS", 54 | dash_profile: "NONE", 55 | dash_segment_duration_seconds: 2, 56 | dash_segment_template: "TIME_WITH_TIMELINE", 57 | dash_manifest_window_seconds: 60, 58 | dash_max_video_bits_per_second: 2147483647, 59 | dash_min_video_bits_per_second: 0, 60 | dash_stream_order: "ORIGINAL", 61 | cmaf_segment_duration_seconds: 4, 62 | cmaf_include_I_frame: false, 63 | cmaf_program_date_interval: 60, 64 | cmaf_max_video_bits_per_second: 2147483647, 65 | cmaf_min_video_bits_per_second: 0, 66 | cmaf_stream_order: "ORIGINAL", 67 | cmaf_playlist_window_seconds: 60, 68 | mss_segment_duration_seconds: 2, 69 | mss_manifest_window_seconds: 60, 70 | mss_max_video_bits_per_second: 2147483647, 71 | mss_min_video_bits_per_second: 0, 72 | mss_stream_order: "ORIGINAL", 73 | }; 74 | 75 | export class EventReplayEngine extends cdk.Stack { 76 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 77 | super(scope, id, props); 78 | 79 | const region = this.region; 80 | console.log(__dirname); 81 | console.log("Current Region: ", region); 82 | 83 | // S3 bucket for MediaLive Archive 84 | const mediaLiveArchiveBucket = new s3.Bucket( 85 | this, 86 | "mediaLiveArchiveBucket" 87 | ); 88 | const mediaConvertBucket = new s3.Bucket(this, "MediaConvertBucket"); 89 | const rekognitionBucket = new s3.Bucket(this, "RekognitionBucket"); 90 | 91 | // DynamoDB Table for this stack 92 | const ddbTable = new dynamodb.Table(this, "EventReplayEnginTable", { 93 | billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, 94 | partitionKey: { 95 | name: "PK", 96 | type: dynamodb.AttributeType.STRING, 97 | }, 98 | sortKey: { 99 | name: "SK", 100 | type: dynamodb.AttributeType.STRING, 101 | }, 102 | }); 103 | /* 104 | * First step: Create MediaPackage Channel 👇 105 | */ 106 | const mediaPackageChannel = new MediaPackageCdnAuth( 107 | this, 108 | "MyMediaPackageChannel", 109 | configurationMediaPackage 110 | ); 111 | 112 | /* 113 | * Second step: Create MediaLive Channel 👇 114 | */ 115 | 116 | const mediaLiveChannel = new MediaLive( 117 | this, 118 | "MyMediaLiveChannel", 119 | configurationMediaLive, 120 | mediaPackageChannel.myChannel.id, 121 | mediaLiveArchiveBucket.bucketName 122 | ); 123 | 124 | /* 125 | * Third step: Create CloudFront Distribution for Streaming Event 👇 126 | */ 127 | const streamingCloudfront = new CloudFrontForStreaming( 128 | this, 129 | "MyCloudFrontDistribution", 130 | mediaPackageChannel.myChannelEndpointHls.attrUrl, 131 | mediaPackageChannel.myChannelEndpointDash.attrUrl 132 | ); 133 | 134 | //👇Add dependencyto wait for MediaPackage channel to be ready before deploying MediaLive 135 | mediaLiveChannel.node.addDependency(mediaPackageChannel); 136 | 137 | // SQS to queue converting jobs 138 | const lambdaReqQueue = new sqs.Queue(this, "EventReplayEngine", { 139 | visibilityTimeout: Duration.seconds(60), 140 | }); 141 | const fromS3LamdbatoSQS = new S3LambdaToSQS( 142 | this, 143 | "S3LamdbaToSQS", 144 | region, 145 | mediaLiveArchiveBucket, 146 | lambdaReqQueue 147 | ); 148 | 149 | const sqsToMediaConvert = new mediaConvertLambda( 150 | this, 151 | "LamdbaCallingMediaConvert", 152 | region, 153 | lambdaReqQueue.queueUrl, 154 | mediaConvertBucket, 155 | rekognitionBucket, 156 | lambdaReqQueue 157 | ); 158 | 159 | const mediaConvertToRekognition = new MediaConvertToRekognition( 160 | this, 161 | "MediaConvertToRekognition", 162 | region, 163 | rekognitionBucket, 164 | ddbTable.tableName 165 | ); 166 | 167 | // CloudFront Distribution for VOD contents 168 | const cloudfrontForVod = new CloudFrontForVod( 169 | this, 170 | "CloudFrontForVod", 171 | mediaConvertBucket 172 | ); 173 | 174 | // Resources for Amplify Frontend: API Gateway + Lambda 175 | const frontendStack = new AmplifyStack( 176 | this, 177 | "AmplifyFrontend", 178 | region, 179 | ddbTable, 180 | cloudfrontForVod.domainName 181 | ); 182 | 183 | /* 184 | * Final step: CloudFormation Output 👇 185 | */ 186 | // MediaLive 👇 187 | new CfnOutput(this, "MyMediaLiveChannelArn", { 188 | value: mediaLiveChannel.myChannelArn, 189 | exportName: Aws.STACK_NAME + "mediaLiveChannelArn", 190 | description: "The Arn of the MediaLive Channel", 191 | }); 192 | new CfnOutput(this, "MyMediaLiveChannelInputName", { 193 | value: mediaLiveChannel.myChannelInput, 194 | exportName: Aws.STACK_NAME + "mediaLiveChannelInputName", 195 | description: "The Input Name of the MediaLive Channel", 196 | }); 197 | if ( 198 | ["UDP_PUSH", "RTP_PUSH", "RTMP_PUSH"].includes( 199 | configurationMediaLive["inputType"] 200 | ) 201 | ) { 202 | if (configurationMediaLive["channelClass"] == "STANDARD") { 203 | new CfnOutput(this, "MyMediaLiveChannelDestPri", { 204 | value: Fn.join("", [ 205 | Fn.select(0, mediaLiveChannel.channelInput.attrDestinations), 206 | ]), 207 | exportName: Aws.STACK_NAME + "mediaLiveChannelDestPri", 208 | description: "Primary MediaLive input Url", 209 | }); 210 | new CfnOutput(this, "MyMediaLiveChannelDestSec", { 211 | value: Fn.join("", [ 212 | Fn.select(1, mediaLiveChannel.channelInput.attrDestinations), 213 | ]), 214 | exportName: Aws.STACK_NAME + "mediaLiveChannelDestSec", 215 | description: "Seconday MediaLive input Url", 216 | }); 217 | } else { 218 | new CfnOutput(this, "MyMediaLiveChannelDestPri", { 219 | value: Fn.join("", [ 220 | Fn.select(0, mediaLiveChannel.channelInput.attrDestinations), 221 | ]), 222 | exportName: Aws.STACK_NAME + "mediaLiveChannelDestPri", 223 | description: "Primary MediaLive input Url", 224 | }); 225 | } 226 | } 227 | new CfnOutput(this, "MyRegion", { 228 | value: region! as string, 229 | exportName: Aws.STACK_NAME + "currentRegion", 230 | description: "Deployed Region", 231 | }); 232 | new CfnOutput(this, "MyMediaLiveArchiveBucket", { 233 | value: mediaLiveArchiveBucket.bucketName, 234 | exportName: Aws.STACK_NAME + "mediaLiveArchiveBucket", 235 | description: "MediaLive S3 Archive Bucket", 236 | }); 237 | new CfnOutput(this, "MyMediaConvertBucket", { 238 | value: mediaConvertBucket.bucketName, 239 | exportName: Aws.STACK_NAME + "mediaConvertBucket", 240 | description: "MediaConvert Output Buckets", 241 | }); 242 | new CfnOutput(this, "MyRekognitionBucket", { 243 | value: rekognitionBucket.bucketName, 244 | exportName: Aws.STACK_NAME + "rekognitionBucket", 245 | description: "Rekognition Analysis Bucket", 246 | }); 247 | 248 | new CfnOutput(this, "MyMediaLiveChannelS3ArchivePath", { 249 | value: mediaLiveChannel.s3ArchivePath, 250 | exportName: Aws.STACK_NAME + "mediaLiveChannelS3ArchivePath", 251 | description: "S3 Archive Path of the MediaLive Channel", 252 | }); 253 | 254 | // MediaPackage 👇 255 | new CfnOutput(this, "MyMediaPackageChannelName", { 256 | value: mediaPackageChannel.myChannelName, 257 | exportName: Aws.STACK_NAME + "mediaPackageName", 258 | description: "The name of the MediaPackage Channel", 259 | }); 260 | 261 | // CloudFront 👇 262 | new CfnOutput(this, "MyStreamingCloudFrontHlsEndpoint", { 263 | value: streamingCloudfront.hlsPlayback, 264 | exportName: Aws.STACK_NAME + "cloudFrontHlsEndpoint", 265 | description: "The HLS playback endpoint", 266 | }); 267 | new CfnOutput(this, "MyStreamingloudFrontDashEndpoint", { 268 | value: streamingCloudfront.dashPlayback, 269 | exportName: Aws.STACK_NAME + "cloudFrontDashEndpoint", 270 | description: "The MPEG DASH playback endpoint", 271 | }); 272 | // Exporting S3 Buckets for the Log and the hosting demo 273 | new CfnOutput(this, "MyStreamingCloudFrontS3LogBucket", { 274 | value: streamingCloudfront.s3LogBucket.bucketName, 275 | exportName: Aws.STACK_NAME + "cloudFrontS3BucketLog", 276 | description: "The S3 bucket for CloudFront logs", 277 | }); 278 | new CfnOutput(this, "MyStreamingCloudFrontForVod", { 279 | value: cloudfrontForVod.domainName, 280 | exportName: Aws.STACK_NAME + "CloudFrontForVod", 281 | description: "CloudFront Domain Name for Vod Contents", 282 | }); 283 | new CfnOutput(this, "APIGatewayURLforAmplify", { 284 | value: frontendStack.agwUrl, 285 | exportName: Aws.STACK_NAME + "APIGatewayURL", 286 | description: "API Gateway URL for Amplify Frontend", 287 | }); 288 | new CfnOutput(this, "FrontPageURL", { 289 | value: "http://" + frontendStack.frontPage, 290 | exportName: Aws.STACK_NAME + "FrontPageURL", 291 | description: 292 | "Frontend web page in S3 web hosting with CloudFront distribution.", 293 | }); 294 | new CfnOutput(this, "DDBTableName", { 295 | value: ddbTable.tableName, 296 | exportName: Aws.STACK_NAME + "DDBTableName", 297 | description: "DynamoDB table that stores the metadata of clips.", 298 | }); 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /lib/media_live.ts: -------------------------------------------------------------------------------- 1 | import { aws_medialive as medialive, aws_iam as iam, Aws, aws_s3 as s3 } from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import { NagSuppressions } from "cdk-nag"; 4 | 5 | export class MediaLive extends Construct { 6 | public readonly channelLive: medialive.CfnChannel; 7 | public readonly channelInput: medialive.CfnInput; 8 | public readonly myChannelArn: string; 9 | public readonly myChannelName: string; 10 | public readonly myChannelInput: string; 11 | public readonly s3ArchivePath: string; 12 | 13 | constructor( 14 | scope: Construct, 15 | id: string, 16 | configuration: any, 17 | mediaPackageChannelId: string, 18 | s3ArchiveBucket: string 19 | ) { 20 | super(scope, id); 21 | const myMediaLiveChannelName = Aws.STACK_NAME + "_EML-CDK"; 22 | 23 | let destinationValue = []; 24 | let inputSettingsValue = {}; 25 | 26 | /* 27 | * First step: Create MediaLive Policy & Role 👇 28 | */ 29 | 30 | //👇Generate Policy for MediaLive to access MediaPackage, MediaConnect, S3, MediaStore... 31 | const customPolicyMediaLive = new iam.PolicyDocument({ 32 | statements: [ 33 | new iam.PolicyStatement({ 34 | resources: ["*"], 35 | actions: [ 36 | "s3:ListBucket", 37 | "s3:PutObject", 38 | "s3:GetObject", 39 | "s3:DeleteObject", 40 | "mediastore:ListContainers", 41 | "mediastore:PutObject", 42 | "mediastore:GetObject", 43 | "mediastore:DeleteObject", 44 | "mediastore:DescribeObject", 45 | "mediaconnect:ManagedDescribeFlow", 46 | "mediaconnect:ManagedAddOutput", 47 | "mediaconnect:ManagedRemoveOutput", 48 | "logs:CreateLogGroup", 49 | "logs:CreateLogStream", 50 | "logs:PutLogEvents", 51 | "logs:DescribeLogStreams", 52 | "logs:DescribeLogGroups", 53 | "mediaconnect:ManagedDescribeFlow", 54 | "mediaconnect:ManagedAddOutput", 55 | "mediaconnect:ManagedRemoveOutput", 56 | "ec2:describeSubnets", 57 | "ec2:describeNetworkInterfaces", 58 | "ec2:createNetworkInterface", 59 | "ec2:createNetworkInterfacePermission", 60 | "ec2:deleteNetworkInterface", 61 | "ec2:deleteNetworkInterfacePermission", 62 | "ec2:describeSecurityGroups", 63 | "mediapackage:DescribeChannel", 64 | ], 65 | }), 66 | ], 67 | }); 68 | 69 | //👇Generate a Role for MediaLive to access MediaPackage and S3. You can modify the role to restrict to specific S3 buckets 70 | const role = new iam.Role(this, "MediaLiveAccessRole", { 71 | inlinePolicies: { 72 | policy: customPolicyMediaLive, 73 | }, 74 | assumedBy: new iam.ServicePrincipal("medialive.amazonaws.com"), 75 | }); 76 | NagSuppressions.addResourceSuppressions(role, [ 77 | { 78 | id: "AwsSolutions-IAM5", 79 | reason: "Remediated through property override.", 80 | }, 81 | ]); 82 | /* 83 | * Second step: Create Security Groups 👇 84 | */ 85 | //👇Generate Security Groups for RTP and RTMP (Push) inputs 86 | const mediaLiveSG = new medialive.CfnInputSecurityGroup(this, "MediaLiveInputSecurityGroup", { 87 | whitelistRules: [ 88 | { 89 | cidr: configuration["inputCidr"], 90 | }, 91 | ], 92 | }); 93 | 94 | /* 95 | * Third step: Create Input and specific info based on the input types 👇 96 | */ 97 | //👇 1. Create a MediaLive input 98 | const inputName = Aws.STACK_NAME + "_" + configuration["inputType"] + "_MediaLiveInput"; 99 | let cfnInputProps: medialive.CfnInputProps = { 100 | name: "", 101 | roleArn: "", 102 | type: "", 103 | inputSecurityGroups: [], 104 | destinations: [ 105 | { 106 | streamName: "", 107 | }, 108 | ], 109 | inputDevices: [ 110 | { 111 | id: "", 112 | }, 113 | ], 114 | mediaConnectFlows: [ 115 | { 116 | flowArn: "", 117 | }, 118 | ], 119 | sources: [ 120 | { 121 | passwordParam: "passwordParam", 122 | url: "url", 123 | username: "username", 124 | }, 125 | ], 126 | vpc: { 127 | securityGroupIds: [""], 128 | subnetIds: [""], 129 | }, 130 | }; 131 | 132 | //👇1.1 Testing the Input Type 133 | switch (configuration["inputType"]) { 134 | case "INPUT_DEVICE": 135 | //👇 Validating if STANDARD or SINGLE_PIPELINE Channel to provide 1 or 2 InputDevice 136 | if (configuration["channelClass"] == "STANDARD") { 137 | destinationValue = [{ id: configuration["priLink"] }, { id: configuration["secLink"] }]; 138 | } else { 139 | destinationValue = [{ id: configuration["priLink"] }]; 140 | } 141 | cfnInputProps = { 142 | name: inputName, 143 | type: configuration["inputType"], 144 | inputDevices: destinationValue, 145 | }; 146 | break; 147 | 148 | case "RTP_PUSH": 149 | cfnInputProps = { 150 | name: inputName, 151 | type: configuration["inputType"], 152 | inputSecurityGroups: [mediaLiveSG.ref], 153 | }; 154 | break; 155 | case "RTMP_PUSH": 156 | //👇 Validating if STANDARD or SINGLE_PIPELINE Channel to provide 1 or 2 URL 157 | if (configuration["channelClass"] == "STANDARD") { 158 | destinationValue = [ 159 | { streamName: configuration["streamName"] + "/primary" }, 160 | { streamName: configuration["streamName"] + "/secondary" }, 161 | ]; 162 | } else { 163 | destinationValue = [{ streamName: configuration["streamName"] + "/primary" }]; 164 | } 165 | cfnInputProps = { 166 | name: inputName, 167 | type: configuration["inputType"], 168 | inputSecurityGroups: [mediaLiveSG.ref], 169 | destinations: destinationValue, 170 | }; 171 | break; 172 | case "MP4_FILE": 173 | case "RTMP_PULL": 174 | case "URL_PULL": 175 | case "TS_FILE": 176 | //👇 Validating if STANDARD or SINGLE_PIPELINE Channel to provide 1 or 2 URL 177 | if (configuration["channelClass"] == "STANDARD") { 178 | destinationValue = [{ url: configuration["priUrl"] }, { url: configuration["secUrl"] }]; 179 | } else { 180 | destinationValue = [{ url: configuration["priUrl"] }]; 181 | } 182 | cfnInputProps = { 183 | name: inputName, 184 | type: configuration["inputType"], 185 | sources: destinationValue, 186 | }; 187 | inputSettingsValue = { sourceEndBehavior: configuration["sourceEndBehavior"] }; 188 | break; 189 | case "MEDIACONNECT": 190 | //👇 Validating if STANDARD or SINGLE_PIPELINE Channel to provide 1 or 2 URL 191 | if (configuration["channelClass"] == "STANDARD") { 192 | destinationValue = [{ flowArn: configuration["priFlow"] }, { flowArn: configuration["secFlow"] }]; 193 | } else { 194 | destinationValue = [{ flowArn: configuration["priFlow"] }]; 195 | } 196 | cfnInputProps = { 197 | name: inputName, 198 | type: configuration["inputType"], 199 | roleArn: role.roleArn, 200 | mediaConnectFlows: destinationValue, 201 | }; 202 | break; 203 | } 204 | 205 | const mediaLiveInput = new medialive.CfnInput(this, "MediaInputChannel", cfnInputProps); 206 | 207 | //2. Create Channel 208 | 209 | const s3ArchivePath = "s3ssl://" + s3ArchiveBucket + "/medialiveArchive/"; 210 | 211 | let params = { 212 | resolution: "", 213 | maximumBitrate: "", 214 | encoderSettings: "", 215 | }; 216 | 217 | switch (configuration["encodingProfile"]) { 218 | case "HD-1080p": 219 | params.resolution = "HD"; 220 | params.maximumBitrate = "MAX_20_MBPS"; 221 | params.encoderSettings = require("../config/encoding-profiles/hd-1080p"); 222 | break; 223 | case "HD-720p": 224 | params.resolution = "HD"; 225 | params.maximumBitrate = "MAX_10_MBPS"; 226 | params.encoderSettings = require("../config/encoding-profiles/hd-720p"); 227 | break; 228 | case "SD-540p": 229 | params.resolution = "SD"; 230 | params.maximumBitrate = "MAX_10_MBPS"; 231 | params.encoderSettings = require("../config/encoding-profiles/sd-540p"); 232 | break; 233 | default: 234 | throw new Error(`EncodingProfile is invalid or undefined: ${configuration["encodingProfile"]}`); 235 | } 236 | 237 | const channelLive = new medialive.CfnChannel(this, "MediaLiveChannel", { 238 | channelClass: configuration["channelClass"], 239 | destinations: [ 240 | { 241 | id: "media-destination", 242 | mediaPackageSettings: [ 243 | { 244 | channelId: mediaPackageChannelId, 245 | }, 246 | ], 247 | }, 248 | { 249 | id: "s3-archive", 250 | settings: [ 251 | { 252 | url: s3ArchivePath, 253 | }, 254 | ], 255 | mediaPackageSettings: [], 256 | }, 257 | ], 258 | inputSpecification: { 259 | codec: configuration.codec, 260 | resolution: params.resolution, 261 | maximumBitrate: params.maximumBitrate, 262 | }, 263 | name: myMediaLiveChannelName, 264 | roleArn: role.roleArn, 265 | inputAttachments: [ 266 | { 267 | inputId: mediaLiveInput.ref, 268 | inputAttachmentName: inputName, 269 | inputSettings: inputSettingsValue, 270 | }, 271 | ], 272 | encoderSettings: params.encoderSettings as medialive.CfnChannel.EncoderSettingsProperty, 273 | }); 274 | 275 | /* 276 | * Final step: Exporting Varibales for Cfn Outputs 👇 277 | */ 278 | this.myChannelName = myMediaLiveChannelName; 279 | this.myChannelArn = channelLive.attrArn; 280 | this.myChannelInput = inputName; 281 | this.channelInput = mediaLiveInput; 282 | this.s3ArchivePath = s3ArchivePath; 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /lib/media_package.ts: -------------------------------------------------------------------------------- 1 | import { Aws, aws_iam as iam, Duration, CfnOutput, aws_mediapackage as mediapackage } from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | // import { Secrets } from "./secrets_mediapackage"; 4 | 5 | export class MediaPackageCdnAuth extends Construct { 6 | public readonly myChannel: mediapackage.CfnChannel; 7 | public readonly myChannelEndpointHls: mediapackage.CfnOriginEndpoint; 8 | public readonly myChannelEndpointDash: mediapackage.CfnOriginEndpoint; 9 | public readonly myChannelEndpointCmaf: mediapackage.CfnOriginEndpoint; 10 | 11 | // public readonly secret: Secrets; 12 | public readonly myChannelName: string; 13 | 14 | constructor(scope: Construct, id: string, configuration: any) { 15 | super(scope, id); 16 | const myMediaPackageChannelName = Aws.STACK_NAME + "_EMP-CDK"; 17 | //const configuration = loadMediaPackageconfiguration(); 18 | 19 | /* 20 | * First step: Preparing Secrets + IAM 👇 21 | */ 22 | //👇 Creating Secrets for CDN authorization on MediaPackage using Secret Manager 23 | // this.secret = secret; 24 | const adTrigger = [ 25 | "BREAK", 26 | "DISTRIBUTOR_ADVERTISEMENT", 27 | "DISTRIBUTOR_OVERLAY_PLACEMENT_OPPORTUNITY", 28 | "DISTRIBUTOR_PLACEMENT_OPPORTUNITY", 29 | "PROVIDER_ADVERTISEMENT", 30 | "PROVIDER_OVERLAY_PLACEMENT_OPPORTUNITY", 31 | "PROVIDER_PLACEMENT_OPPORTUNITY", 32 | "SPLICE_INSERT", 33 | ]; 34 | 35 | //👇 Create Role to be assumed by MediaPackage 36 | const role4mediapackage = new iam.Role(this, "MyMediaPackageRole", { 37 | description: "A role to be assumed by MediaPackage", 38 | assumedBy: new iam.ServicePrincipal("mediapackage.amazonaws.com"), 39 | maxSessionDuration: Duration.hours(1), 40 | }); 41 | 42 | /* 43 | * Second step: Creating MediaPackage Channel and Endpoints 👇 44 | */ 45 | //👇 Creating EMP channel 46 | this.myChannel = new mediapackage.CfnChannel(this, "MyCfnChannel", { 47 | id: myMediaPackageChannelName, 48 | description: "Channel for " + Aws.STACK_NAME, 49 | }); 50 | 51 | //👇 HLS Packaging & endpoint with CDN authorization 52 | const hlsPackage: mediapackage.CfnOriginEndpoint.HlsPackageProperty = { 53 | adMarkers: configuration["ad_markers"], 54 | adTriggers: adTrigger, 55 | segmentDurationSeconds: configuration["hls_segment_duration_seconds"], 56 | programDateTimeIntervalSeconds: configuration["hls_program_date_interval"], 57 | playlistWindowSeconds: configuration["hls_playlist_window_seconds"], 58 | useAudioRenditionGroup: configuration["hls_audio_rendition_group"], 59 | includeIframeOnlyStream: configuration["hls_include_I_frame"], 60 | streamSelection: { 61 | minVideoBitsPerSecond: configuration["hls_min_video_bits_per_second"], 62 | maxVideoBitsPerSecond: configuration["hls_max_video_bits_per_second"], 63 | streamOrder: configuration["hls_stream_order"], 64 | }, 65 | }; 66 | const hlsEndpoint = new mediapackage.CfnOriginEndpoint(this, "HlsEndpoint", { 67 | channelId: this.myChannel.id, 68 | id: Aws.STACK_NAME + "-hls-" + this.myChannel.id, 69 | hlsPackage, 70 | // the properties below are optional 71 | }); 72 | 73 | hlsEndpoint.node.addDependency(this.myChannel); 74 | 75 | const dashPackage: mediapackage.CfnOriginEndpoint.DashPackageProperty = { 76 | periodTriggers: [configuration["dash_period_triggers"]], 77 | adTriggers: adTrigger, 78 | segmentDurationSeconds: configuration["dash_segment_duration_seconds"], 79 | segmentTemplateFormat: configuration["dash_segment_template"], 80 | profile: configuration["dash_profile"], 81 | minBufferTimeSeconds: 10, 82 | minUpdatePeriodSeconds: configuration["dash_segment_duration_seconds"], 83 | manifestWindowSeconds: configuration["dash_manifest_window_seconds"], 84 | streamSelection: { 85 | minVideoBitsPerSecond: configuration["dash_min_video_bits_per_second"], 86 | maxVideoBitsPerSecond: configuration["dash_max_video_bits_per_second"], 87 | streamOrder: configuration["dash_stream_order"], 88 | }, 89 | }; 90 | const dashEndpoint = new mediapackage.CfnOriginEndpoint(this, "DashEndpoint", { 91 | channelId: this.myChannel.id, 92 | id: Aws.STACK_NAME + "-dash-" + this.myChannel.id, 93 | dashPackage, 94 | }); 95 | 96 | dashEndpoint.node.addDependency(this.myChannel); 97 | //👇 CMAF Packaging & endpoint with CDN authorization 98 | 99 | const cmafPackage: mediapackage.CfnOriginEndpoint.CmafPackageProperty = { 100 | hlsManifests: [ 101 | { 102 | id: Aws.STACK_NAME + "-cmaf-" + this.myChannel.id, 103 | // the properties below are optional 104 | adMarkers: configuration["ad_markers"], 105 | adTriggers: adTrigger, 106 | includeIframeOnlyStream: configuration["cmaf_include_I_frame"], 107 | manifestName: "index", 108 | playlistWindowSeconds: configuration["cmaf_playlist_window_seconds"], 109 | programDateTimeIntervalSeconds: configuration["cmaf_program_date_interval"], 110 | url: "url", 111 | }, 112 | ], 113 | segmentDurationSeconds: configuration["cmaf_segment_duration_seconds"], 114 | segmentPrefix: "cmaf", 115 | streamSelection: { 116 | minVideoBitsPerSecond: configuration["cmaf_min_video_bits_per_second"], 117 | maxVideoBitsPerSecond: configuration["cmaf_max_video_bits_per_second"], 118 | streamOrder: configuration["cmaf_stream_order"], 119 | }, 120 | }; 121 | 122 | const cmafEndpoint = new mediapackage.CfnOriginEndpoint(this, "cmafEndpoint", { 123 | channelId: this.myChannel.id, 124 | id: Aws.STACK_NAME + "-cmaf-" + this.myChannel.id, 125 | cmafPackage, 126 | }); 127 | cmafEndpoint.node.addDependency(this.myChannel); 128 | 129 | /* 130 | * Final step: Exporting Varibales for Cfn Outputs 👇 131 | */ 132 | new CfnOutput(this, "MyMediaPackageChannelRole", { 133 | value: role4mediapackage.roleArn, 134 | exportName: Aws.STACK_NAME + "mediaPackageRoleName", 135 | description: "The role of the MediaPackage Channel", 136 | }); 137 | 138 | this.myChannelName = myMediaPackageChannelName; 139 | this.myChannelEndpointHls = hlsEndpoint; 140 | this.myChannelEndpointDash = dashEndpoint; 141 | this.myChannelEndpointCmaf = cmafEndpoint; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /lib/mediaconvert-rekognition.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from "constructs"; 2 | import { 3 | aws_lambda as lambda, 4 | aws_iam as iam, 5 | aws_s3 as s3, 6 | aws_events as eventbridge, 7 | aws_events_targets as targets, 8 | Duration, 9 | aws_s3_notifications as notifications, 10 | Size, 11 | } from "aws-cdk-lib"; 12 | import * as path from "path"; 13 | import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs"; 14 | 15 | export class MediaConvertToRekognition extends Construct { 16 | constructor(scope: Construct, id: string, region: string, rekogBucket: s3.Bucket, tableName: string) { 17 | super(scope, id); 18 | 19 | // Create an event rule for MediaConvert Complete 20 | const rule = new eventbridge.Rule(this, "MediaConvertJobCompleteEvent", { 21 | eventPattern: { 22 | source: ["aws.mediaconvert"], 23 | detail: { status: ["COMPLETE"] }, 24 | }, 25 | }); 26 | 27 | // Create an lambda that is invoked by the event above. 28 | // This lambda will call Rekognition to analyze the video content 29 | const lamdbaRole = new iam.Role(this, "RoleForRekognitionLambad", { 30 | assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), 31 | managedPolicies: [ 32 | iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonRekognitionFullAccess"), 33 | iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonS3FullAccess"), 34 | iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonDynamoDBFullAccess"), 35 | iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole"), 36 | ], 37 | }); 38 | 39 | const myLambda = new NodejsFunction(this, "RekognitionInvoker", { 40 | role: lamdbaRole, 41 | entry: path.join(__dirname, `/../resources/rekognition.ts`), 42 | handler: "handler", 43 | memorySize: 10240, 44 | ephemeralStorageSize: Size.mebibytes(10240), 45 | timeout: Duration.minutes(15), 46 | environment: { 47 | REGION: region, 48 | DEST_BUCKET: rekogBucket.bucketName, 49 | TABLE: tableName, 50 | }, 51 | }); 52 | 53 | // Add a rekognition lambda as a target of EventBridge rule 54 | rule.addTarget( 55 | new targets.LambdaFunction(myLambda, { 56 | retryAttempts: 3, 57 | }) 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/s3lambda-to-sqs.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from "constructs"; 2 | import { 3 | aws_lambda as lambda, 4 | aws_iam as iam, 5 | aws_s3 as s3, 6 | aws_sqs as sqs, 7 | aws_s3_notifications as notifications, 8 | } from "aws-cdk-lib"; 9 | import { ManagedPolicy } from "aws-cdk-lib/aws-iam"; 10 | import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs"; 11 | import * as path from "path"; 12 | 13 | export class S3LambdaToSQS extends Construct { 14 | public readonly sqsQueueURL: string; 15 | constructor(scope: Construct, id: string, region: string, s3ArchiveBucket: s3.Bucket, lambdaReqQueue: sqs.Queue) { 16 | super(scope, id); 17 | 18 | const lamdbaRole = new iam.Role(this, "RoleForS3UploadLambda", { 19 | assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), 20 | managedPolicies: [ 21 | iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonS3FullAccess"), 22 | iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSQSFullAccess"), 23 | iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole"), 24 | ], 25 | }); 26 | 27 | const myLambda = new NodejsFunction(this, "EventReplayEngineS3UploadTrigger", { 28 | role: lamdbaRole, 29 | entry: path.join(__dirname, `/../resources/s3upload.ts`), 30 | handler: "handler", 31 | environment: { 32 | REGION: region, 33 | BUCKET_NAME: s3ArchiveBucket.bucketName, 34 | QUEUE_URL: lambdaReqQueue.queueUrl, 35 | }, 36 | }); 37 | 38 | const trigger = new notifications.LambdaDestination(myLambda); 39 | trigger.bind(this, s3ArchiveBucket); 40 | s3ArchiveBucket.addObjectCreatedNotification(trigger, { suffix: ".ts" }); 41 | 42 | this.sqsQueueURL = lambdaReqQueue.queueUrl; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/secrets_mediapackage.ts: -------------------------------------------------------------------------------- 1 | import { Aws, CfnOutput, aws_secretsmanager as secretsmanager } from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import { NagSuppressions } from "cdk-nag"; 4 | 5 | export class Secrets extends Construct { 6 | public readonly cdnSecret: secretsmanager.ISecret; 7 | 8 | constructor(scope: Construct, id: string) { 9 | super(scope, id); 10 | 11 | const cdnSecret = new secretsmanager.Secret(this, "CdnSecret", { 12 | secretName: "MediaPackage/" + Aws.STACK_NAME, 13 | description: "Secret for Secure Resilient Live Streaming Delivery", 14 | generateSecretString: { 15 | secretStringTemplate: JSON.stringify({ MediaPackageCDNIdentifier: "" }), 16 | generateStringKey: "MediaPackageCDNIdentifier", //MUST keep this StringKey to use with EMP 17 | }, 18 | }); 19 | this.cdnSecret = cdnSecret; 20 | NagSuppressions.addResourceSuppressions(cdnSecret, [ 21 | { 22 | id: "AwsSolutions-SMG4", 23 | reason: "Remediated through property override.", 24 | }, 25 | ]); 26 | 27 | new CfnOutput(this, "cdnSecret", { 28 | value: cdnSecret.secretName, 29 | exportName: Aws.STACK_NAME + "cdnSecret", 30 | description: "The name of the cdnSecret", 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/sqs-to-mediaconvert.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from "constructs"; 2 | import { 3 | aws_lambda as lambda, 4 | aws_iam as iam, 5 | aws_mediaconvert as mediaconvert, 6 | Size, 7 | Duration, 8 | aws_s3 as s3, 9 | aws_sqs as sqs, 10 | aws_lambda_event_sources as eventSource, 11 | } from "aws-cdk-lib"; 12 | import "dotenv/config"; 13 | import * as dotenv from "dotenv"; 14 | import * as path from "path"; 15 | import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs"; 16 | 17 | dotenv.config({ path: __dirname + "/../.env" }); 18 | 19 | export class mediaConvertLambda extends Construct { 20 | public readonly jobTemplateName: string; 21 | constructor( 22 | scope: Construct, 23 | id: string, 24 | region: string, 25 | sqsQueueUrl: string, 26 | mcBucket: s3.Bucket, 27 | rkBucket: s3.Bucket, 28 | jobQueue: sqs.IQueue 29 | ) { 30 | super(scope, id); 31 | 32 | // Create a job template using JSON 33 | const jobTemplateParams = require("../config/encoding-profiles/mc-job-template"); 34 | 35 | // Set S3 bucket name for outputs 36 | jobTemplateParams["OutputGroups"][0]["OutputGroupSettings"]["HlsGroupSettings"]["Destination"] = 37 | "s3://" + mcBucket.bucketName + "/vod-assets/"; 38 | 39 | jobTemplateParams["OutputGroups"][1]["OutputGroupSettings"]["FileGroupSettings"]["Destination"] = 40 | "s3://" + rkBucket.bucketName + "/rekog-data/"; 41 | 42 | const jobTemplate = new mediaconvert.CfnJobTemplate(this, "MyCfnJobTemplate", { 43 | category: "OTT-HLS", 44 | // queue: "arn:aws:mediaconvert:ap-northeast-2:236241703319:queues/Default", 45 | name: "event-replay-engine-job-template", 46 | settingsJson: jobTemplateParams, 47 | accelerationSettings: { 48 | mode: "DISABLED", 49 | }, 50 | statusUpdateInterval: "SECONDS_60", 51 | priority: 0, 52 | hopDestinations: [], 53 | }); 54 | 55 | const jobTemplateName: string = jobTemplate.name!; 56 | 57 | const lamdbaRole = new iam.Role(this, "RoleForMediaConvertLambda", { 58 | assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), 59 | managedPolicies: [ 60 | iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSQSFullAccess"), 61 | iam.ManagedPolicy.fromAwsManagedPolicyName("AWSElementalMediaConvertFullAccess"), 62 | iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonS3FullAccess"), 63 | iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole"), 64 | ], 65 | }); 66 | 67 | // We need another role for MediaConvert 68 | const roleForMediaConvert = new iam.Role(this, "RoleForMediaConvert", { 69 | assumedBy: new iam.ServicePrincipal("mediaconvert.amazonaws.com"), 70 | managedPolicies: [ 71 | iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonS3FullAccess"), 72 | iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonAPIGatewayInvokeFullAccess"), 73 | ], 74 | }); 75 | 76 | const myLambda = new NodejsFunction(this, "EventReplayEngineS3UploadTrigger", { 77 | role: lamdbaRole, 78 | entry: path.join(__dirname, `/../resources/mediaconvert.ts`), 79 | handler: "handler", 80 | ephemeralStorageSize: Size.mebibytes(1024), 81 | timeout: Duration.minutes(1), 82 | environment: { 83 | REGION: region, 84 | QUEUE_URL: sqsQueueUrl, 85 | MC_JOB_TEMPLATE: jobTemplateName, 86 | MC_ROLE_ARN: roleForMediaConvert.roleArn, 87 | }, 88 | }); 89 | 90 | // Register SQS as a event source of lambda_sqs_to_mediaconvert_handler 91 | const sqsEventSource = new eventSource.SqsEventSource(jobQueue, { batchSize: 1 }); 92 | myLambda.addEventSource(sqsEventSource); 93 | 94 | this.jobTemplateName = jobTemplateName; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "event-replay-engine", 3 | "version": "0.1.0", 4 | "bin": { 5 | "event-replay-engine": "bin/event-replay-engine.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^27.5.2", 15 | "@types/node": "10.17.27", 16 | "@types/prettier": "2.6.0", 17 | "aws-cdk": "^2.45.0", 18 | "esbuild": "^0.17.18", 19 | "jest": "^27.5.1", 20 | "ts-jest": "^27.1.4", 21 | "ts-node": "^10.9.1", 22 | "typescript": "~3.9.7" 23 | }, 24 | "dependencies": { 25 | "@aws-sdk/client-dynamodb": "^3.362.0", 26 | "@aws-sdk/client-mediaconvert": "^3.231.0", 27 | "@aws-sdk/client-rekognition": "^3.231.0", 28 | "@aws-sdk/client-sqs": "^3.362.0", 29 | "@types/aws-lambda": "^8.10.109", 30 | "aws-amplify": "^5.3.3", 31 | "aws-cdk-lib": "^2.85.0", 32 | "aws-sdk": "^2.1407.0", 33 | "cdk-nag": "^2.20.5", 34 | "constructs": "^10.0.0", 35 | "dotenv": "^16.0.3", 36 | "source-map-support": "^0.5.21" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /resources/getData.ts: -------------------------------------------------------------------------------- 1 | // Based on the following reference 2 | // https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/dynamodb-example-query-scan.html 3 | 4 | // Create service client module using ES6 syntax. 5 | import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; 6 | import { ScanCommand } from "@aws-sdk/client-dynamodb"; 7 | 8 | interface ISvgObj { 9 | [key: string]: string; 10 | } 11 | 12 | type Record = { 13 | celebName: string; 14 | createdAt: string; 15 | url: string; 16 | }; 17 | 18 | /** 19 | * @type {import('@types/aws-lambda').APIGatewayProxyHandler} 20 | */ 21 | export const handler = async () => { 22 | const region = process.env["REGION"]!; 23 | // Set the AWS Region. 24 | const cfDomain = process.env["CF_URL"]; 25 | 26 | // Create an Amazon DynamoDB service client object. 27 | const ddbTable = process.env["DDB_TABLE"]; 28 | const ddbClient = new DynamoDBClient({ region }); 29 | 30 | // Set the parameters. 31 | const params = { 32 | TableName: ddbTable, 33 | }; 34 | 35 | const data = await ddbClient.send(new ScanCommand(params)); 36 | console.log(JSON.stringify(data.Items)); 37 | 38 | const urls: ISvgObj = {}; 39 | const payload: Record[] = []; 40 | 41 | data.Items!.forEach(function (element, index, array) { 42 | const filepath = element.PATH.S!.split("//")[1].split("/"); 43 | const key = filepath[filepath.length - 1].split("."); 44 | const fileName = key[0] + "." + key[1] + ".m3u8"; 45 | // const folder = filepath.slice(1).join("/"); 46 | const url = cfDomain + "/vod-assets/" + fileName; 47 | console.log(url + " " + key); 48 | urls[fileName] = url; 49 | 50 | const item: Record = { 51 | celebName: element.PK.S! as string, 52 | createdAt: element.SK.S! as string, 53 | url, 54 | }; 55 | 56 | payload.push(item); 57 | }); 58 | 59 | return { 60 | statusCode: 200, 61 | headers: { 62 | "Access-Control-Allow-Origin": "*", 63 | "Access-Control-Allow-Headers": "*", 64 | }, 65 | body: JSON.stringify(payload), 66 | }; 67 | }; 68 | -------------------------------------------------------------------------------- /resources/mediaconvert.ts: -------------------------------------------------------------------------------- 1 | import { SQSClient, ReceiveMessageCommand, DeleteMessageCommand } from "@aws-sdk/client-sqs"; 2 | import { SQSHandler } from "aws-lambda"; 3 | import { MediaConvertClient, CreateJobCommand, DescribeEndpointsCommand } from "@aws-sdk/client-mediaconvert"; 4 | 5 | export const handler: SQSHandler = async (event, context) => { 6 | const queueURL = process.env["QUEUE_URL"]; 7 | const mcJobTemplate: string = process.env["MC_JOB_TEMPLATE"]!; 8 | const mcRoleArn = process.env["MC_ROLE_ARN"]!; 9 | const region = process.env["REGION"]!; 10 | 11 | const sqs = new SQSClient({ region }); 12 | 13 | let mediaconvert = new MediaConvertClient({ region }); 14 | // Get MediaConvert Endpoint 15 | const data = await mediaconvert.send(new DescribeEndpointsCommand({ MaxResults: 0 })); 16 | const endpoint = data.Endpoints![0]; 17 | console.log(`My MediaConvert endpoint is ${JSON.stringify(endpoint)}`); 18 | mediaconvert = new MediaConvertClient({ endpoint: endpoint.Url }); 19 | 20 | console.log(`Number of Records: ${event.Records.length}`); 21 | console.log(`Content of Records: ${JSON.stringify(event)}`); 22 | 23 | for (const record of event.Records) { 24 | const message = record; 25 | const receiptHandle = message.receiptHandle; 26 | const deketeMsgParams = { 27 | QueueUrl: queueURL, 28 | ReceiptHandle: receiptHandle, 29 | }; 30 | 31 | const deleteMsgResponse = await sqs.send(new DeleteMessageCommand(deketeMsgParams)); 32 | 33 | const sourceBucket: string = message["messageAttributes"]!["Bucket"]["stringValue"]!; 34 | const sourceKey: string = message["messageAttributes"]!["Key"]["stringValue"]!; 35 | 36 | const mediaConvertResponse = convertMedia(mediaconvert, sourceBucket, sourceKey, mcJobTemplate, mcRoleArn); 37 | console.log("Response from MediaConvert: ", mediaConvertResponse); 38 | } 39 | }; 40 | 41 | const convertMedia = ( 42 | mediaconvert: MediaConvertClient, 43 | sourceBucket: string, 44 | sourceKey: string, 45 | mcJobTemplate: string, 46 | mcRoleArn: string 47 | ) => { 48 | const inputPath: string = "s3://" + sourceBucket + "/" + sourceKey; 49 | 50 | const rc = mediaconvert.send( 51 | new CreateJobCommand({ 52 | JobTemplate: mcJobTemplate, 53 | Role: mcRoleArn, 54 | Settings: { 55 | Inputs: [ 56 | { 57 | AudioSelectors: { 58 | "Audio Selector 1": { DefaultSelection: "DEFAULT" }, 59 | }, 60 | VideoSelector: {}, 61 | TimecodeSource: "ZEROBASED", 62 | FileInput: inputPath, 63 | }, 64 | ], 65 | }, 66 | }) 67 | ); 68 | return rc; 69 | }; 70 | -------------------------------------------------------------------------------- /resources/rekognition.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb"; 2 | import { 3 | RekognitionClient, 4 | StartCelebrityRecognitionCommand, 5 | GetCelebrityRecognitionCommand, 6 | } from "@aws-sdk/client-rekognition"; 7 | import { EventBridgeEvent, S3Event } from "aws-lambda"; 8 | 9 | interface ISvgObj { 10 | [key: string]: string; 11 | } 12 | 13 | export const handler = async ( 14 | event: EventBridgeEvent<"Media Convertin Complete Event", any> 15 | ) => { 16 | const region = process.env["REGION"]!; 17 | 18 | const rekognition = new RekognitionClient({ region }); 19 | const dynamodb = new DynamoDBClient({ region }); 20 | 21 | // Get dynamoDB table name from environment variables 22 | const tableName = process.env["TABLE"]; 23 | 24 | console.log(`Event received from EventBridge: ${JSON.stringify(event)}`); 25 | 26 | // Get the bucket and key name from the event 27 | let objectInfo = 28 | event["detail"]["outputGroupDetails"][1]["outputDetails"][0][ 29 | "outputFilePaths" 30 | ][0].split("//"); 31 | objectInfo = objectInfo[1].split("/"); 32 | 33 | const bucket = objectInfo[0]; 34 | let key = objectInfo[1]; 35 | let i = 2; 36 | while (i < objectInfo.length) { 37 | key += "/" + objectInfo[i]; 38 | i++; 39 | } 40 | 41 | console.log(`Bucket: ${bucket}, Key: ${key}`); 42 | 43 | const celebrities = await rekognize(rekognition, bucket, key); 44 | 45 | // Store all celebrities as a primary key and the current timestamp as a sort key 46 | // and save the videoPath for as an attribute 47 | for (const celeb in celebrities) { 48 | const videoPath: string = "s3://" + bucket + "/" + key; 49 | 50 | dynamodb.send( 51 | new PutItemCommand({ 52 | TableName: tableName, 53 | Item: { 54 | PK: { S: celeb }, 55 | SK: { S: Date.now().toString() }, 56 | PATH: { S: videoPath }, 57 | }, 58 | }) 59 | ); 60 | } 61 | }; 62 | 63 | const rekognize = async ( 64 | rekog: RekognitionClient, 65 | bucket: string, 66 | key: string 67 | ) => { 68 | let theCelebs: ISvgObj = {}; 69 | 70 | const startRekogCelebRes = await rekog.send( 71 | new StartCelebrityRecognitionCommand({ 72 | Video: { 73 | S3Object: { 74 | Bucket: bucket, 75 | Name: key, 76 | }, 77 | }, 78 | }) 79 | ); 80 | 81 | // Get Job Id 82 | const jobId = startRekogCelebRes["JobId"]; 83 | console.log(`Rekognition Job Id: ${jobId}`); 84 | 85 | let getRekogCelebRes = await rekog.send( 86 | new GetCelebrityRecognitionCommand({ 87 | JobId: jobId, 88 | SortBy: "TIMESTAMP", 89 | }) 90 | ); 91 | 92 | // if Rekognition job is still in progress, sleep for 5 seconds 93 | // and try it again 94 | while (getRekogCelebRes.JobStatus == "IN_PROGRESS") { 95 | console.log("Rekognition job in progress...", getRekogCelebRes.Celebrities); 96 | await new Promise((f) => setTimeout(f, 5000)); 97 | getRekogCelebRes = await rekog.send( 98 | new GetCelebrityRecognitionCommand({ 99 | JobId: jobId, 100 | SortBy: "TIMESTAMP", 101 | }) 102 | ); 103 | } 104 | 105 | console.log(getRekogCelebRes.JobStatus); 106 | console.log(JSON.stringify(getRekogCelebRes)); 107 | 108 | for (const celebrity of getRekogCelebRes.Celebrities!) { 109 | let strDetail = ""; 110 | if (celebrity.Celebrity !== undefined) { 111 | const cconfidence = celebrity["Celebrity"]["Confidence"]; 112 | if (cconfidence !== undefined && cconfidence > 95) { 113 | const ts = celebrity.Timestamp; 114 | const name = celebrity.Celebrity.Name!; 115 | strDetail += 116 | strDetail + 117 | `At ${ts} ms: ${name} (confidence ${Math.round(cconfidence)})`; 118 | if (!(name in theCelebs)) { 119 | theCelebs[name] = name; 120 | } 121 | } 122 | } 123 | } 124 | return theCelebs; 125 | }; 126 | -------------------------------------------------------------------------------- /resources/s3upload.ts: -------------------------------------------------------------------------------- 1 | import { S3Event } from "aws-lambda"; 2 | import { SQSClient } from "@aws-sdk/client-sqs"; 3 | import { SendMessageCommand } from "@aws-sdk/client-sqs"; 4 | 5 | export const handler = async (event: S3Event) => { 6 | const region = process.env["REGION"]!; 7 | 8 | const sqs = new SQSClient({ region }); 9 | const queueURL = process.env["QUEUE_URL"]; 10 | 11 | const s3Info = event["Records"][0]["s3"]; 12 | const bucketName = s3Info["bucket"]["name"]; 13 | const bucketArn = s3Info["bucket"]["arn"]; 14 | const objectKey = s3Info["object"]["key"]; 15 | 16 | const params = { 17 | QueueUrl: queueURL, 18 | MessageAttributes: { 19 | Bucket: { DataType: "String", StringValue: bucketName }, 20 | ARN: { DataType: "String", StringValue: bucketArn }, 21 | Key: { DataType: "String", StringValue: objectKey }, 22 | }, 23 | MessageBody: "Information about uploaded video", 24 | }; 25 | 26 | const data = await sqs.send(new SendMessageCommand(params)); 27 | console.log("Success, message sent. MessageID:", data.MessageId); 28 | 29 | return { 30 | statusCode: 200, 31 | body: JSON.stringify({ 32 | message: event, 33 | }), 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /samples/medialive_configs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "abp-demo-medialive-channel", 3 | "id": "5148100", 4 | "arn": "arn:aws:medialive:ap-northeast-2:236241703319:channel:5148100", 5 | "inputAttachments": [ 6 | { 7 | "inputId": "8923431", 8 | "inputAttachmentName": "testinput-1", 9 | "inputSettings": { 10 | "sourceEndBehavior": "CONTINUE", 11 | "inputFilter": "AUTO", 12 | "filterStrength": 1, 13 | "deblockFilter": "DISABLED", 14 | "denoiseFilter": "DISABLED", 15 | "smpte2038DataPreference": "IGNORE", 16 | "audioSelectors": [], 17 | "captionSelectors": [] 18 | } 19 | } 20 | ], 21 | "state": "IDLE", 22 | "pipelinesRunningCount": 0, 23 | "destinations": [ 24 | { 25 | "id": "6wwcqg", 26 | "settings": [], 27 | "mediaPackageSettings": [ 28 | { 29 | "channelId": "abp-mp-channel" 30 | } 31 | ] 32 | }, 33 | { 34 | "id": "djdil", 35 | "settings": [ 36 | { 37 | "url": "s3ssl://abpdemovodbackendcdkstac-abpdemofrommedialivebuck-3lt79lu0aova/streaming-raw-1/" 38 | }, 39 | { 40 | "url": "s3ssl://abpdemovodbackendcdkstac-abpdemofrommedialivebuck-3lt79lu0aova/streaming-raw-2/" 41 | } 42 | ], 43 | "mediaPackageSettings": [] 44 | } 45 | ], 46 | "egressEndpoints": [ 47 | { 48 | "sourceIp": "3.35.237.156" 49 | }, 50 | { 51 | "sourceIp": "3.39.165.205" 52 | } 53 | ], 54 | "encoderSettings": { 55 | "audioDescriptions": [ 56 | { 57 | "audioSelectorName": "default", 58 | "codecSettings": { 59 | "aacSettings": { 60 | "bitrate": 64000, 61 | "rawFormat": "NONE", 62 | "spec": "MPEG4" 63 | } 64 | }, 65 | "audioTypeControl": "FOLLOW_INPUT", 66 | "languageCodeControl": "FOLLOW_INPUT", 67 | "name": "audio_1_aac64" 68 | }, 69 | { 70 | "audioSelectorName": "default", 71 | "codecSettings": { 72 | "aacSettings": { 73 | "bitrate": 64000, 74 | "rawFormat": "NONE", 75 | "spec": "MPEG4" 76 | } 77 | }, 78 | "audioTypeControl": "FOLLOW_INPUT", 79 | "languageCodeControl": "FOLLOW_INPUT", 80 | "name": "audio_2_aac64" 81 | }, 82 | { 83 | "audioSelectorName": "default", 84 | "codecSettings": { 85 | "aacSettings": { 86 | "bitrate": 64000, 87 | "rawFormat": "NONE", 88 | "spec": "MPEG4" 89 | } 90 | }, 91 | "audioTypeControl": "FOLLOW_INPUT", 92 | "languageCodeControl": "FOLLOW_INPUT", 93 | "name": "audio_3_aac64" 94 | }, 95 | { 96 | "audioSelectorName": "default", 97 | "codecSettings": { 98 | "aacSettings": { 99 | "bitrate": 96000, 100 | "rawFormat": "NONE", 101 | "spec": "MPEG4" 102 | } 103 | }, 104 | "audioTypeControl": "FOLLOW_INPUT", 105 | "languageCodeControl": "FOLLOW_INPUT", 106 | "name": "audio_1_aac96" 107 | }, 108 | { 109 | "audioSelectorName": "default", 110 | "codecSettings": { 111 | "aacSettings": { 112 | "bitrate": 96000, 113 | "rawFormat": "NONE", 114 | "spec": "MPEG4" 115 | } 116 | }, 117 | "audioTypeControl": "FOLLOW_INPUT", 118 | "languageCodeControl": "FOLLOW_INPUT", 119 | "name": "audio_2_aac96" 120 | }, 121 | { 122 | "audioSelectorName": "default", 123 | "codecSettings": { 124 | "aacSettings": { 125 | "bitrate": 96000, 126 | "rawFormat": "NONE", 127 | "spec": "MPEG4" 128 | } 129 | }, 130 | "audioTypeControl": "FOLLOW_INPUT", 131 | "languageCodeControl": "FOLLOW_INPUT", 132 | "name": "audio_3_aac96" 133 | }, 134 | { 135 | "audioSelectorName": "default", 136 | "codecSettings": { 137 | "aacSettings": { 138 | "bitrate": 128000, 139 | "rawFormat": "NONE", 140 | "spec": "MPEG4" 141 | } 142 | }, 143 | "audioTypeControl": "FOLLOW_INPUT", 144 | "languageCodeControl": "FOLLOW_INPUT", 145 | "name": "audio_1_aac128" 146 | }, 147 | { 148 | "audioSelectorName": "default", 149 | "codecSettings": { 150 | "aacSettings": { 151 | "bitrate": 128000, 152 | "rawFormat": "NONE", 153 | "spec": "MPEG4" 154 | } 155 | }, 156 | "audioTypeControl": "FOLLOW_INPUT", 157 | "languageCodeControl": "FOLLOW_INPUT", 158 | "name": "audio_2_aac128" 159 | }, 160 | { 161 | "audioSelectorName": "default", 162 | "codecSettings": { 163 | "aacSettings": { 164 | "bitrate": 128000, 165 | "rawFormat": "NONE", 166 | "spec": "MPEG4" 167 | } 168 | }, 169 | "audioTypeControl": "FOLLOW_INPUT", 170 | "languageCodeControl": "FOLLOW_INPUT", 171 | "name": "audio_3_aac128" 172 | }, 173 | { 174 | "audioTypeControl": "FOLLOW_INPUT", 175 | "languageCodeControl": "FOLLOW_INPUT", 176 | "name": "audio_uge9rm" 177 | } 178 | ], 179 | "captionDescriptions": [], 180 | "outputGroups": [ 181 | { 182 | "outputGroupSettings": { 183 | "mediaPackageGroupSettings": { 184 | "destination": { 185 | "destinationRefId": "6wwcqg" 186 | } 187 | } 188 | }, 189 | "outputs": [ 190 | { 191 | "outputSettings": { 192 | "mediaPackageOutputSettings": {} 193 | }, 194 | "outputName": "960_540", 195 | "videoDescriptionName": "video_960_540", 196 | "audioDescriptionNames": ["audio_2_aac96"], 197 | "captionDescriptionNames": [] 198 | }, 199 | { 200 | "outputSettings": { 201 | "mediaPackageOutputSettings": {} 202 | }, 203 | "outputName": "1280_720_1", 204 | "videoDescriptionName": "video_1280_720_1", 205 | "audioDescriptionNames": ["audio_3_aac96"], 206 | "captionDescriptionNames": [] 207 | }, 208 | { 209 | "outputSettings": { 210 | "mediaPackageOutputSettings": {} 211 | }, 212 | "outputName": "1280_720_2", 213 | "videoDescriptionName": "video_1280_720_2", 214 | "audioDescriptionNames": ["audio_1_aac128"], 215 | "captionDescriptionNames": [] 216 | }, 217 | { 218 | "outputSettings": { 219 | "mediaPackageOutputSettings": {} 220 | }, 221 | "outputName": "1280_720_3", 222 | "videoDescriptionName": "video_1280_720_3", 223 | "audioDescriptionNames": ["audio_2_aac128"], 224 | "captionDescriptionNames": [] 225 | }, 226 | { 227 | "outputSettings": { 228 | "mediaPackageOutputSettings": {} 229 | }, 230 | "outputName": "1920_1080", 231 | "videoDescriptionName": "video_1920_1080", 232 | "audioDescriptionNames": ["audio_3_aac128"], 233 | "captionDescriptionNames": [] 234 | }, 235 | { 236 | "outputSettings": { 237 | "mediaPackageOutputSettings": {} 238 | }, 239 | "outputName": "416_234", 240 | "videoDescriptionName": "video_416_234", 241 | "audioDescriptionNames": ["audio_1_aac64"], 242 | "captionDescriptionNames": [] 243 | }, 244 | { 245 | "outputSettings": { 246 | "mediaPackageOutputSettings": {} 247 | }, 248 | "outputName": "480_270", 249 | "videoDescriptionName": "video_480_270", 250 | "audioDescriptionNames": ["audio_2_aac64"], 251 | "captionDescriptionNames": [] 252 | }, 253 | { 254 | "outputSettings": { 255 | "mediaPackageOutputSettings": {} 256 | }, 257 | "outputName": "640_360", 258 | "videoDescriptionName": "video_640_360", 259 | "audioDescriptionNames": ["audio_3_aac64"], 260 | "captionDescriptionNames": [] 261 | }, 262 | { 263 | "outputSettings": { 264 | "mediaPackageOutputSettings": {} 265 | }, 266 | "outputName": "768_432", 267 | "videoDescriptionName": "video_768_432", 268 | "audioDescriptionNames": ["audio_1_aac96"], 269 | "captionDescriptionNames": [] 270 | } 271 | ] 272 | }, 273 | { 274 | "outputGroupSettings": { 275 | "archiveGroupSettings": { 276 | "destination": { 277 | "destinationRefId": "djdil" 278 | }, 279 | "rolloverInterval": 60 280 | } 281 | }, 282 | "name": "", 283 | "outputs": [ 284 | { 285 | "outputSettings": { 286 | "archiveOutputSettings": { 287 | "nameModifier": "$dt$", 288 | "extension": "ts", 289 | "containerSettings": { 290 | "m2tsSettings": { 291 | "ccDescriptor": "DISABLED", 292 | "ebif": "NONE", 293 | "nielsenId3Behavior": "NO_PASSTHROUGH", 294 | "programNum": 1, 295 | "patInterval": 100, 296 | "pmtInterval": 100, 297 | "pcrControl": "PCR_EVERY_PES_PACKET", 298 | "pcrPeriod": 40, 299 | "timedMetadataBehavior": "NO_PASSTHROUGH", 300 | "bufferModel": "MULTIPLEX", 301 | "rateMode": "CBR", 302 | "audioBufferModel": "ATSC", 303 | "audioStreamType": "DVB", 304 | "audioFramesPerPes": 2, 305 | "segmentationStyle": "MAINTAIN_CADENCE", 306 | "segmentationMarkers": "NONE", 307 | "ebpPlacement": "VIDEO_AND_AUDIO_PIDS", 308 | "ebpAudioInterval": "VIDEO_INTERVAL", 309 | "esRateInPes": "EXCLUDE", 310 | "arib": "DISABLED", 311 | "aribCaptionsPidControl": "AUTO", 312 | "absentInputAudioBehavior": "ENCODE_SILENCE", 313 | "pmtPid": "480", 314 | "videoPid": "481", 315 | "audioPids": "482-498", 316 | "dvbTeletextPid": "499", 317 | "dvbSubPids": "460-479", 318 | "scte27Pids": "450-459", 319 | "scte35Pid": "500", 320 | "scte35Control": "NONE", 321 | "klv": "NONE", 322 | "klvDataPids": "501", 323 | "timedMetadataPid": "502", 324 | "etvPlatformPid": "504", 325 | "etvSignalPid": "505", 326 | "aribCaptionsPid": "507" 327 | } 328 | } 329 | } 330 | }, 331 | "outputName": "ykm19e", 332 | "videoDescriptionName": "video_xu3olh", 333 | "audioDescriptionNames": ["audio_uge9rm"], 334 | "captionDescriptionNames": [] 335 | } 336 | ] 337 | } 338 | ], 339 | "timecodeConfig": { 340 | "source": "SYSTEMCLOCK" 341 | }, 342 | "videoDescriptions": [ 343 | { 344 | "codecSettings": { 345 | "h264Settings": { 346 | "colorMetadata": "INSERT", 347 | "adaptiveQuantization": "HIGH", 348 | "bitrate": 200000, 349 | "entropyEncoding": "CAVLC", 350 | "flickerAq": "ENABLED", 351 | "framerateControl": "SPECIFIED", 352 | "framerateNumerator": 15000, 353 | "framerateDenominator": 1001, 354 | "gopBReference": "DISABLED", 355 | "gopNumBFrames": 0, 356 | "gopSize": 30, 357 | "gopSizeUnits": "FRAMES", 358 | "level": "H264_LEVEL_3", 359 | "lookAheadRateControl": "HIGH", 360 | "parControl": "SPECIFIED", 361 | "profile": "BASELINE", 362 | "rateControlMode": "CBR", 363 | "syntax": "DEFAULT", 364 | "sceneChangeDetect": "ENABLED", 365 | "spatialAq": "ENABLED", 366 | "temporalAq": "ENABLED" 367 | } 368 | }, 369 | "height": 236, 370 | "name": "video_416_234", 371 | "scalingBehavior": "DEFAULT", 372 | "width": 416 373 | }, 374 | { 375 | "codecSettings": { 376 | "h264Settings": { 377 | "afdSignaling": "NONE", 378 | "colorMetadata": "INSERT", 379 | "adaptiveQuantization": "HIGH", 380 | "bitrate": 400000, 381 | "entropyEncoding": "CAVLC", 382 | "flickerAq": "ENABLED", 383 | "forceFieldPictures": "DISABLED", 384 | "framerateControl": "SPECIFIED", 385 | "framerateNumerator": 15000, 386 | "framerateDenominator": 1001, 387 | "gopBReference": "DISABLED", 388 | "gopClosedCadence": 1, 389 | "gopNumBFrames": 0, 390 | "gopSize": 30, 391 | "gopSizeUnits": "FRAMES", 392 | "subgopLength": "FIXED", 393 | "scanType": "PROGRESSIVE", 394 | "level": "H264_LEVEL_3", 395 | "lookAheadRateControl": "HIGH", 396 | "numRefFrames": 1, 397 | "parControl": "SPECIFIED", 398 | "profile": "BASELINE", 399 | "rateControlMode": "CBR", 400 | "syntax": "DEFAULT", 401 | "sceneChangeDetect": "ENABLED", 402 | "spatialAq": "ENABLED", 403 | "temporalAq": "ENABLED", 404 | "timecodeInsertion": "DISABLED" 405 | } 406 | }, 407 | "height": 272, 408 | "name": "video_480_270", 409 | "respondToAfd": "NONE", 410 | "sharpness": 50, 411 | "scalingBehavior": "DEFAULT", 412 | "width": 480 413 | }, 414 | { 415 | "codecSettings": { 416 | "h264Settings": { 417 | "colorMetadata": "INSERT", 418 | "adaptiveQuantization": "HIGH", 419 | "bitrate": 800000, 420 | "entropyEncoding": "CABAC", 421 | "flickerAq": "ENABLED", 422 | "framerateControl": "SPECIFIED", 423 | "framerateNumerator": 30000, 424 | "framerateDenominator": 1001, 425 | "gopBReference": "ENABLED", 426 | "gopNumBFrames": 3, 427 | "gopSize": 60, 428 | "gopSizeUnits": "FRAMES", 429 | "level": "H264_LEVEL_3", 430 | "lookAheadRateControl": "HIGH", 431 | "parControl": "SPECIFIED", 432 | "profile": "MAIN", 433 | "rateControlMode": "CBR", 434 | "syntax": "DEFAULT", 435 | "sceneChangeDetect": "ENABLED", 436 | "spatialAq": "ENABLED", 437 | "temporalAq": "ENABLED" 438 | } 439 | }, 440 | "height": 360, 441 | "name": "video_640_360", 442 | "scalingBehavior": "DEFAULT", 443 | "width": 640 444 | }, 445 | { 446 | "codecSettings": { 447 | "h264Settings": { 448 | "afdSignaling": "NONE", 449 | "colorMetadata": "INSERT", 450 | "adaptiveQuantization": "HIGH", 451 | "bitrate": 1200000, 452 | "entropyEncoding": "CABAC", 453 | "flickerAq": "ENABLED", 454 | "forceFieldPictures": "DISABLED", 455 | "framerateControl": "SPECIFIED", 456 | "framerateNumerator": 30000, 457 | "framerateDenominator": 1001, 458 | "gopBReference": "ENABLED", 459 | "gopClosedCadence": 1, 460 | "gopNumBFrames": 3, 461 | "gopSize": 60, 462 | "gopSizeUnits": "FRAMES", 463 | "subgopLength": "FIXED", 464 | "scanType": "PROGRESSIVE", 465 | "level": "H264_LEVEL_4_1", 466 | "lookAheadRateControl": "HIGH", 467 | "numRefFrames": 1, 468 | "parControl": "SPECIFIED", 469 | "profile": "MAIN", 470 | "rateControlMode": "CBR", 471 | "syntax": "DEFAULT", 472 | "sceneChangeDetect": "ENABLED", 473 | "spatialAq": "ENABLED", 474 | "temporalAq": "ENABLED", 475 | "timecodeInsertion": "DISABLED" 476 | } 477 | }, 478 | "height": 432, 479 | "name": "video_768_432", 480 | "respondToAfd": "NONE", 481 | "sharpness": 50, 482 | "scalingBehavior": "DEFAULT", 483 | "width": 768 484 | }, 485 | { 486 | "codecSettings": { 487 | "h264Settings": { 488 | "afdSignaling": "NONE", 489 | "colorMetadata": "INSERT", 490 | "adaptiveQuantization": "HIGH", 491 | "bitrate": 2200000, 492 | "entropyEncoding": "CABAC", 493 | "flickerAq": "ENABLED", 494 | "forceFieldPictures": "DISABLED", 495 | "framerateControl": "SPECIFIED", 496 | "framerateNumerator": 30000, 497 | "framerateDenominator": 1001, 498 | "gopBReference": "ENABLED", 499 | "gopClosedCadence": 1, 500 | "gopNumBFrames": 3, 501 | "gopSize": 60, 502 | "gopSizeUnits": "FRAMES", 503 | "subgopLength": "FIXED", 504 | "scanType": "PROGRESSIVE", 505 | "level": "H264_LEVEL_4_1", 506 | "lookAheadRateControl": "HIGH", 507 | "numRefFrames": 1, 508 | "parControl": "SPECIFIED", 509 | "profile": "HIGH", 510 | "rateControlMode": "CBR", 511 | "syntax": "DEFAULT", 512 | "sceneChangeDetect": "ENABLED", 513 | "spatialAq": "ENABLED", 514 | "temporalAq": "ENABLED", 515 | "timecodeInsertion": "DISABLED" 516 | } 517 | }, 518 | "height": 540, 519 | "name": "video_960_540", 520 | "respondToAfd": "NONE", 521 | "sharpness": 50, 522 | "scalingBehavior": "DEFAULT", 523 | "width": 960 524 | }, 525 | { 526 | "codecSettings": { 527 | "h264Settings": { 528 | "colorMetadata": "INSERT", 529 | "adaptiveQuantization": "HIGH", 530 | "bitrate": 3300000, 531 | "entropyEncoding": "CABAC", 532 | "flickerAq": "ENABLED", 533 | "framerateControl": "SPECIFIED", 534 | "framerateNumerator": 30000, 535 | "framerateDenominator": 1001, 536 | "gopBReference": "ENABLED", 537 | "gopNumBFrames": 3, 538 | "gopSize": 60, 539 | "gopSizeUnits": "FRAMES", 540 | "level": "H264_LEVEL_4_1", 541 | "lookAheadRateControl": "HIGH", 542 | "parControl": "SPECIFIED", 543 | "profile": "HIGH", 544 | "rateControlMode": "CBR", 545 | "syntax": "DEFAULT", 546 | "sceneChangeDetect": "ENABLED", 547 | "spatialAq": "ENABLED", 548 | "temporalAq": "ENABLED" 549 | } 550 | }, 551 | "height": 720, 552 | "name": "video_1280_720_1", 553 | "scalingBehavior": "DEFAULT", 554 | "width": 1280 555 | }, 556 | { 557 | "codecSettings": { 558 | "h264Settings": { 559 | "afdSignaling": "NONE", 560 | "colorMetadata": "INSERT", 561 | "adaptiveQuantization": "HIGH", 562 | "bitrate": 4700000, 563 | "entropyEncoding": "CABAC", 564 | "flickerAq": "ENABLED", 565 | "forceFieldPictures": "DISABLED", 566 | "framerateControl": "SPECIFIED", 567 | "framerateNumerator": 30000, 568 | "framerateDenominator": 1001, 569 | "gopBReference": "ENABLED", 570 | "gopClosedCadence": 1, 571 | "gopNumBFrames": 3, 572 | "gopSize": 60, 573 | "gopSizeUnits": "FRAMES", 574 | "subgopLength": "FIXED", 575 | "scanType": "PROGRESSIVE", 576 | "level": "H264_LEVEL_4_1", 577 | "lookAheadRateControl": "HIGH", 578 | "numRefFrames": 1, 579 | "parControl": "SPECIFIED", 580 | "profile": "HIGH", 581 | "rateControlMode": "CBR", 582 | "syntax": "DEFAULT", 583 | "sceneChangeDetect": "ENABLED", 584 | "spatialAq": "ENABLED", 585 | "temporalAq": "ENABLED", 586 | "timecodeInsertion": "DISABLED" 587 | } 588 | }, 589 | "height": 720, 590 | "name": "video_1280_720_2", 591 | "respondToAfd": "NONE", 592 | "sharpness": 50, 593 | "scalingBehavior": "DEFAULT", 594 | "width": 1280 595 | }, 596 | { 597 | "codecSettings": { 598 | "h264Settings": { 599 | "colorMetadata": "INSERT", 600 | "adaptiveQuantization": "HIGH", 601 | "bitrate": 6200000, 602 | "entropyEncoding": "CABAC", 603 | "flickerAq": "ENABLED", 604 | "framerateControl": "SPECIFIED", 605 | "framerateNumerator": 30000, 606 | "framerateDenominator": 1001, 607 | "gopBReference": "ENABLED", 608 | "gopNumBFrames": 3, 609 | "gopSize": 60, 610 | "gopSizeUnits": "FRAMES", 611 | "level": "H264_LEVEL_4_1", 612 | "lookAheadRateControl": "HIGH", 613 | "parControl": "SPECIFIED", 614 | "profile": "HIGH", 615 | "rateControlMode": "CBR", 616 | "syntax": "DEFAULT", 617 | "sceneChangeDetect": "ENABLED", 618 | "spatialAq": "ENABLED", 619 | "temporalAq": "ENABLED" 620 | } 621 | }, 622 | "height": 720, 623 | "name": "video_1280_720_3", 624 | "scalingBehavior": "DEFAULT", 625 | "width": 1280 626 | }, 627 | { 628 | "codecSettings": { 629 | "h264Settings": { 630 | "colorMetadata": "INSERT", 631 | "adaptiveQuantization": "HIGH", 632 | "bitrate": 8000000, 633 | "entropyEncoding": "CABAC", 634 | "flickerAq": "ENABLED", 635 | "framerateControl": "SPECIFIED", 636 | "framerateNumerator": 30000, 637 | "framerateDenominator": 1001, 638 | "gopBReference": "DISABLED", 639 | "gopNumBFrames": 1, 640 | "gopSize": 60, 641 | "gopSizeUnits": "FRAMES", 642 | "level": "H264_LEVEL_4_1", 643 | "lookAheadRateControl": "HIGH", 644 | "parControl": "SPECIFIED", 645 | "profile": "HIGH", 646 | "rateControlMode": "CBR", 647 | "syntax": "DEFAULT", 648 | "sceneChangeDetect": "ENABLED", 649 | "spatialAq": "ENABLED", 650 | "temporalAq": "ENABLED" 651 | } 652 | }, 653 | "height": 1080, 654 | "name": "video_1920_1080", 655 | "scalingBehavior": "DEFAULT", 656 | "width": 1920 657 | }, 658 | { 659 | "codecSettings": { 660 | "h264Settings": { 661 | "afdSignaling": "NONE", 662 | "colorMetadata": "INSERT", 663 | "adaptiveQuantization": "AUTO", 664 | "entropyEncoding": "CABAC", 665 | "flickerAq": "ENABLED", 666 | "forceFieldPictures": "DISABLED", 667 | "framerateControl": "INITIALIZE_FROM_SOURCE", 668 | "gopBReference": "DISABLED", 669 | "gopClosedCadence": 1, 670 | "gopSize": 90, 671 | "gopSizeUnits": "FRAMES", 672 | "subgopLength": "FIXED", 673 | "scanType": "PROGRESSIVE", 674 | "level": "H264_LEVEL_AUTO", 675 | "lookAheadRateControl": "MEDIUM", 676 | "numRefFrames": 1, 677 | "parControl": "INITIALIZE_FROM_SOURCE", 678 | "profile": "MAIN", 679 | "rateControlMode": "CBR", 680 | "syntax": "DEFAULT", 681 | "sceneChangeDetect": "ENABLED", 682 | "spatialAq": "ENABLED", 683 | "temporalAq": "ENABLED", 684 | "timecodeInsertion": "DISABLED" 685 | } 686 | }, 687 | "name": "video_xu3olh", 688 | "respondToAfd": "NONE", 689 | "sharpness": 50, 690 | "scalingBehavior": "DEFAULT" 691 | } 692 | ] 693 | }, 694 | "roleArn": "arn:aws:iam::236241703319:role/MediaLiveAccessRole", 695 | "inputSpecification": { 696 | "codec": "AVC", 697 | "resolution": "HD", 698 | "maximumBitrate": "MAX_20_MBPS" 699 | }, 700 | "logLevel": "DISABLED", 701 | "tags": {}, 702 | "channelClass": "STANDARD", 703 | "pipelineDetails": [], 704 | "maintenanceWindow": "WEDNESDAY_1800", 705 | "maintenanceStatus": "", 706 | "maintenance": { 707 | "maintenanceDay": "WEDNESDAY", 708 | "maintenanceStartTime": "18:00" 709 | } 710 | } 711 | -------------------------------------------------------------------------------- /samples/rekognition-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0", 3 | "id": "46290e0f-95a0-c44f-075f-3b329de67a06", 4 | "detail-type": "MediaConvert Job State Change", 5 | "source": "aws.mediaconvert", 6 | "account": "236241703319", 7 | "time": "2023-01-04T14:57:02Z", 8 | "region": "ap-northeast-2", 9 | "resources": ["arn:aws:mediaconvert:ap-northeast-2:236241703319:jobs/1672844148399-sem5yu"], 10 | "detail": { 11 | "timestamp": 1672844222805, 12 | "accountId": "236241703319", 13 | "queue": "arn:aws:mediaconvert:ap-northeast-2:236241703319:queues/Default", 14 | "jobId": "1672844148399-sem5yu", 15 | "status": "COMPLETE", 16 | "userMetadata": {}, 17 | "outputGroupDetails": [ 18 | { 19 | "outputDetails": [ 20 | { 21 | "outputFilePaths": [ 22 | "s3://eventreplayengine-mediaconvertbucket0ffa08e9-8cmng59ampbw/vod-assets/20230104T145231.000000_Ott_Hls_Ts_Avc_Aac_16x9_480x270p_15Hz_400Kbps.m3u8" 23 | ], 24 | "durationInMs": 60060, 25 | "videoDetails": { 26 | "widthInPx": 480, 27 | "heightInPx": 270 28 | } 29 | }, 30 | { 31 | "outputFilePaths": [ 32 | "s3://eventreplayengine-mediaconvertbucket0ffa08e9-8cmng59ampbw/vod-assets/20230104T145231.000000_Ott_Hls_Ts_Avc_Aac_16x9_640x360p_30Hz_600Kbps.m3u8" 33 | ], 34 | "durationInMs": 60026, 35 | "videoDetails": { 36 | "widthInPx": 640, 37 | "heightInPx": 360 38 | } 39 | }, 40 | { 41 | "outputFilePaths": [ 42 | "s3://eventreplayengine-mediaconvertbucket0ffa08e9-8cmng59ampbw/vod-assets/20230104T145231.000000_Ott_Hls_Ts_Avc_Aac_16x9_640x360p_30Hz_1200Kbps.m3u8" 43 | ], 44 | "durationInMs": 60026, 45 | "videoDetails": { 46 | "widthInPx": 640, 47 | "heightInPx": 360 48 | } 49 | }, 50 | { 51 | "outputFilePaths": [ 52 | "s3://eventreplayengine-mediaconvertbucket0ffa08e9-8cmng59ampbw/vod-assets/20230104T145231.000000_Ott_Hls_Ts_Avc_Aac_16x9_960x540p_30Hz_3500Kbps.m3u8" 53 | ], 54 | "durationInMs": 60026, 55 | "videoDetails": { 56 | "widthInPx": 960, 57 | "heightInPx": 540 58 | } 59 | }, 60 | { 61 | "outputFilePaths": [ 62 | "s3://eventreplayengine-mediaconvertbucket0ffa08e9-8cmng59ampbw/vod-assets/20230104T145231.000000_Ott_Hls_Ts_Avc_Aac_16x9_1280x720p_30Hz_3500Kbps.m3u8" 63 | ], 64 | "durationInMs": 60026, 65 | "videoDetails": { 66 | "widthInPx": 1280, 67 | "heightInPx": 720 68 | } 69 | }, 70 | { 71 | "outputFilePaths": [ 72 | "s3://eventreplayengine-mediaconvertbucket0ffa08e9-8cmng59ampbw/vod-assets/20230104T145231.000000_Ott_Hls_Ts_Avc_Aac_16x9_1280x720p_30Hz_5000Kbps.m3u8" 73 | ], 74 | "durationInMs": 60026, 75 | "videoDetails": { 76 | "widthInPx": 1280, 77 | "heightInPx": 720 78 | } 79 | }, 80 | { 81 | "outputFilePaths": [ 82 | "s3://eventreplayengine-mediaconvertbucket0ffa08e9-8cmng59ampbw/vod-assets/20230104T145231.000000_Ott_Hls_Ts_Avc_Aac_16x9_1280x720p_30Hz_6500Kbps.m3u8" 83 | ], 84 | "durationInMs": 60026, 85 | "videoDetails": { 86 | "widthInPx": 1280, 87 | "heightInPx": 720 88 | } 89 | }, 90 | { 91 | "outputFilePaths": [ 92 | "s3://eventreplayengine-mediaconvertbucket0ffa08e9-8cmng59ampbw/vod-assets/20230104T145231.000000_Ott_Hls_Ts_Avc_Aac_16x9_1920x1080p_30Hz_8500Kbps.m3u8" 93 | ], 94 | "durationInMs": 60026, 95 | "videoDetails": { 96 | "widthInPx": 1920, 97 | "heightInPx": 1080 98 | } 99 | } 100 | ], 101 | "playlistFilePaths": [ 102 | "s3://eventreplayengine-mediaconvertbucket0ffa08e9-8cmng59ampbw/vod-assets/20230104T145231.000000.m3u8" 103 | ], 104 | "type": "HLS_GROUP" 105 | }, 106 | { 107 | "outputDetails": [ 108 | { 109 | "outputFilePaths": [ 110 | "s3://eventreplayengine-rekognitionbucketb1399c89-6thjer0hfba0/rekog-data/20230104T145231.000000.mp4" 111 | ], 112 | "durationInMs": 60000, 113 | "videoDetails": { 114 | "widthInPx": 1280, 115 | "heightInPx": 720 116 | } 117 | } 118 | ], 119 | "type": "FILE_GROUP" 120 | } 121 | ] 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /samples/s3-upload-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "eventVersion": "2.1", 5 | "eventSource": "aws:s3", 6 | "awsRegion": "ap-northeast-2", 7 | "eventTime": "2022-06-08T13:50:55.961Z", 8 | "eventName": "ObjectCreated:CompleteMultipartUpload", 9 | "userIdentity": { 10 | "principalId": "AWS:AROATOAI2KGLQ7Q2MTR4I:medialive_ap-northeast-2_236241703319_5148100-0" 11 | }, 12 | "requestParameters": { 13 | "sourceIPAddress": "10.10.3.228" 14 | }, 15 | "responseElements": { 16 | "x-amz-request-id": "0B8Y4NMZS82BV1R8", 17 | "x-amz-id-2": "ce/41pHMqiEdo2ASSL6opTIDrdwzDCj7OmVaTpOan9KqnlywJO9L2BVEB137Rik1XMlBnP8z5IyJjnYbGAHezLNP39/ZPr7Y" 18 | }, 19 | "s3": { 20 | "s3SchemaVersion": "1.0", 21 | "configurationId": "NTk0ZjMyOWQtZTExZi00ODZjLWE5Y2UtMDRmMTllMWU0NWUw", 22 | "bucket": { 23 | "name": "eventreplayengine-myeventreplayenginebucket041b52-idumtg66h5uk", 24 | "ownerIdentity": { 25 | "principalId": "A1Z5P64ETLCEXT" 26 | }, 27 | "arn": "arn:aws:s3:::eventreplayengine-myeventreplayenginebucket041b52-idumtg66h5uk" 28 | }, 29 | "object": { 30 | "key": "medialiveArchive/20221217T093335.000004.ts", 31 | "size": 44932000, 32 | "eTag": "9d47bec926ff571a8d29e52cec1bf087-3", 33 | "sequencer": "0062A0A920A943B1E1" 34 | } 35 | } 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Media Replay Engine 8 | 9 | 10 |

    Media Replay Engine

    11 | 12 | 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["node_modules", "cdk.out"] 23 | } 24 | --------------------------------------------------------------------------------