├── .eslintignore ├── .eslintrc.json ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── usage-question.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── jest.yml │ ├── nodejs.yml │ ├── publish.yml │ └── version.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── __mocks__ ├── .gitkeep ├── inquirer.js ├── package.json └── project-config.json ├── amplify-plugin.json ├── commands ├── video.js └── video │ ├── add.js │ ├── build.js │ ├── get-info.js │ ├── push.js │ ├── remove.js │ ├── setup-obs.js │ ├── setup-video-player.js │ ├── start.js │ ├── stop.js │ ├── update.js │ └── version.js ├── index.js ├── jest.config.js ├── package-lock.json ├── package.json ├── provider-utils ├── awscloudformation │ ├── cloudformation-templates │ │ ├── ivs-helpers │ │ │ └── IVS-Channel.template │ │ ├── ivs-workflow-template.yaml.ejs │ │ ├── livestream-helpers │ │ │ ├── LambdaFunctions │ │ │ │ └── psdemo-js-live-workflow_v0.4.0 │ │ │ │ │ ├── lib │ │ │ │ │ ├── babelfish.js │ │ │ │ │ ├── cfResponse.js │ │ │ │ │ ├── distribution.js │ │ │ │ │ ├── flagfish.js │ │ │ │ │ ├── jellyfish.js │ │ │ │ │ └── mxStoreResponse.js │ │ │ │ │ ├── orchestration.js │ │ │ │ │ └── resources │ │ │ │ │ ├── cloudfront │ │ │ │ │ ├── cacheBehavior.template.json │ │ │ │ │ ├── distributionConfig.template.json │ │ │ │ │ └── origin.template.json │ │ │ │ │ ├── full.json │ │ │ │ │ ├── hd.json │ │ │ │ │ ├── mobile.json │ │ │ │ │ ├── sd.json │ │ │ │ │ └── uhd.json │ │ │ ├── cloudfront-distribution.template │ │ │ ├── lambda.template │ │ │ ├── medialive-channel.template │ │ │ ├── medialive-iam.template │ │ │ ├── medialive.template │ │ │ ├── mediapackage-channel.template │ │ │ ├── mediapackage-iam.template │ │ │ ├── mediapackage.template │ │ │ ├── mediastore-container.template │ │ │ ├── mediastore-iam.template │ │ │ └── mediastore.template │ │ ├── livestream-workflow-template.json.ejs │ │ ├── vod-helpers │ │ │ ├── CFDistribution.template.ejs │ │ │ ├── CFTokenGen.template │ │ │ ├── CreateJobTemplate.template.ejs │ │ │ ├── InputTriggerLambda.template │ │ │ ├── LambdaFunctions │ │ │ │ ├── CloudFrontTokenGen │ │ │ │ │ └── index.js │ │ │ │ ├── InputLambda │ │ │ │ │ ├── dash_settings.json │ │ │ │ │ ├── hls_dash_settings.json │ │ │ │ │ ├── hls_settings.json │ │ │ │ │ ├── index.js │ │ │ │ │ └── settings.json │ │ │ │ ├── MediaConvertStatusLambda │ │ │ │ │ └── index.js │ │ │ │ ├── OutputLambda │ │ │ │ │ └── index.js.ejs │ │ │ │ └── SetupTriggerLambda │ │ │ │ │ └── index.js │ │ │ ├── OutputTriggerLambda.template │ │ │ ├── S3InputBucket.template.ejs │ │ │ ├── S3OutputBucket.template │ │ │ ├── S3TriggerSetup.template │ │ │ └── SnsSetup.template.ejs │ │ └── vod-workflow-template.yaml.ejs │ ├── default-values │ │ ├── livestream-defaults.json │ │ └── vod-defaults.json │ ├── index.js │ ├── obs-templates │ │ ├── basic.ini │ │ ├── hd-ivs.ini │ │ └── sd-ivs.ini │ ├── schemas │ │ └── schema.graphql.ejs │ ├── service-walkthroughs │ │ ├── ivs-push.js │ │ ├── livestream-push.js │ │ ├── vod-push.js │ │ └── vod-roles.js │ ├── templates │ │ ├── Amplify_Video_DASH.json │ │ ├── Amplify_Video_HLS.json │ │ ├── Amplify_Video_HLS_DASH.json │ │ ├── Amplify_Video_System_Accelerated_Ott_Hls_Ts_Avc_Aac.json │ │ └── Amplify_Video_System_Ott_Hls_Ts_Avc_Aac.json │ ├── utils │ │ ├── exports-templates │ │ │ └── aws-video-exports.js.ejs │ │ ├── get-aws.js │ │ ├── headless-mode.js │ │ ├── livestream-obs.js │ │ ├── livestream-startstop.js │ │ ├── video-getinfo.js │ │ ├── video-player-utils.js │ │ ├── video-player.js │ │ └── video-staging.js │ └── video-player-templates │ │ ├── android │ │ ├── activity_video_player.xml │ │ └── video-player.ejs │ │ ├── ios │ │ ├── bridging-header.h.ejs │ │ ├── empty.cpp.ejs │ │ ├── empty.hpp.ejs │ │ ├── ios-video-component.ejs │ │ └── video-player.ejs │ │ └── web │ │ ├── angular-video-component.ejs │ │ ├── ember-video-component.ejs │ │ ├── none-video-component.ejs │ │ ├── react-video-component.ejs │ │ ├── video-player.component.scss │ │ ├── video-player.ejs │ │ └── vue-video-component.ejs ├── ivs-questions.json ├── livestream-questions.json ├── supported-services.json └── vod-questions.json ├── scripts ├── headless │ ├── add-ivs.sh │ ├── add-vod.sh │ ├── amplify-delete.sh │ ├── amplify-push.sh │ └── init-new-project.sh ├── post-install.js ├── setup.js └── teardown.js └── tests ├── .gitkeep ├── integration ├── cloudformation.test.js └── vod.test.js ├── service-questions.test.js └── video-player.test.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tests 3 | src/ 4 | __mocks__/ 5 | LambdaFunctions/ 6 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["jest"], 3 | "extends": ["airbnb-base", "plugin:jest/recommended"], 4 | "rules": { 5 | "no-param-reassign": "off", 6 | "no-return-await" : "off", 7 | "no-continue": "off", 8 | "no-await-in-loop": "off", 9 | "no-use-before-define": "off", 10 | "no-console": "off", 11 | "global-require": "off", 12 | "import/no-dynamic-require": "off", 13 | "consistent-return": "off", 14 | "no-plusplus": "off" 15 | }, 16 | "env": { 17 | "jest/globals": true 18 | } 19 | } -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/usage-question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Usage Question 3 | about: Ask a question about AWS Amplify CLI usage 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | >**Note**: If your question is regarding the AWS Amplify Console service, please log it in the 11 | [official AWS Amplify Console forum](https://forums.aws.amazon.com/forum.jspa?forumID=314&start=0) 12 | 13 | ** Which Category is your question related to? ** 14 | 15 | ** What AWS Services are you utilizing? ** 16 | 17 | ** Provide additional details e.g. code snippets ** 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. -------------------------------------------------------------------------------- /.github/workflows/jest.yml: -------------------------------------------------------------------------------- 1 | name: Test On Pull 2 | 3 | on: 4 | push: 5 | branches: 6 | - release/** 7 | 8 | jobs: 9 | jest: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | node-version: [14.x] 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | # - name: Install AWS CLI 21 | # run : | 22 | # curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" 23 | # unzip awscliv2.zip 24 | # ./aws/install --update 25 | - name: Configure AWS credentials from Test account 26 | uses: aws-actions/configure-aws-credentials@v1 27 | with: 28 | aws-access-key-id: ${{ secrets.TEST_AWS_ACCESS_KEY_ID }} 29 | aws-secret-access-key: ${{ secrets.TEST_AWS_SECRET_ACCESS_KEY }} 30 | aws-region: us-west-2 31 | - name: Add profile credentials to ~/.aws/credentials 32 | env: 33 | AWS_ACCESS_KEY_ID: ${{ secrets.TEST_AWS_ACCESS_KEY_ID }} 34 | AWS_SECRET_ACCESS_KEY: ${{ secrets.TEST_AWS_SECRET_ACCESS_KEY }} 35 | run: | 36 | aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID --profile default 37 | aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY --profile default 38 | aws configure set region us-west-2 --profile default 39 | - name: Install amplify-cli 40 | run : npm install -g @aws-amplify/cli 41 | - name: Install amplify-video 42 | run : npm install -g 43 | - name: Install dependencies 44 | run : npm install 45 | - name: Run Jest 46 | run : npm test 47 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Lint On Pull 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [12.x, 14.x] 11 | steps: 12 | - uses: actions/checkout@v1 13 | - name: Use Node.js ${{ matrix.node-version }} 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | - name: Install eslint 18 | run : npx install-peerdeps --dev eslint-config-airbnb 19 | - name: eslint 20 | run : npm run lint 21 | - name: build 22 | run : npm install 23 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Version Pull Request 2 | on: 3 | push: 4 | branches: 5 | - master 6 | tags: 7 | - 'v*' 8 | jobs: 9 | publishPackage: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: '12.x' 16 | registry-url: 'https://registry.npmjs.org' 17 | - run: npm install 18 | - run: npm publish 19 | env: 20 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/version.yml: -------------------------------------------------------------------------------- 1 | name: Version Pull Request 2 | on: 3 | push: 4 | branches: 5 | - release/** 6 | paths: 7 | - 'package.json' 8 | jobs: 9 | createPullRequest: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Create Pull Request 14 | uses: thomaseizinger/create-pull-request@1.0.0 15 | with: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | title: '[Version Release] New version Release' 18 | body: > 19 | This PR is auto-generated. 20 | reviewers: wizage 21 | head: ${{ github.ref }} 22 | base: master -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | node_modules 3 | scripts/flow/*/.flowconfig 4 | *~ 5 | *.pyc 6 | .grunt 7 | _SpecRunner.html 8 | __benchmarks__ 9 | build/ 10 | remote-repo/ 11 | coverage/ 12 | .module-cache 13 | fixtures/dom/public/react-dom.js 14 | fixtures/dom/public/react.js 15 | test/the-files-to-test.generated.js 16 | *.log* 17 | chrome-user-data 18 | *.sublime-project 19 | *.sublime-workspace 20 | .idea 21 | *.iml 22 | .vscode 23 | *.swp 24 | *.swo 25 | *._* 26 | src/ 27 | amplify/ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Amplify Video 2 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amplify Video Plugin 2 |

3 | 4 | 5 | 6 |

7 | 8 | An open source plugin for the Amplify CLI that makes it easy to incorporate video streaming into your mobile and web applications powered by [AWS Amplify](https://aws-amplify.github.io/) and [AWS Media Services](https://aws.amazon.com/media-services/) 9 | 10 | Read more about Amplify Video on the [AWS Media Blog](https://aws.amazon.com/blogs/media/introducing_aws_amplify_video/) 11 | 12 | ## Installation 13 | 14 | Amplify Video is a [Category Plugin for AWS Amplify](https://aws-amplify.github.io/docs/cli-toolchain/plugins?sdk=js) that provides video streaming resources to your Amplify project. It requires that you have the Amplify CLI installed on your system before installing the Amplify Video plugin 15 | 16 | To get started install the Amplify CLI via NPM as shown below or follow the [getting started guide](https://github.com/aws-amplify/amplify-cli/). 17 | 18 | 19 | ``` 20 | npm install -g @aws-amplify/cli 21 | amplify configure 22 | ``` 23 | 24 | With the Amplify CLI installed, install this plugin: 25 | 26 | ``` 27 | npm i amplify-category-video -g 28 | ``` 29 | 30 | Add a video resource to your Amplify project 31 | 32 | ``` 33 | amplify video add 34 | ``` 35 | 36 | ## Getting Started with Amplify Video 37 | 38 | * [Documentation](https://github.com/awslabs/amplify-video/wiki) 39 | * [Commands](https://github.com/awslabs/amplify-video/wiki/CLI-Reference) 40 | * [Getting Started with VOD](https://github.com/awslabs/amplify-video/wiki/Getting-Started-with-VOD) 41 | * [Getting Started with Live](https://github.com/awslabs/amplify-video/wiki/Getting-Started-with-Live) 42 | 43 | ## Tutorials 44 | 45 | * [UnicornFlix](https://github.com/awslabs/unicornflix) 46 | * [UnicornTrivia](https://github.com/awslabs/aws-amplify-unicorntrivia-workshop) 47 | * [CodingCatDev Video Tutorial](https://www.youtube.com/watch?v=vM_YoZbLQQ0) 48 | 49 | ## Contributions 50 | 51 | Interested in helping us with this project? Please see the [contribution guide](CONTRIBUTING.md). 52 | 53 | ## License 54 | 55 | This library is licensed under the Apache 2.0 License. 56 | -------------------------------------------------------------------------------- /__mocks__/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/amplify-video/1f11013a9473f0750940cc05c8b4154f8e6643b7/__mocks__/.gitkeep -------------------------------------------------------------------------------- /__mocks__/inquirer.js: -------------------------------------------------------------------------------- 1 | const inquirer = jest.genMockFromModule('inquirer'); 2 | 3 | function prompt(obj) { 4 | return obj; 5 | } 6 | 7 | inquirer.prompt = prompt; 8 | 9 | module.exports = inquirer; 10 | -------------------------------------------------------------------------------- /__mocks__/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "video.js": "7.11.4" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /__mocks__/project-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "amplifyVideoProject", 3 | "version": "3.1", 4 | "frontend": "javascript", 5 | "javascript": { 6 | "framework": "react", 7 | "config": { 8 | "SourceDir": "src", 9 | "DistributionDir": "dist", 10 | "BuildCommand": "npm run-script build", 11 | "StartCommand": "npm run-script start" 12 | } 13 | }, 14 | "providers": ["awscloudformation"] 15 | } 16 | -------------------------------------------------------------------------------- /amplify-plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video", 3 | "type": "category", 4 | "commands": [ 5 | "add", 6 | "build", 7 | "get-info", 8 | "help", 9 | "push", 10 | "remove", 11 | "setup-obs", 12 | "setup-video-player", 13 | "start", 14 | "stop", 15 | "update", 16 | "version" 17 | ], 18 | "eventHandlers":[ 19 | "PrePush" 20 | ] 21 | } -------------------------------------------------------------------------------- /commands/video.js: -------------------------------------------------------------------------------- 1 | const featureName = 'video'; 2 | 3 | module.exports = { 4 | name: featureName, 5 | run: async (context) => { 6 | if (/^win/.test(process.platform)) { 7 | try { 8 | const { run } = require(`./${featureName}/${context.parameters.first}`); 9 | return run(context); 10 | } catch (e) { 11 | context.print.error('Command not found'); 12 | } 13 | } 14 | const header = `amplify ${featureName} `; 15 | 16 | const commands = [ 17 | { 18 | name: 'add', 19 | description: `Takes you through a CLI flow to add a ${featureName} resource to your local backend`, 20 | }, 21 | { 22 | name: 'get-info', 23 | description: `Gets info for ${featureName} resource from the CloudFormation template`, 24 | }, 25 | { 26 | name: 'push', 27 | description: `Provisions ${featureName} cloud resources and it's dependencies with the latest local developments`, 28 | }, 29 | { 30 | name: 'remove', 31 | description: `Removes ${featureName} resource from your local backend and will remove them on amplify push`, 32 | }, 33 | { 34 | name: 'setup-obs', 35 | description: 'Sets up OBS with your stream settings.', 36 | }, 37 | { 38 | name: 'setup-video-player', 39 | description: 'Sets up a player with your settings.', 40 | }, 41 | { 42 | name: 'start', 43 | description: `Starts your ${featureName} stream from an idle state`, 44 | }, 45 | { 46 | name: 'stop', 47 | description: `Puts your ${featureName} stream into an idle state`, 48 | }, 49 | { 50 | name: 'update', 51 | description: `Takes you through a CLI flow to update a ${featureName} resource`, 52 | }, 53 | { 54 | name: 'version', 55 | description: 'Prints the version of Amplify Video that you are using', 56 | }, 57 | ]; 58 | 59 | context.amplify.showHelp(header, commands); 60 | 61 | context.print.info(''); 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /commands/video/add.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const serviceMetadata = JSON.parse(fs.readFileSync(`${__dirname}/../../provider-utils/supported-services.json`)); 4 | const subcommand = 'add'; 5 | const category = 'video'; 6 | 7 | let options; 8 | 9 | module.exports = { 10 | name: subcommand, 11 | run: async (context) => { 12 | const { amplify } = context; 13 | 14 | // Headless mode 15 | if (context.parameters.options.payload) { 16 | const args = JSON.parse(context.parameters.options.payload); 17 | options = { 18 | service: args.service, 19 | serviceType: args.serviceType, 20 | providerPlugin: args.providerName, 21 | }; 22 | const providerController = require(`../../provider-utils/${options.providerPlugin}/index`); 23 | if (!providerController) { 24 | context.print.error('Provider not configured for this category'); 25 | return; 26 | } 27 | return providerController.addResource(context, options.serviceType, options); 28 | } 29 | 30 | // Normal mode 31 | return amplify.serviceSelectionPrompt(context, category, serviceMetadata).then((results) => { 32 | options = { 33 | service: category, 34 | serviceType: results.service, 35 | providerPlugin: results.providerName, 36 | }; 37 | const providerController = require(`../../provider-utils/${results.providerName}/index`); 38 | if (!providerController) { 39 | context.print.error('Provider not configured for this category'); 40 | return; 41 | } 42 | return providerController.addResource(context, results.service, options); 43 | }); 44 | }, 45 | 46 | }; 47 | -------------------------------------------------------------------------------- /commands/video/build.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer'); 2 | const fs = require('fs'); 3 | 4 | const subcommand = 'build'; 5 | const category = 'video'; 6 | 7 | module.exports = { 8 | name: subcommand, 9 | run: async (context) => { 10 | const { amplify } = context; 11 | const amplifyMeta = amplify.getProjectMeta(); 12 | const targetDir = amplify.pathManager.getBackendDirPath(); 13 | 14 | if (!(category in amplifyMeta) || Object.keys(amplifyMeta[category]).length === 0) { 15 | context.print.error(`You have no ${category} projects.`); 16 | return; 17 | } 18 | 19 | const chooseProject = [ 20 | { 21 | type: 'list', 22 | name: 'resourceName', 23 | message: 'Choose what project you want to build?', 24 | choices: Object.keys(amplifyMeta[category]), 25 | default: Object.keys(amplifyMeta[category])[0], 26 | }, 27 | ]; 28 | const shared = await inquirer.prompt(chooseProject); 29 | 30 | const options = amplifyMeta.video[shared.resourceName]; 31 | 32 | const { buildTemplates } = require(`../../provider-utils/${options.providerPlugin}/utils/video-staging`); 33 | if (!buildTemplates) { 34 | context.print.error('No builder is configured for this provider'); 35 | return; 36 | } 37 | 38 | const props = JSON.parse(fs.readFileSync(`${targetDir}/video/${shared.resourceName}/props.json`)); 39 | 40 | return buildTemplates(context, props); 41 | }, 42 | 43 | }; 44 | -------------------------------------------------------------------------------- /commands/video/get-info.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer'); 2 | 3 | const subcommand = 'get-info'; 4 | const category = 'video'; 5 | 6 | module.exports = { 7 | name: subcommand, 8 | run: async (context) => { 9 | const { amplify } = context; 10 | const amplifyMeta = amplify.getProjectMeta(); 11 | 12 | if (!(category in amplifyMeta) || Object.keys(amplifyMeta[category]).length === 0) { 13 | context.print.error(`You have no ${category} projects.`); 14 | return; 15 | } 16 | 17 | const chooseProject = [ 18 | { 19 | type: 'list', 20 | name: 'resourceName', 21 | message: 'Choose what project you want to get info for?', 22 | choices: Object.keys(amplifyMeta[category]), 23 | default: Object.keys(amplifyMeta[category])[0], 24 | }, 25 | ]; 26 | 27 | let props; 28 | if (context.parameters.options.default) { 29 | if (typeof context.parameters.options.default === 'boolean') { 30 | props = { resourceName: chooseProject[0].default }; 31 | } else { 32 | props = { resourceName: context.parameters.options.default }; 33 | } 34 | console.log(props); 35 | } else { 36 | props = await inquirer.prompt(chooseProject); 37 | } 38 | 39 | const options = amplifyMeta.video[props.resourceName]; 40 | 41 | const infoController = require(`../../provider-utils/${options.providerPlugin}/utils/video-getinfo`); 42 | if (!infoController) { 43 | context.print.error('Info controller not configured for this category'); 44 | return; 45 | } 46 | 47 | return infoController.getVideoInfo(context, props.resourceName); 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /commands/video/push.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer'); 2 | 3 | const subcommand = 'push'; 4 | const category = 'video'; 5 | 6 | module.exports = { 7 | name: subcommand, 8 | run: async (context) => { 9 | const { amplify } = context; 10 | 11 | const chooseProject = [ 12 | { 13 | type: 'list', 14 | name: 'resourceName', 15 | message: 'Choose what project you want to update?', 16 | choices: Object.keys(context.amplify.getProjectMeta()[category]), 17 | default: Object.keys(context.amplify.getProjectMeta()[category])[0], 18 | }, 19 | ]; 20 | 21 | const answer = await inquirer.prompt(chooseProject); 22 | amplify.constructExeInfo(context); 23 | return amplify.pushResources(context, category, answer.resourceName) 24 | .catch((err) => { 25 | context.print.info(err.stack); 26 | context.print.error('There was an error pushing the video resource'); 27 | }); 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /commands/video/remove.js: -------------------------------------------------------------------------------- 1 | const subcommand = 'remove'; 2 | const category = 'video'; 3 | 4 | module.exports = { 5 | name: subcommand, 6 | run: async (context) => { 7 | await context.amplify.removeResource(context, category); 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /commands/video/setup-obs.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer'); 2 | 3 | const subcommand = 'setup-obs'; 4 | const category = 'video'; 5 | 6 | module.exports = { 7 | name: subcommand, 8 | run: async (context) => { 9 | const { amplify } = context; 10 | const amplifyMeta = amplify.getProjectMeta(); 11 | 12 | if (!(category in amplifyMeta) || Object.keys(amplifyMeta[category]).length === 0) { 13 | context.print.error(`You have no ${category} projects.`); 14 | return; 15 | } 16 | 17 | const filteredProjects = Object.keys(amplifyMeta[category]).filter((project) => ( 18 | amplifyMeta[category][project].serviceType === 'livestream' || amplifyMeta[category][project].serviceType === 'ivs')); 19 | if (filteredProjects.length === 0) { 20 | context.print.error('You have no livestreaming projects.'); 21 | return; 22 | } 23 | 24 | const chooseProject = [ 25 | { 26 | type: 'list', 27 | name: 'resourceName', 28 | message: 'Choose what project you want to set up OBS for?', 29 | choices: filteredProjects, 30 | default: filteredProjects[0], 31 | }, 32 | ]; 33 | const props = await inquirer.prompt(chooseProject); 34 | 35 | const options = amplifyMeta.video[props.resourceName]; 36 | 37 | const obsController = require(`../../provider-utils/${options.providerPlugin}/utils/livestream-obs`); 38 | if (!obsController && obsController.serviceType !== 'livestream') { 39 | context.print.error('OBS controller not configured for this project.'); 40 | return; 41 | } 42 | 43 | return obsController.setupOBS(context, props.resourceName); 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /commands/video/setup-video-player.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer'); 2 | 3 | const subcommand = 'setup-video-player'; 4 | const category = 'video'; 5 | 6 | module.exports = { 7 | name: subcommand, 8 | run: async (context) => { 9 | const { amplify } = context; 10 | const amplifyMeta = amplify.getProjectMeta(); 11 | 12 | if (!(category in amplifyMeta) || Object.keys(amplifyMeta[category]).length === 0) { 13 | context.print.error(`You have no ${category} projects.`); 14 | return; 15 | } 16 | 17 | const filteredProjects = Object.keys(amplifyMeta[category]); 18 | 19 | const chooseProject = [ 20 | { 21 | type: 'list', 22 | name: 'resourceName', 23 | message: 'Choose what project you want to set up a player for?', 24 | choices: filteredProjects, 25 | default: filteredProjects[0], 26 | }, 27 | ]; 28 | const props = await inquirer.prompt(chooseProject); 29 | 30 | const options = amplifyMeta.video[props.resourceName]; 31 | 32 | const playerController = require(`../../provider-utils/${options.providerPlugin}/utils/video-player.js`); 33 | if (!playerController) { 34 | context.print.error('Player controller not configured for this project.'); 35 | return; 36 | } 37 | 38 | return playerController.setupVideoPlayer(context, props.resourceName); 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /commands/video/start.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer'); 2 | 3 | const subcommand = 'start'; 4 | const category = 'video'; 5 | 6 | module.exports = { 7 | name: subcommand, 8 | run: async (context) => { 9 | const { amplify } = context; 10 | const amplifyMeta = amplify.getProjectMeta(); 11 | 12 | if (!(category in amplifyMeta) || Object.keys(amplifyMeta[category]).length === 0) { 13 | context.print.error(`You have no ${category} projects.`); 14 | return; 15 | } 16 | const filteredProjects = Object.keys(amplifyMeta[category]).filter((project) => amplifyMeta[category][project].serviceType === 'livestream'); 17 | if (filteredProjects.length === 0) { 18 | context.print.error('You have no livestreaming projects.'); 19 | return; 20 | } 21 | 22 | const chooseProject = [ 23 | { 24 | type: 'list', 25 | name: 'resourceName', 26 | message: 'Choose what project you want to start?', 27 | choices: filteredProjects, 28 | default: filteredProjects[0], 29 | }, 30 | ]; 31 | 32 | const props = await inquirer.prompt(chooseProject); 33 | 34 | const options = amplifyMeta.video[props.resourceName]; 35 | 36 | const providerController = require(`../../provider-utils/${options.providerPlugin}/index`); 37 | if (!providerController) { 38 | context.print.error('Provider not configured for this category'); 39 | return; 40 | } 41 | 42 | /* eslint-disable */ 43 | return providerController.livestreamStartStop(context, options.serviceType, options, props.resourceName, true); 44 | /* eslint-enable */ 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /commands/video/stop.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer'); 2 | 3 | const subcommand = 'stop'; 4 | const category = 'video'; 5 | 6 | module.exports = { 7 | name: subcommand, 8 | run: async (context) => { 9 | const { amplify } = context; 10 | const amplifyMeta = amplify.getProjectMeta(); 11 | 12 | if (!(category in amplifyMeta) || Object.keys(amplifyMeta[category]).length === 0) { 13 | context.print.error(`You have no ${category} projects.`); 14 | return; 15 | } 16 | const filteredProjects = Object.keys(amplifyMeta[category]).filter((project) => amplifyMeta[category][project].serviceType === 'livestream'); 17 | if (filteredProjects.length === 0) { 18 | context.print.error('You have no livestreaming projects.'); 19 | return; 20 | } 21 | 22 | const chooseProject = [ 23 | { 24 | type: 'list', 25 | name: 'resourceName', 26 | message: 'Choose what project you want to stop?', 27 | choices: filteredProjects, 28 | default: filteredProjects[0], 29 | }, 30 | ]; 31 | 32 | const props = await inquirer.prompt(chooseProject); 33 | 34 | const options = amplifyMeta.video[props.resourceName]; 35 | 36 | const providerController = require(`../../provider-utils/${options.providerPlugin}/index`); 37 | if (!providerController) { 38 | context.print.error('Provider not configured for this category'); 39 | return; 40 | } 41 | 42 | /* eslint-disable */ 43 | return providerController.livestreamStartStop(context, options.serviceType, options, props.resourceName, false); 44 | /* eslint-enable */ 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /commands/video/update.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer'); 2 | 3 | const subcommand = 'update'; 4 | const category = 'video'; 5 | 6 | module.exports = { 7 | name: subcommand, 8 | run: async (context) => { 9 | const { amplify } = context; 10 | const amplifyMeta = amplify.getProjectMeta(); 11 | 12 | if (!(category in amplifyMeta) || Object.keys(amplifyMeta[category]).length === 0) { 13 | context.print.error(`You have no ${category} projects.`); 14 | return; 15 | } 16 | 17 | const chooseProject = [ 18 | { 19 | type: 'list', 20 | name: 'resourceName', 21 | message: 'Choose what project you want to update?', 22 | choices: Object.keys(amplifyMeta[category]), 23 | default: Object.keys(amplifyMeta[category])[0], 24 | }, 25 | ]; 26 | 27 | const props = await inquirer.prompt(chooseProject); 28 | 29 | const options = amplifyMeta.video[props.resourceName]; 30 | 31 | const providerController = require(`../../provider-utils/${options.providerPlugin}/index`); 32 | if (!providerController) { 33 | context.print.error('Provider not configured for this category'); 34 | return; 35 | } 36 | 37 | /* eslint-disable */ 38 | return providerController.updateResource(context, options.serviceType, options, props.resourceName); 39 | /* eslint-enable */ 40 | }, 41 | 42 | }; 43 | -------------------------------------------------------------------------------- /commands/video/version.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const serviceMetadata = JSON.parse(fs.readFileSync(`${__dirname}/../../package.json`)); 4 | const subcommand = 'version'; 5 | 6 | module.exports = { 7 | name: subcommand, 8 | run: async (context) => { 9 | context.print.info(`amplify-category-video@${serviceMetadata.version}`); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const category = 'video'; 2 | const path = require('path'); 3 | const fs = require('fs-extra'); 4 | const { pushTemplates } = require('./provider-utils/awscloudformation/utils/video-staging'); 5 | const { createCDNEnvVars } = require('./provider-utils/awscloudformation/service-walkthroughs/vod-push'); 6 | 7 | async function add(context, providerName, service) { 8 | const options = { 9 | service, 10 | providerPlugin: providerName, 11 | }; 12 | const providerController = require(`./provider-utils/${providerName}/index`); 13 | if (!providerController) { 14 | context.print.error('Provider not configured for this category'); 15 | return; 16 | } 17 | return providerController.addResource(context, category, service, options); 18 | } 19 | 20 | async function console(context) { 21 | context.print.info(`to be implemented: ${category} console`); 22 | } 23 | 24 | async function onAmplifyCategoryOutputChange(context) { 25 | // Hard coded to CF. Find a better way to handle this. 26 | const infoController = require('./provider-utils/awscloudformation/utils/video-getinfo'); 27 | await infoController.getInfoVideoAll(context); 28 | } 29 | 30 | async function createNewEnv(context, resourceName) { 31 | const { amplify } = context; 32 | const amplifyMeta = amplify.getProjectMeta(); 33 | const { teamProviderInfo, localEnvInfo } = context.exeInfo; 34 | const { envName } = localEnvInfo; 35 | if (teamProviderInfo 36 | && teamProviderInfo[envName] 37 | && teamProviderInfo[envName].categories 38 | && teamProviderInfo[envName].categories[category] 39 | && teamProviderInfo[envName].categories[category][resourceName] 40 | && teamProviderInfo[envName].categories[category][resourceName].secretPem) { 41 | return; 42 | } 43 | const targetDir = amplify.pathManager.getBackendDirPath(); 44 | const props = JSON.parse(fs.readFileSync(`${targetDir}/video/${resourceName}/props.json`)); 45 | const options = amplifyMeta.video[resourceName]; 46 | if (options.serviceType === 'video-on-demand') { 47 | if (props.contentDeliveryNetwork && props.contentDeliveryNetwork.signedKey) { 48 | await createCDNEnvVars(context, options, resourceName); 49 | } 50 | } 51 | } 52 | 53 | async function initEnv(context) { 54 | const { amplify } = context; 55 | const amplifyMeta = amplify.getProjectMeta(); 56 | const projectEnvCreate = []; 57 | 58 | if (!(category in amplifyMeta) || Object.keys(amplifyMeta[category]).length === 0) { 59 | return; 60 | } 61 | Object.keys(amplifyMeta[category]).forEach((resourceName) => { 62 | projectEnvCreate.push(createNewEnv(context, resourceName)); 63 | }); 64 | await Promise.all(projectEnvCreate); 65 | await pushTemplates(context); 66 | } 67 | 68 | async function migrate(context) { 69 | const { projectPath, amplifyMeta } = context.migrationInfo; 70 | const migrateResourcePromises = []; 71 | Object.keys(amplifyMeta).forEach((categoryName) => { 72 | if (categoryName === category) { 73 | Object.keys(amplifyMeta[category]).forEach((resourceName) => { 74 | try { 75 | const providerController = require(`./provider-utils/${amplifyMeta[category][resourceName].providerPlugin}/index`); 76 | if (providerController) { 77 | migrateResourcePromises.push(providerController.migrateResource( 78 | context, 79 | projectPath, 80 | amplifyMeta[category][resourceName].service, 81 | resourceName, 82 | )); 83 | } else { 84 | context.print.error(`Provider not configured for ${category}: ${resourceName}`); 85 | } 86 | } catch (e) { 87 | context.print.warning(`Could not run migration for ${category}: ${resourceName}`); 88 | throw e; 89 | } 90 | }); 91 | } 92 | }); 93 | 94 | await Promise.all(migrateResourcePromises); 95 | } 96 | 97 | async function executeAmplifyCommand(context) { 98 | let commandPath = path.normalize(path.join(__dirname, 'commands')); 99 | if (context.input.command === 'help') { 100 | commandPath = path.join(commandPath, category); 101 | } else { 102 | commandPath = path.join(commandPath, category, context.input.command); 103 | } 104 | 105 | const commandModule = require(commandPath); 106 | await commandModule.run(context); 107 | } 108 | 109 | async function handleAmplifyEvent(context, args) { 110 | if (args.event === 'PrePush') { 111 | await handlePrePush(context); 112 | } 113 | } 114 | 115 | async function handlePrePush(context) { 116 | const { amplify } = context; 117 | const amplifyMeta = amplify.getProjectMeta(); 118 | 119 | if (!(category in amplifyMeta) || Object.keys(amplifyMeta[category]).length === 0) { 120 | return; 121 | } 122 | 123 | await pushTemplates(context); 124 | } 125 | 126 | module.exports = { 127 | add, 128 | console, 129 | migrate, 130 | onAmplifyCategoryOutputChange, 131 | executeAmplifyCommand, 132 | handleAmplifyEvent, 133 | initEnv, 134 | }; 135 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | testEnvironment: 'node', 4 | globalSetup: './scripts/setup.js', 5 | globalTeardown: process.env.NODE_ENV === 'test' ? './scripts/teardown.js' : null, // Jest automatically set NODE_ENV to test if it's not already set to something else. 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amplify-category-video", 3 | "version": "3.9.2", 4 | "description": "Plugin for Amplify to add support for live streaming. Made for Unicorn Trivia Workshop", 5 | "main": "index.js", 6 | "scripts": { 7 | "postinstall": "node scripts/post-install.js", 8 | "preversion": "git fetch upstream && git checkout upstream/master && npm run lint && npm run test", 9 | "version": "cross-env-shell git checkout -b release/$npm_package_version ", 10 | "postversion": "git push upstream && git push --tags", 11 | "test": "jest --detectOpenHandles --runInBand", 12 | "dev-test": "NODE_ENV=dev jest --detectOpenHandles --runInBand", 13 | "lint": "eslint .", 14 | "lint-fix": "eslint . --fix", 15 | "release": "node scripts/release.js" 16 | }, 17 | "author": "wizage", 18 | "license": "Apache 2.0", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/awslabs/amplify-video.git" 22 | }, 23 | "keywords": [ 24 | "amplify", 25 | "plugin", 26 | "video" 27 | ], 28 | "dependencies": { 29 | "archiver": "^5.3.1", 30 | "chalk": "^5.0.1", 31 | "child_process": "^1.0.2", 32 | "ejs": "^3.1.8", 33 | "fs-extra": "^10.1.0", 34 | "ini": "^3.0.1", 35 | "inquirer": "^9.1.1", 36 | "mime-types": "^2.1.35", 37 | "node-html-parser": "^5.4.2", 38 | "ora": "^6.1.2", 39 | "sha1": "^1.1.1", 40 | "xcode": "^3.0.1", 41 | "xml2js": "^0.4.23", 42 | "yaml": "^2.1.1" 43 | }, 44 | "devDependencies": { 45 | "aws-sdk": "^2.1210.0", 46 | "axios": "^0.27.2", 47 | "cross-env": "^7.0.3", 48 | "eslint": "^8.23.0", 49 | "eslint-config-airbnb": "^19.0.4", 50 | "eslint-plugin-import": "^2.26.0", 51 | "eslint-plugin-jest": "^27.0.1", 52 | "eslint-plugin-jsx-a11y": "^6.6.1", 53 | "eslint-plugin-react": "^7.31.7", 54 | "eslint-plugin-react-hooks": "^4.6.0", 55 | "glob": "^8.0.3", 56 | "jest": "^29.0.2", 57 | "supertest": "^6.2.4" 58 | }, 59 | "bugs": { 60 | "url": "https://github.com/awslabs/amplify-video/issues" 61 | }, 62 | "homepage": "https://github.com/awslabs/amplify-video#readme" 63 | } 64 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/ivs-helpers/IVS-Channel.template: -------------------------------------------------------------------------------- 1 | Description: S3 Workflow 2 | 3 | Parameters: 4 | pProjectName: 5 | Type: String 6 | Description: ProjectName 7 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" 8 | Default: DefaultName 9 | pLatencyMode: 10 | Type: String 11 | Description: Latency Mode for IVS-Channel 12 | Default: LOW 13 | pQuality: 14 | Type: String 15 | Description: Quality of channel 16 | Default: BASIC 17 | Outputs: 18 | oVideoChannelArn: 19 | Value: !Ref rIVSChannel 20 | oVideoOutput: 21 | Value: !GetAtt rIVSChannel.PlaybackUrl 22 | oVideoInputURL: 23 | Value: !GetAtt rIVSChannel.IngestEndpoint 24 | oVideoInputKey: 25 | Value: !GetAtt rStreamKey.Value 26 | 27 | Resources: 28 | rIVSChannel: 29 | Type: AWS::IVS::Channel 30 | Properties: 31 | Name: !Ref pProjectName 32 | Type: !Ref pQuality 33 | LatencyMode: !Ref pLatencyMode 34 | Tags: 35 | - Key: "amplify-video" 36 | Value: "amplify-video" 37 | 38 | rStreamKey: 39 | Type: AWS::IVS::StreamKey 40 | Properties: 41 | ChannelArn: !Ref rIVSChannel 42 | Tags: 43 | - Key: "amplify-video" 44 | Value: "amplify-video" 45 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/ivs-workflow-template.yaml.ejs: -------------------------------------------------------------------------------- 1 | Description: <%= props.shared.resourceName %> 2 | 3 | Parameters: 4 | env: 5 | Type: String 6 | Description: The environment name. e.g. Dev, Test, or Production. 7 | Default: NONE 8 | pS3: 9 | Type: String 10 | Description: Store template and lambda package 11 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" 12 | Default: "<%= props.shared.bucket %>" 13 | pSourceFolder: 14 | Type: String 15 | Description: Store template and lambda package 16 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" 17 | Default: ivs-helpers 18 | pProjectName: 19 | Type: String 20 | Description: ProjectName 21 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" 22 | Default: <%= props.shared.resourceName %> 23 | pLatencyMode: 24 | Type: String 25 | Description: Latency Mode for IVS-Channel 26 | Default: <%= props.channel.channelLatency %> 27 | pQuality: 28 | Type: String 29 | Description: Quality of channel 30 | Default: <%= props.channel.channelQuality %> 31 | 32 | Conditions: 33 | HasEnvironmentParameter: 34 | !Not [!Equals [!Ref env, NONE]] 35 | 36 | Resources: 37 | rIVSChannel: 38 | Type: AWS::CloudFormation::Stack 39 | Properties: 40 | TemplateURL: !Sub "https://s3.amazonaws.com/${pS3}/${pSourceFolder}/IVS-Channel.template" 41 | Parameters: 42 | pProjectName: 43 | !If 44 | - HasEnvironmentParameter 45 | - !Join 46 | - '-' 47 | - - !Ref pProjectName 48 | - !Ref env 49 | - !Ref pProjectName 50 | pLatencyMode: !Ref pLatencyMode 51 | pQuality: !Ref pQuality 52 | 53 | Outputs: 54 | oVideoOutput: 55 | Value: !GetAtt rIVSChannel.Outputs.oVideoOutput 56 | oVideoInputURL: 57 | Value: !GetAtt rIVSChannel.Outputs.oVideoInputURL 58 | oVideoInputKey: 59 | Value: !GetAtt rIVSChannel.Outputs.oVideoInputKey 60 | oVideoChannelArn: 61 | Value: !GetAtt rIVSChannel.Outputs.oVideoChannelArn -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/livestream-helpers/LambdaFunctions/psdemo-js-live-workflow_v0.4.0/lib/cfResponse.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | /* eslint-disable import/no-unresolved */ 3 | /* eslint-disable no-console */ 4 | const URL = require('url'); 5 | const HTTPS = require('https'); 6 | 7 | const SUCCESS = 'SUCCESS'; 8 | const FAILED = 'FAILED'; 9 | 10 | /** 11 | * @class CloudFormationResponse 12 | */ 13 | class CloudFormationResponse { 14 | constructor(event, context) { 15 | this.$event = null; 16 | this.$context = null; 17 | this.$initError = null; 18 | this.initialize(event, context); 19 | } 20 | 21 | initialize(event, context) { 22 | try { 23 | this.$event = event; 24 | this.$context = context; 25 | /* sanity check on the response */ 26 | let missing = [ 27 | 'StackId', 'RequestId', 'ResponseURL', 'LogicalResourceId', 28 | ].filter(x => this.$event[x] === undefined); 29 | if (missing.length) { 30 | throw new Error(`event missing ${missing.join(', ')}`); 31 | } 32 | missing = ['logStreamName'].filter(x => this.$context[x] === undefined); 33 | if (missing.length) { 34 | throw new Error(`context missing ${missing.join(', ')}`); 35 | } 36 | } catch (e) { 37 | throw e; 38 | } 39 | } 40 | 41 | get event() { return this.$event; } 42 | 43 | get context() { return this.$context; } 44 | 45 | get stackId() { return this.event.StackId; } 46 | 47 | get requestId() { return this.event.RequestId; } 48 | 49 | get responseUrl() { return this.event.ResponseURL; } 50 | 51 | get logicalResourceId() { return this.event.LogicalResourceId; } 52 | 53 | get logStreamName() { return this.context.logStreamName; } 54 | 55 | isUnitTest() { 56 | return !!(this.event.ResourceProperties.PS_UNIT_TEST); 57 | } 58 | 59 | static parseResponseData(data) { 60 | if (data instanceof Error) { 61 | return [ 62 | FAILED, 63 | { 64 | Error: data.message, 65 | Stack: data.stack, 66 | StatusCode: data.StatusCode || 500, 67 | }, 68 | ]; 69 | } 70 | return [SUCCESS, data]; 71 | } 72 | 73 | async send(data, physicalResourceId) { 74 | return new Promise((resolve, reject) => { 75 | const [ 76 | responseStatus, 77 | responseData, 78 | ] = CloudFormationResponse.parseResponseData(data); 79 | console.log(`parseResponseData = ${JSON.stringify({ responseStatus, responseData }, null, 2)}`); 80 | 81 | /* TODO: remove the testing code */ 82 | if (this.isUnitTest()) { 83 | resolve(responseData); 84 | return; 85 | } 86 | 87 | const responseBody = JSON.stringify({ 88 | Status: responseStatus, 89 | Reason: `See details in CloudWatch Log Stream: ${this.logStreamName}`, 90 | PhysicalResourceId: physicalResourceId || this.logStreamName, 91 | StackId: this.stackId, 92 | RequestId: this.requestId, 93 | LogicalResourceId: this.logicalResourceId, 94 | Data: responseData, 95 | }); 96 | 97 | let result = ''; 98 | const url = URL.parse(this.responseUrl); 99 | const params = { 100 | hostname: url.hostname, 101 | port: 443, 102 | path: url.path, 103 | method: 'PUT', 104 | headers: { 105 | 'Content-Type': '', 106 | 'Content-Length': responseBody.length, 107 | }, 108 | }; 109 | 110 | const request = HTTPS.request(params, (response) => { 111 | response.setEncoding('utf8'); 112 | response.on('data', (chunk) => { 113 | result += chunk.toString(); 114 | }); 115 | response.on('end', () => { 116 | if (response.statusCode >= 400) { 117 | const e = new Error(`${params.method} ${url.path} ${response.statusCode}`); 118 | e.statusCode = response.statusCode; 119 | reject(e); 120 | } else { 121 | resolve(result); 122 | } 123 | }); 124 | }); 125 | 126 | request.once('error', (e) => { 127 | e.message = `${params.method} ${url.path} - ${e.message}`; 128 | reject(e); 129 | }); 130 | if (responseBody.length > 0) { 131 | request.write(responseBody); 132 | } 133 | request.end(); 134 | }); 135 | } 136 | } 137 | 138 | module.exports.CloudFormationResponse = CloudFormationResponse; 139 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/livestream-helpers/LambdaFunctions/psdemo-js-live-workflow_v0.4.0/lib/mxStoreResponse.js: -------------------------------------------------------------------------------- 1 | /* Definitions of store response data */ 2 | 3 | /** 4 | * @mixin mxStoreResponse 5 | * @description store key value pair to responseData 6 | * 7 | */ 8 | const mxStoreResponse = Super => class extends Super { 9 | constructor(params) { 10 | super(params); 11 | /* responseData */ 12 | this.$responseData = {}; 13 | } 14 | 15 | get responseData() { return this.$responseData; } 16 | 17 | /** 18 | * @function storeResponseData 19 | * 20 | * @param {string} key 21 | * @param {string|object} value. If is object, expects the object (hash) to have the same 'key' 22 | * 23 | */ 24 | storeResponseData(key, val) { 25 | if (val === undefined || val === null) { 26 | delete this.$responseData[key]; 27 | } else if (typeof val !== 'object') { 28 | this.$responseData[key] = val; 29 | } else { 30 | this.$responseData[key] = val[key]; 31 | } 32 | return this; 33 | } 34 | }; 35 | 36 | module.exports.mxStoreResponse = mxStoreResponse; 37 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/livestream-helpers/LambdaFunctions/psdemo-js-live-workflow_v0.4.0/orchestration.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable strict */ 2 | /* eslint-disable global-require */ 3 | /* eslint-disable no-console */ 4 | const { CloudFormationResponse } = require('./lib/cfResponse'); 5 | const { Babelfish } = require('./lib/babelfish'); 6 | const { Flagfish } = require('./lib/flagfish'); 7 | const { Jellyfish } = require('./lib/jellyfish'); 8 | const { Distribution } = require('./lib/distribution'); 9 | 10 | /** 11 | * 12 | * @function MediaPackageChannel 13 | * 14 | */ 15 | exports.MediaPackageChannel = async (event, context) => { 16 | console.log(` 17 | const event = ${JSON.stringify(event, null, 2)}; 18 | const context = ${JSON.stringify(context, null, 2)}; 19 | `); 20 | 21 | let response; 22 | const cfResponse = new CloudFormationResponse(event, context); 23 | try { 24 | const instance = new Babelfish(event, context); 25 | response = await instance.entry(); 26 | response = await cfResponse.send(response); 27 | return response; 28 | } catch (e) { 29 | console.error(e); 30 | response = await cfResponse.send(e); 31 | return response; 32 | } 33 | }; 34 | 35 | /** 36 | * 37 | * @function MediaLiveChannel 38 | * 39 | */ 40 | exports.MediaLiveChannel = async (event, context) => { 41 | console.log(` 42 | const event = ${JSON.stringify(event, null, 2)}; 43 | const context = ${JSON.stringify(context, null, 2)}; 44 | `); 45 | 46 | let response; 47 | const cfResponse = new CloudFormationResponse(event, context); 48 | try { 49 | const instance = new Flagfish(event, context); 50 | response = await instance.entry(); 51 | response = await cfResponse.send(response); 52 | return response; 53 | } catch (e) { 54 | console.error(e); 55 | response = await cfResponse.send(e); 56 | return response; 57 | } 58 | }; 59 | 60 | /** 61 | * @function MediaStoreContainer 62 | * @description backend lambda to create/delete MediaStore container 63 | * @param {object} event - event information 64 | * @param {object} context - lambda context 65 | */ 66 | exports.MediaStoreContainer = async (event, context) => { 67 | console.log(` 68 | const event = ${JSON.stringify(event, null, 2)}; 69 | const context = ${JSON.stringify(context, null, 2)}; 70 | `); 71 | 72 | let response; 73 | const cfResponse = new CloudFormationResponse(event, context); 74 | try { 75 | const instance = new Jellyfish(event, context); 76 | response = await instance.entry(); 77 | response = await cfResponse.send(response); 78 | return response; 79 | } catch (e) { 80 | console.error(e); 81 | response = await cfResponse.send(e); 82 | return response; 83 | } 84 | }; 85 | 86 | /** 87 | * 88 | * @function UpdateDistribution 89 | * 90 | */ 91 | exports.UpdateDistribution = async (event, context) => { 92 | console.log(` 93 | const event = ${JSON.stringify(event, null, 2)}; 94 | const context = ${JSON.stringify(context, null, 2)}; 95 | `); 96 | 97 | let response; 98 | const cfResponse = new CloudFormationResponse(event, context); 99 | try { 100 | const instance = new Distribution(event, context); 101 | response = await instance.entry(); 102 | response = await cfResponse.send(response); 103 | return response; 104 | } catch (e) { 105 | console.error(e); 106 | response = await cfResponse.send(e); 107 | return response; 108 | } 109 | }; 110 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/livestream-helpers/LambdaFunctions/psdemo-js-live-workflow_v0.4.0/resources/cloudfront/cacheBehavior.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "TargetOriginId": "##ORIGIN ID GOES HERE", 3 | "PathPattern": "##PATH PATTERN GOES HERE", 4 | "SmoothStreaming": false, 5 | "ViewerProtocolPolicy": "https-only", 6 | "MinTTL": 0, 7 | "MaxTTL": 31536000, 8 | "DefaultTTL": 86400, 9 | "Compress": false, 10 | "ForwardedValues": { 11 | "Headers": { 12 | "Quantity": 4, 13 | "Items": [ 14 | "Access-Control-Request-Headers", 15 | "Access-Control-Request-Method", 16 | "Origin", 17 | "User-Agent" 18 | ] 19 | }, 20 | "Cookies": { 21 | "Forward": "all" 22 | }, 23 | "QueryStringCacheKeys": { 24 | "Quantity": 0 25 | }, 26 | "QueryString": true 27 | }, 28 | "TrustedSigners": { 29 | "Quantity": 0, 30 | "Enabled": false 31 | }, 32 | "LambdaFunctionAssociations": { 33 | "Quantity": 0 34 | }, 35 | "AllowedMethods": { 36 | "Quantity": 3, 37 | "Items": [ 38 | "HEAD", 39 | "GET", 40 | "OPTIONS" 41 | ], 42 | "CachedMethods": { 43 | "Quantity": 3, 44 | "Items": [ 45 | "HEAD", 46 | "GET", 47 | "OPTIONS" 48 | ] 49 | } 50 | }, 51 | "FieldLevelEncryptionId": "" 52 | } 53 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/livestream-helpers/LambdaFunctions/psdemo-js-live-workflow_v0.4.0/resources/cloudfront/distributionConfig.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "DistributionConfig": { 3 | "CallerReference": "##CALLER REFERENCE", 4 | "Comment": "##COMMENT GOES HERE", 5 | "PriceClass": "##PRICE CLASS GOES HERE", 6 | "DefaultCacheBehavior": {}, 7 | "CacheBehaviors": { 8 | "Quantity": 0, 9 | "Items": [] 10 | }, 11 | "IsIPV6Enabled": false, 12 | "Logging": { 13 | "Bucket": "## S3 BUCKET GOES HERE", 14 | "Prefix": "cf_logs/", 15 | "Enabled": true, 16 | "IncludeCookies": true 17 | }, 18 | "WebACLId": "", 19 | "Origins": { 20 | "Quantity": 0, 21 | "Items": [] 22 | }, 23 | "DefaultRootObject": "", 24 | "Enabled": true, 25 | "ViewerCertificate": { 26 | "CloudFrontDefaultCertificate": true, 27 | "MinimumProtocolVersion": "TLSv1", 28 | "CertificateSource": "cloudfront" 29 | }, 30 | "CustomErrorResponses": { 31 | "Quantity": 0 32 | }, 33 | "HttpVersion": "http1.1", 34 | "Restrictions": { 35 | "GeoRestriction": { 36 | "Quantity": 0, 37 | "RestrictionType": "none" 38 | } 39 | }, 40 | "Aliases": { 41 | "Quantity": 0 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/livestream-helpers/LambdaFunctions/psdemo-js-live-workflow_v0.4.0/resources/cloudfront/origin.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Id": "##ORIGIN ID GOES HERE", 3 | "DomainName": "##DOMAIN NAME GOES HERE", 4 | "OriginPath": "", 5 | "CustomOriginConfig": { 6 | "OriginSslProtocols": { 7 | "Quantity": 3, 8 | "Items": [ 9 | "TLSv1", 10 | "TLSv1.1", 11 | "TLSv1.2" 12 | ] 13 | }, 14 | "OriginProtocolPolicy": "https-only", 15 | "OriginReadTimeout": 30, 16 | "HTTPPort": 80, 17 | "HTTPSPort": 443, 18 | "OriginKeepaliveTimeout": 5 19 | }, 20 | "CustomHeaders": { 21 | "Quantity": 0 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/livestream-helpers/lambda.template: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | 4 | "Description": "Create lambda function", 5 | 6 | "Metadata": { 7 | "AWS::CloudFormation::Interface": { 8 | "ParameterGroups": [ 9 | { 10 | "Label": { "default": "Lambda Configuration" }, 11 | "Parameters": [ 12 | "pS3", 13 | "pZipFile", 14 | "pLambdaFunction", 15 | "pLambdaHandler", 16 | "pLambdaRoleArn", 17 | "pMemorySize", 18 | "pTimeout" 19 | ] 20 | } 21 | ], 22 | "ParameterLabels": { 23 | "pS3": { 24 | "default": "S3 Bucket" 25 | }, 26 | 27 | "pZipFile": { 28 | "default": "Lambda Package Name" 29 | }, 30 | 31 | "pLambdaFunction": { 32 | "default": "Function Name" 33 | }, 34 | 35 | "pLambdaHandler": { 36 | "default": "Function Handler" 37 | }, 38 | 39 | "pLambdaRoleArn": { 40 | "default": "IAM Role" 41 | }, 42 | 43 | "pMemorySize": { 44 | "default": "Memory Size" 45 | }, 46 | 47 | "pTimeout": { 48 | "default": "Timeout" 49 | } 50 | } 51 | } 52 | }, 53 | 54 | "Resources": { 55 | "rLambdaFunction": { 56 | "Type": "AWS::Lambda::Function", 57 | "Properties": { 58 | "FunctionName": { 59 | "Fn::If": [ 60 | "bLambdaFunction", 61 | { "Ref": "pLambdaFunction" }, 62 | { "Ref": "AWS::NoValue" } 63 | ] 64 | }, 65 | "Runtime": "nodejs14.x", 66 | "MemorySize": { "Ref": "pMemorySize" }, 67 | "Timeout": { "Ref": "pTimeout" }, 68 | "Handler": { "Ref": "pLambdaHandler" }, 69 | "Role": { "Ref": "pLambdaRoleArn" }, 70 | "Code": { 71 | "S3Bucket": { 72 | "Ref": "pS3" 73 | }, 74 | "S3Key": { 75 | "Ref": "pZipFile" 76 | } 77 | }, 78 | "Environment": { 79 | "Variables": { 80 | } 81 | } 82 | } 83 | } 84 | }, 85 | 86 | "Parameters": { 87 | "pS3": { 88 | "Type": "String", 89 | "Description": "store lambda package", 90 | "Default": "mediapackage-demo" 91 | }, 92 | 93 | "pZipFile": { 94 | "Type": "String", 95 | "Description": "zip file package path", 96 | "Default": "livestream-helpers/psdemo-js-live-workflow_v0.1.0.zip" 97 | }, 98 | 99 | "pLambdaFunction": { 100 | "Type": "String", 101 | "Description": "leave it blank to auto-generate the lambda function name", 102 | "Default": "" 103 | }, 104 | 105 | "pLambdaHandler": { 106 | "Type": "String", 107 | "Description": "leave it as is.", 108 | "Default": "file.FunctionName" 109 | }, 110 | 111 | "pLambdaRoleArn": { 112 | "Type": "String", 113 | "Description": "lambda execution IAM role", 114 | "Default": "" 115 | }, 116 | 117 | "pMemorySize": { 118 | "Type": "String", 119 | "Description": "in MB", 120 | "Default": "128" 121 | }, 122 | 123 | "pTimeout": { 124 | "Type": "String", 125 | "Description": "in second", 126 | "Default": "300" 127 | } 128 | }, 129 | 130 | "Conditions": { 131 | "bLambdaFunction" : { 132 | "Fn::Not": [ 133 | { 134 | "Fn::Equals": [ { "Ref": "pLambdaFunction" }, "" ] 135 | } 136 | ] 137 | } 138 | }, 139 | 140 | "Outputs": { 141 | "oS3": { 142 | "Value": { "Ref": "pS3" }, 143 | "Description": "S3 Bucket" 144 | }, 145 | 146 | "oZipFile": { 147 | "Value": { "Ref": "pZipFile" }, 148 | "Description": "Lambda Package Path" 149 | }, 150 | 151 | "oLambdaFunction": { 152 | "Value": { "Ref": "pLambdaFunction" }, 153 | "Description": "Lambda Function Name" 154 | }, 155 | 156 | "oLambdaHandler": { 157 | "Value": { "Ref": "pLambdaHandler" }, 158 | "Description": "Lambda Function Handler" 159 | }, 160 | 161 | "oLambdaArn": { 162 | "Value": { 163 | "Fn::GetAtt": [ "rLambdaFunction", "Arn" ] 164 | }, 165 | "Description": "Arn of Workflow Lambda Function" 166 | }, 167 | 168 | "oLambdaRoleArn": { 169 | "Value": { "Ref": "pLambdaRoleArn" }, 170 | "Description": "Lambda Execution Role" 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/livestream-helpers/medialive-iam.template: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | 4 | "Description": "IAM role and policy for MediaLive", 5 | 6 | "Metadata": { 7 | "AWS::CloudFormation::Interface": { 8 | "ParameterGroups": [ 9 | { 10 | "Label": { "default": "Global Configuration" }, 11 | "Parameters": [ 12 | "pPrefix" 13 | ] 14 | } 15 | ], 16 | "ParameterLabels": { 17 | "pPrefix": { 18 | "default": "Prefix" 19 | } 20 | } 21 | } 22 | }, 23 | 24 | "Resources": { 25 | "rAccessRole": { 26 | "Type": "AWS::IAM::Role", 27 | "Properties": { 28 | "AssumeRolePolicyDocument": { 29 | "Version": "2012-10-17", 30 | "Statement": [ 31 | { 32 | "Effect": "Allow", 33 | "Principal": { 34 | "Service": [ 35 | "ec2.amazonaws.com", 36 | "medialive.amazonaws.com", 37 | "lambda.amazonaws.com" 38 | ] 39 | }, 40 | "Action": "sts:AssumeRole" 41 | } 42 | ] 43 | }, 44 | "ManagedPolicyArns": [ 45 | "arn:aws:iam::aws:policy/AmazonS3FullAccess", 46 | "arn:aws:iam::aws:policy/CloudWatchLogsReadOnlyAccess", 47 | "arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess" 48 | ], 49 | "Path": "/service-role/", 50 | "Policies": [ 51 | { 52 | "PolicyName": "mediastore-access-policy", 53 | "PolicyDocument": { 54 | "Version": "2012-10-17", 55 | "Statement": [ 56 | { 57 | "Sid": "MediaStoreFullAccess", 58 | "Effect": "Allow", 59 | "Action": [ 60 | "mediastore:ListContainers", 61 | "mediastore:DescribeObject", 62 | "mediastore:PutObject", 63 | "mediastore:GetObject", 64 | "mediastore:DeleteObject" 65 | ], 66 | "Resource": { 67 | "Fn::Sub": "arn:aws:mediastore:${AWS::Region}:${AWS::AccountId}:container/*" 68 | } 69 | } 70 | ] 71 | } 72 | } 73 | ], 74 | "RoleName": { 75 | "Fn::If": [ 76 | "cPrefix", 77 | { "Fn::Sub": "${pPrefix}-medialive-access-role-${AWS::Region}" }, 78 | { "Fn::Sub": "${AWS::StackName}-medialive-access-role-${AWS::Region}" } 79 | ] 80 | } 81 | } 82 | }, 83 | 84 | "rManagedPolicy": { 85 | "Type": "AWS::IAM::ManagedPolicy", 86 | "Properties": { 87 | "ManagedPolicyName": { 88 | "Fn::If": [ 89 | "cPrefix", 90 | { "Fn::Sub": "${pPrefix}-medialive-managed-policy-${AWS::Region}" }, 91 | { "Fn::Sub": "${AWS::StackName}-medialive-managed-policy-${AWS::Region}" } 92 | ] 93 | }, 94 | "Description": "AWS Elemental MediaLive Managed Policy", 95 | "Path": "/", 96 | "PolicyDocument": { 97 | "Version": "2012-10-17", 98 | "Statement": [ 99 | { 100 | "Effect": "Allow", 101 | "Action": [ 102 | "medialive:*" 103 | ], 104 | "Resource": "*" 105 | }, 106 | { 107 | "Effect": "Allow", 108 | "Action": [ 109 | "logs:CreateLogGroup", 110 | "logs:CreateLogStream", 111 | "logs:DescribeLogStreams", 112 | "logs:PutLogEvents" 113 | ], 114 | "Resource": [ 115 | "arn:aws:logs:*:*:*" 116 | ] 117 | }, 118 | { 119 | "Effect": "Allow", 120 | "Action": [ 121 | "sns:GetTopicAttributes", 122 | "sns:SetTopicAttributes", 123 | "sns:ListSubscriptionsByTopic", 124 | "sns:DeleteTopic", 125 | "sns:Subscribe", 126 | "sns:Publish" 127 | ], 128 | "Resource": [ 129 | { "Fn::Sub": "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:*" } 130 | ] 131 | }, 132 | { 133 | "Effect": "Allow", 134 | "Action": [ 135 | "ssm:Describe*", 136 | "ssm:Get*", 137 | "ssm:List*", 138 | "ssm:PutParameter*", 139 | "ssm:DeleteParameter*" 140 | ], 141 | "Resource": { 142 | "Fn::Sub": "arn:aws:ssm:*:${AWS::AccountId}:parameter/*" 143 | } 144 | }, 145 | { 146 | "Effect": "Allow", 147 | "Action": [ 148 | "iam:PassRole" 149 | ], 150 | "Resource": "*" 151 | } 152 | ] 153 | } 154 | } 155 | }, 156 | 157 | "rProvisionRole": { 158 | "Type": "AWS::IAM::Role", 159 | "Properties": { 160 | "AssumeRolePolicyDocument": { 161 | "Version": "2012-10-17", 162 | "Statement": [ 163 | { 164 | "Effect": "Allow", 165 | "Principal": { 166 | "AWS": { 167 | "Fn::Sub": "${AWS::AccountId}" 168 | }, 169 | "Service": [ 170 | "lambda.amazonaws.com" 171 | ] 172 | }, 173 | "Action": "sts:AssumeRole" 174 | } 175 | ] 176 | }, 177 | "ManagedPolicyArns": [ 178 | { 179 | "Ref": "rManagedPolicy" 180 | } 181 | ], 182 | "Path": "/service-role/", 183 | "RoleName": { 184 | "Fn::If": [ 185 | "cPrefix", 186 | { "Fn::Sub": "${pPrefix}-medialive-role-${AWS::Region}" }, 187 | { "Fn::Sub": "${AWS::StackName}-medialive-role-${AWS::Region}" } 188 | ] 189 | } 190 | } 191 | } 192 | }, 193 | 194 | "Parameters": { 195 | "pPrefix": { 196 | "Type": "String", 197 | "Description": "used to prefix resource name", 198 | "AllowedPattern" : "[a-zA-Z][a-zA-Z0-9-_]*" 199 | } 200 | }, 201 | 202 | "Conditions": { 203 | "cPrefix": { 204 | "Fn::Not": [ 205 | { 206 | "Fn::Equals": [ { "Ref": "pPrefix" }, "" ] 207 | } 208 | ] 209 | } 210 | }, 211 | 212 | "Outputs": { 213 | "oManagedPolicy": { 214 | "Value": { "Ref": "rManagedPolicy" }, 215 | "Description": "Managed Policy" 216 | }, 217 | 218 | "oProvisionRoleArn": { 219 | "Value": { "Fn::GetAtt": [ "rProvisionRole", "Arn" ] }, 220 | "Description": "Lambda Provision Role" 221 | }, 222 | 223 | "oAccessRoleArn": { 224 | "Value": { "Fn::GetAtt": [ "rAccessRole", "Arn" ] }, 225 | "Description": "MediaLive Access Role" 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/livestream-helpers/mediapackage-channel.template: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | 4 | "Description": "Per MediaPackage Channel resource", 5 | 6 | "Metadata": { 7 | "AWS::CloudFormation::Interface": { 8 | "ParameterGroups": [ 9 | { 10 | "Label": { "default": "MediaPackage Channel: Provision Configuration" }, 11 | "Parameters": [ 12 | "pLambdaArn" 13 | ] 14 | }, 15 | { 16 | "Label": { "default": "MediaPackage Channel: Configuration" }, 17 | "Parameters": [ 18 | "pChannelId", 19 | "pIngestType", 20 | "pEndpoints", 21 | "pStartOverWindow", 22 | "pGopSizeInSec", 23 | "pGopPerSegment", 24 | "pSegmentPerPlaylist" 25 | ] 26 | } 27 | ], 28 | "ParameterLabels": { 29 | "pLambdaArn": { 30 | "default": "Lambda Function Arn" 31 | }, 32 | 33 | "pChannelId": { 34 | "default": "Channel ID" 35 | }, 36 | 37 | "pIngestType": { 38 | "default": "Ingest Type" 39 | }, 40 | 41 | "pEndpoints": { 42 | "default": "Packaging Type(s)" 43 | }, 44 | 45 | "pStartOverWindow": { 46 | "default": "Content Window (in seconds)" 47 | }, 48 | 49 | "pGopSizeInSec": { 50 | "default": "GOP Size (in seconds)" 51 | }, 52 | 53 | "pGopPerSegment": { 54 | "default": "GOP Per Segment" 55 | }, 56 | 57 | "pSegmentPerPlaylist": { 58 | "default": "Segment(s) Per Playlist" 59 | } 60 | } 61 | } 62 | }, 63 | 64 | "Resources": { 65 | "rCreateChannelLambda": { 66 | "Type": "Custom::rCreateChannelLambda", 67 | "Properties": { 68 | "ServiceToken": { 69 | "Fn::Sub": "${pLambdaArn}" 70 | }, 71 | 72 | "PS_CHANNEL_ID": { 73 | "Fn::If": [ 74 | "cChannelId", 75 | { "Fn::Sub": "${pChannelId}" }, 76 | { "Fn::Sub": "${AWS::StackName}" } 77 | ] 78 | }, 79 | 80 | "PS_CHANNEL_DESC": { 81 | "Fn::If": [ 82 | "cChannelId", 83 | { "Fn::Sub": "Channel ${pChannelId} created by ${AWS::StackName}" }, 84 | { "Fn::Sub": "Channel ${AWS::StackName} created by ${AWS::StackName}" } 85 | ] 86 | }, 87 | 88 | "PS_INGEST_TYPE": { 89 | "Ref": "pIngestType" 90 | }, 91 | 92 | "PS_ENDPOINTS": { 93 | "Ref": "pEndpoints" 94 | }, 95 | 96 | "PS_STARTOVER_WINDOW": { 97 | "Ref": "pStartOverWindow" 98 | }, 99 | 100 | "PS_GOP_SIZE_IN_SEC": { 101 | "Ref": "pGopSizeInSec" 102 | }, 103 | 104 | "PS_GOP_PER_SEGMENT": { 105 | "Ref": "pGopPerSegment" 106 | }, 107 | 108 | "PS_SEGMENT_PER_PLAYLIST": { 109 | "Ref": "pSegmentPerPlaylist" 110 | } 111 | } 112 | } 113 | }, 114 | 115 | "Parameters": { 116 | "pLambdaArn": { 117 | "Type": "String", 118 | "Description": "used for provisioning the worfklow", 119 | "Default": "" 120 | }, 121 | 122 | "pChannelId": { 123 | "Type": "String", 124 | "Description": "mediapackage channel id. Leave it blank to use Stack Name as Channel Name", 125 | "AllowedPattern" : "[a-zA-Z][a-zA-Z0-9-_]*" 126 | }, 127 | 128 | "pIngestType": { 129 | "Type": "String", 130 | "Description": "mediapackage ingest type. Leave it as is", 131 | "AllowedPattern" : "[a-z]*", 132 | "Default": "hls" 133 | }, 134 | 135 | "pEndpoints": { 136 | "Type": "CommaDelimitedList", 137 | "Description": "comma delimited list. ie., HLS,DASH,MSS", 138 | "Default": "HLS,DASH" 139 | }, 140 | 141 | "pStartOverWindow": { 142 | "Type": "Number", 143 | "Description": "specify how far the user can seek backward", 144 | "Default": "300", 145 | "MinValue": "60" 146 | }, 147 | 148 | "pGopSizeInSec": { 149 | "Type": "Number", 150 | "Description": "specify GOP size in seconds. Use 1s for low-latency (IP-frame only).", 151 | "Default": "1", 152 | "MinValue": "1" 153 | }, 154 | 155 | "pGopPerSegment": { 156 | "Type": "Number", 157 | "Description": "specify how many GOP per segment. Use 1s for low-latency.", 158 | "Default": "1", 159 | "MinValue": "1" 160 | }, 161 | 162 | "pSegmentPerPlaylist": { 163 | "Type": "Number", 164 | "Description": "specify number of segments per playlist/manifest, minimum 1 / recommend 3", 165 | "Default": "3", 166 | "MinValue": "1" 167 | } 168 | }, 169 | 170 | "Conditions": { 171 | "cChannelId": { 172 | "Fn::Not": [ 173 | { 174 | "Fn::Equals": [ { "Ref": "pChannelId" }, "" ] 175 | } 176 | ] 177 | } 178 | }, 179 | 180 | "Outputs": { 181 | "oId": { 182 | "Value": { "Fn::GetAtt": [ "rCreateChannelLambda", "Id" ] }, 183 | "Description": "Channel Id" 184 | }, 185 | 186 | "oIngestUrls": { 187 | "Value": { "Fn::GetAtt": [ "rCreateChannelLambda", "Url" ] }, 188 | "Description": "Channel Ingest Url" 189 | }, 190 | 191 | "oUsers": { 192 | "Value": { "Fn::GetAtt": [ "rCreateChannelLambda", "Username" ] }, 193 | "Description": "Channel User" 194 | }, 195 | 196 | "oParameterStoreKeys": { 197 | "Value": { "Fn::GetAtt": [ "rCreateChannelLambda", "ParameterStoreKey" ] }, 198 | "Description": "Channel Parameter Store Key" 199 | }, 200 | 201 | "oPasswords": { 202 | "Value": { "Fn::GetAtt": [ "rCreateChannelLambda", "Password" ] }, 203 | "Description": "Channel Password" 204 | }, 205 | 206 | "oArn": { 207 | "Value": { "Fn::GetAtt": [ "rCreateChannelLambda", "Arn" ] }, 208 | "Description": "Channel Arn" 209 | }, 210 | 211 | "oHlsEndpoint": { 212 | "Value": { "Fn::GetAtt": [ "rCreateChannelLambda", "HlsEndpoint" ] }, 213 | "Description": "HLS Endpoint Url" 214 | }, 215 | 216 | "oDashEndpoint": { 217 | "Value": { "Fn::GetAtt": [ "rCreateChannelLambda", "DashEndpoint" ] }, 218 | "Description": "DASH Endpoint Url" 219 | }, 220 | 221 | "oMssEndpoint": { 222 | "Value": { "Fn::GetAtt": [ "rCreateChannelLambda", "MssEndpoint" ] }, 223 | "Description": "MSS Endpoint Url" 224 | }, 225 | 226 | "oCmafEndpoint": { 227 | "Value": { "Fn::GetAtt": [ "rCreateChannelLambda", "CmafEndpoint" ] }, 228 | "Description": "CMAF Endpoint Url" 229 | }, 230 | 231 | "oDomainEndpoint": { 232 | "Value": { "Fn::GetAtt": [ "rCreateChannelLambda", "DomainEndpoint" ] }, 233 | "Description": "Endpoint Domain" 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/livestream-helpers/mediapackage-iam.template: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | 4 | "Description": "IAM role and policy for MediaPackage", 5 | 6 | "Metadata": { 7 | "AWS::CloudFormation::Interface": { 8 | "ParameterGroups": [ 9 | { 10 | "Label": { "default": "Global Configuration" }, 11 | "Parameters": [ 12 | "pPrefix" 13 | ] 14 | } 15 | ], 16 | "ParameterLabels": { 17 | "pPrefix": { 18 | "default": "Prefix" 19 | } 20 | } 21 | } 22 | }, 23 | 24 | "Resources": { 25 | "rManagedPolicy": { 26 | "Type": "AWS::IAM::ManagedPolicy", 27 | "Properties": { 28 | "ManagedPolicyName": { 29 | "Fn::If": [ 30 | "cPrefix", 31 | { "Fn::Sub": "${pPrefix}-mediapackage-managed-policy-${AWS::Region}" }, 32 | { "Fn::Sub": "${AWS::StackName}-mediapackage-managed-policy-${AWS::Region}" } 33 | ] 34 | }, 35 | "Description": "AWS Elemental MediaPackage Managed Policy", 36 | "Path": "/", 37 | "PolicyDocument": { 38 | "Version": "2012-10-17", 39 | "Statement": [ 40 | { 41 | "Effect": "Allow", 42 | "Action": [ 43 | "mediapackage:*" 44 | ], 45 | "Resource": "*" 46 | }, 47 | { 48 | "Effect": "Allow", 49 | "Action": [ 50 | "logs:CreateLogGroup", 51 | "logs:CreateLogStream", 52 | "logs:DescribeLogStreams", 53 | "logs:PutLogEvents" 54 | ], 55 | "Resource": [ 56 | "arn:aws:logs:*:*:*" 57 | ] 58 | }, 59 | { 60 | "Effect": "Allow", 61 | "Action": [ 62 | "sns:GetTopicAttributes", 63 | "sns:SetTopicAttributes", 64 | "sns:ListSubscriptionsByTopic", 65 | "sns:DeleteTopic", 66 | "sns:Subscribe", 67 | "sns:Publish" 68 | ], 69 | "Resource": [ 70 | { "Fn::Sub": "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:*" } 71 | ] 72 | }, 73 | { 74 | "Effect": "Allow", 75 | "Action": [ 76 | "ssm:Describe*", 77 | "ssm:Get*", 78 | "ssm:List*", 79 | "ssm:PutParameter*", 80 | "ssm:DeleteParameter*" 81 | ], 82 | "Resource": { 83 | "Fn::Sub": "arn:aws:ssm:*:${AWS::AccountId}:parameter/*" 84 | } 85 | } 86 | ] 87 | } 88 | } 89 | }, 90 | 91 | "rProvisionRole": { 92 | "Type": "AWS::IAM::Role", 93 | "Properties": { 94 | "AssumeRolePolicyDocument": { 95 | "Version": "2012-10-17", 96 | "Statement": [ 97 | { 98 | "Effect": "Allow", 99 | "Principal": { 100 | "AWS": { 101 | "Fn::Sub": "${AWS::AccountId}" 102 | }, 103 | "Service": [ 104 | "lambda.amazonaws.com" 105 | ] 106 | }, 107 | "Action": "sts:AssumeRole" 108 | } 109 | ] 110 | }, 111 | "ManagedPolicyArns": [ 112 | { 113 | "Ref": "rManagedPolicy" 114 | } 115 | ], 116 | "Path": "/service-role/", 117 | "RoleName": { 118 | "Fn::If": [ 119 | "cPrefix", 120 | { "Fn::Sub": "${pPrefix}-mediapackage-role-${AWS::Region}" }, 121 | { "Fn::Sub": "${AWS::StackName}-mediapackage-role-${AWS::Region}" } 122 | ] 123 | } 124 | } 125 | } 126 | }, 127 | 128 | "Parameters": { 129 | "pPrefix": { 130 | "Type": "String", 131 | "Description": "used to prefix resource name", 132 | "AllowedPattern" : "[a-zA-Z][a-zA-Z0-9-_]*" 133 | } 134 | }, 135 | 136 | "Conditions": { 137 | "cPrefix": { 138 | "Fn::Not": [ 139 | { 140 | "Fn::Equals": [ { "Ref": "pPrefix" }, "" ] 141 | } 142 | ] 143 | } 144 | }, 145 | 146 | "Outputs": { 147 | "oManagedPolicy": { 148 | "Value": { "Ref": "rManagedPolicy" }, 149 | "Description": "Managed Policy" 150 | }, 151 | 152 | "oProvisionRoleArn": { 153 | "Value": { "Fn::GetAtt": [ "rProvisionRole", "Arn" ] }, 154 | "Description": "Lambda Provision Role" 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/livestream-helpers/mediastore-container.template: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | 4 | "Description": "Per MediaStore Container resource", 5 | 6 | "Metadata": { 7 | "AWS::CloudFormation::Interface": { 8 | "ParameterGroups": [ 9 | { 10 | "Label": { "default": "Workflow: Provision Configuration" }, 11 | "Parameters": [ 12 | "pLambdaArn" 13 | ] 14 | }, 15 | { 16 | "Label": { "default": "MediaStore: Container Configuration" }, 17 | "Parameters": [ 18 | "pContainerName" 19 | ] 20 | } 21 | ], 22 | "ParameterLabels": { 23 | "pLambdaArn": { 24 | "default": "Lambda Function Arn" 25 | }, 26 | 27 | "pContainerName": { 28 | "default": "Container Name" 29 | } 30 | } 31 | } 32 | }, 33 | 34 | "Resources": { 35 | "rCreateContainerLambda": { 36 | "Type": "Custom::rCreateContainerLambda", 37 | "Properties": { 38 | "ServiceToken": { 39 | "Fn::Sub": "${pLambdaArn}" 40 | }, 41 | 42 | "PS_CONTAINER_NAME": { 43 | "Fn::If": [ 44 | "cContainerName", 45 | { "Fn::Sub": "${pContainerName}" }, 46 | { "Fn::Sub": "${AWS::StackName}" } 47 | ] 48 | } 49 | } 50 | } 51 | }, 52 | 53 | "Parameters": { 54 | "pLambdaArn": { 55 | "Type": "String", 56 | "Description": "used for provisioning the worfklow", 57 | "Default": "" 58 | }, 59 | 60 | "pContainerName": { 61 | "Type": "String", 62 | "Description": "mediastore container name. Leave it blank to use Stack Name as Container Name" 63 | } 64 | }, 65 | 66 | "Conditions": { 67 | "cContainerName": { 68 | "Fn::Not": [ 69 | { 70 | "Fn::Equals": [ { "Ref": "pContainerName" }, "" ] 71 | } 72 | ] 73 | } 74 | }, 75 | 76 | "Outputs": { 77 | "oLambdaArn": { 78 | "Value": { "Ref": "pLambdaArn" }, 79 | "Description": "Provision Lambda Arn" 80 | }, 81 | 82 | "oContainerArn": { 83 | "Value": { "Fn::GetAtt": [ "rCreateContainerLambda", "ContainerArn" ] }, 84 | "Description": "Container Arn" 85 | }, 86 | 87 | "oContainerName": { 88 | "Value": { "Fn::GetAtt": [ "rCreateContainerLambda", "ContainerName" ] }, 89 | "Description": "Container Name" 90 | }, 91 | 92 | "oContainerEndpoint": { 93 | "Value": { "Fn::GetAtt": [ "rCreateContainerLambda", "ContainerEndpoint" ] }, 94 | "Description": "Container Endpoint" 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/livestream-helpers/mediastore-iam.template: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | 4 | "Description": "IAM role and policy for MediaStore", 5 | 6 | "Metadata": { 7 | "AWS::CloudFormation::Interface": { 8 | "ParameterGroups": [ 9 | { 10 | "Label": { "default": "Global Configuration" }, 11 | "Parameters": [ 12 | "pPrefix" 13 | ] 14 | } 15 | ], 16 | "ParameterLabels": { 17 | "pPrefix": { 18 | "default": "Prefix" 19 | } 20 | } 21 | } 22 | }, 23 | 24 | "Resources": { 25 | "rManagedPolicy": { 26 | "Type": "AWS::IAM::ManagedPolicy", 27 | "Properties": { 28 | "ManagedPolicyName": { 29 | "Fn::If": [ 30 | "cPrefix", 31 | { "Fn::Sub": "${pPrefix}-mediastore-managed-policy-${AWS::Region}" }, 32 | { "Fn::Sub": "${AWS::StackName}-mediastore-managed-policy-${AWS::Region}" } 33 | ] 34 | }, 35 | "Description": "AWS Elemental MediaStore Managed Policy", 36 | "Path": "/", 37 | "PolicyDocument": { 38 | "Version": "2012-10-17", 39 | "Statement": [ 40 | { 41 | "Effect": "Allow", 42 | "Action": [ 43 | "mediastore:*" 44 | ], 45 | "Resource": "*" 46 | }, 47 | { 48 | "Effect": "Allow", 49 | "Action": [ 50 | "logs:CreateLogGroup", 51 | "logs:CreateLogStream", 52 | "logs:DescribeLogStreams", 53 | "logs:PutLogEvents" 54 | ], 55 | "Resource": [ 56 | "arn:aws:logs:*:*:*" 57 | ] 58 | }, 59 | { 60 | "Effect": "Allow", 61 | "Action": [ 62 | "iam:PassRole" 63 | ], 64 | "Resource": "*" 65 | } 66 | ] 67 | } 68 | } 69 | }, 70 | 71 | "rProvisionRole": { 72 | "Type": "AWS::IAM::Role", 73 | "Properties": { 74 | "AssumeRolePolicyDocument": { 75 | "Version": "2012-10-17", 76 | "Statement": [ 77 | { 78 | "Effect": "Allow", 79 | "Principal": { 80 | "AWS": { 81 | "Fn::Sub": "${AWS::AccountId}" 82 | }, 83 | "Service": [ 84 | "lambda.amazonaws.com" 85 | ] 86 | }, 87 | "Action": "sts:AssumeRole" 88 | } 89 | ] 90 | }, 91 | "ManagedPolicyArns": [ 92 | { 93 | "Ref": "rManagedPolicy" 94 | } 95 | ], 96 | "Path": "/service-role/", 97 | "RoleName": { 98 | "Fn::If": [ 99 | "cPrefix", 100 | { "Fn::Sub": "${pPrefix}-mediastore-role-${AWS::Region}" }, 101 | { "Fn::Sub": "${AWS::StackName}-mediastore-role-${AWS::Region}" } 102 | ] 103 | } 104 | } 105 | } 106 | }, 107 | 108 | "Parameters": { 109 | "pPrefix": { 110 | "Type": "String", 111 | "Description": "used to prefix resource name", 112 | "AllowedPattern" : "[a-zA-Z][a-zA-Z0-9-_]*" 113 | } 114 | }, 115 | 116 | "Conditions": { 117 | "cPrefix": { 118 | "Fn::Not": [ 119 | { 120 | "Fn::Equals": [ { "Ref": "pPrefix" }, "" ] 121 | } 122 | ] 123 | } 124 | }, 125 | 126 | "Outputs": { 127 | "oManagedPolicy": { 128 | "Value": { "Ref": "rManagedPolicy" }, 129 | "Description": "Managed Policy" 130 | }, 131 | 132 | "oProvisionRoleArn": { 133 | "Value": { "Fn::GetAtt": [ "rProvisionRole", "Arn" ] }, 134 | "Description": "Lambda Provision Role" 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/livestream-helpers/mediastore.template: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | 4 | "Description": "Create resources for MediaStore service", 5 | 6 | "Metadata": { 7 | "AWS::CloudFormation::Interface": { 8 | "ParameterGroups": [ 9 | { 10 | "Label": { "default": "Provision: Source Files Configuration" }, 11 | "Parameters": [ 12 | "pS3", 13 | "pSourceFolder", 14 | "pPackageName", 15 | "pProvisionLambdaHandler" 16 | ] 17 | }, 18 | { 19 | "Label": { "default": "MediaStore: Container Configuration" }, 20 | "Parameters": [ 21 | "pContainerName" 22 | ] 23 | } 24 | ], 25 | "ParameterLabels": { 26 | "pS3": { 27 | "default": "S3 Bucket Name" 28 | }, 29 | 30 | "pSourceFolder": { 31 | "default": "Source Folder" 32 | }, 33 | 34 | "pPackageName": { 35 | "default": "Lambda Package Name" 36 | }, 37 | 38 | "pProvisionLambdaHandler": { 39 | "default": "Lambda Function Handler" 40 | }, 41 | 42 | "pContainerName": { 43 | "default": "Container Name" 44 | } 45 | } 46 | } 47 | }, 48 | 49 | "Resources": { 50 | "rIAM": { 51 | "Type": "AWS::CloudFormation::Stack", 52 | "Properties": { 53 | "TemplateURL": { 54 | "Fn::Sub": "https://s3.amazonaws.com/${pS3}/${pSourceFolder}/mediastore-iam.template" 55 | }, 56 | "Parameters": { 57 | "pPrefix": { 58 | "Fn::If": [ 59 | "cContainerName", 60 | { "Fn::Sub": "${pContainerName}" }, 61 | { "Fn::Sub": "${AWS::StackName}" } 62 | ] 63 | } 64 | } 65 | } 66 | }, 67 | 68 | "rProvisionLambdaFunction": { 69 | "Type": "AWS::CloudFormation::Stack", 70 | "Properties": { 71 | "TemplateURL": { 72 | "Fn::Sub": "https://s3.amazonaws.com/${pS3}/${pSourceFolder}/lambda.template" 73 | }, 74 | "Parameters": { 75 | "pS3": { 76 | "Ref": "pS3" 77 | }, 78 | 79 | "pZipFile": { 80 | "Fn::Sub": "${pSourceFolder}/${pPackageName}" 81 | }, 82 | 83 | "pLambdaHandler": { 84 | "Ref": "pProvisionLambdaHandler" 85 | }, 86 | 87 | "pLambdaRoleArn": { 88 | "Fn::GetAtt": [ "rIAM", "Outputs.oProvisionRoleArn" ] 89 | }, 90 | 91 | "pMemorySize": "512", 92 | 93 | "pTimeout": "300" 94 | } 95 | } 96 | }, 97 | 98 | "rContainer": { 99 | "Type": "AWS::CloudFormation::Stack", 100 | "Properties": { 101 | "TemplateURL": { 102 | "Fn::Sub": "https://s3.amazonaws.com/${pS3}/${pSourceFolder}/mediastore-container.template" 103 | }, 104 | "Parameters": { 105 | "pLambdaArn": { 106 | "Fn::GetAtt": [ "rProvisionLambdaFunction", "Outputs.oLambdaArn" ] 107 | }, 108 | 109 | "pContainerName": { 110 | "Fn::If": [ 111 | "cContainerName", 112 | { "Fn::Sub": "${pContainerName}" }, 113 | { "Fn::Sub": "${AWS::StackName}" } 114 | ] 115 | } 116 | } 117 | } 118 | } 119 | }, 120 | 121 | "Parameters": { 122 | "pS3": { 123 | "Type": "String", 124 | "Description": "store template and lambda package", 125 | "AllowedPattern" : "[a-zA-Z][a-zA-Z0-9-_]*", 126 | "Default": "mediapackage-demo" 127 | }, 128 | 129 | "pSourceFolder": { 130 | "Type": "String", 131 | "Description": "store template and lambda package", 132 | "Default": "livestream-helpers" 133 | }, 134 | 135 | "pPackageName": { 136 | "Type": "String", 137 | "Description": "lambda package zip file", 138 | "Default": "psdemo-js-live-workflow_v0.3.0.zip" 139 | }, 140 | 141 | "pProvisionLambdaHandler": { 142 | "Type": "String", 143 | "Description": "program entrypoint. Leave it as is.", 144 | "Default": "orchestration.MediaStoreContainer" 145 | }, 146 | 147 | "pContainerName": { 148 | "Type": "String", 149 | "Description": "mediastore container name. Use Stack Name for Container name if blank" 150 | } 151 | }, 152 | 153 | "Conditions": { 154 | "cContainerName": { 155 | "Fn::Not": [ 156 | { 157 | "Fn::Equals": [ { "Ref": "pContainerName" }, "" ] 158 | } 159 | ] 160 | } 161 | }, 162 | 163 | "Outputs": { 164 | "oS3": { 165 | "Value": { "Ref": "pS3" }, 166 | "Description": "S3 Bucket for source files" 167 | }, 168 | 169 | "oPackagePath": { 170 | "Value": { "Fn::Sub": "s3://${pS3}/${pSourceFolder}/${pPackageName}" }, 171 | "Description": "Lambda Package Path (Reference)" 172 | }, 173 | 174 | "oManagedPolicy": { 175 | "Value": { 176 | "Fn::GetAtt": [ "rIAM", "Outputs.oManagedPolicy" ] 177 | }, 178 | "Description": "MediaStore Managed Policy" 179 | }, 180 | 181 | "oProvisionRole": { 182 | "Value": { 183 | "Fn::GetAtt": [ "rIAM", "Outputs.oProvisionRoleArn" ] 184 | }, 185 | "Description": "MediaStore Provision Role" 186 | }, 187 | 188 | "oProvisionLambdaArn": { 189 | "Value": { "Fn::GetAtt": [ "rProvisionLambdaFunction", "Outputs.oLambdaArn" ] }, 190 | "Description": "Lambda Provision Arn" 191 | }, 192 | 193 | "oContainerArn": { 194 | "Value": { "Fn::GetAtt": [ "rContainer", "Outputs.oContainerArn" ] }, 195 | "Description": "MediaStore Container Arn" 196 | }, 197 | 198 | "oContainerName": { 199 | "Value": { "Fn::GetAtt": [ "rContainer", "Outputs.oContainerName" ] }, 200 | "Description": "MediaStore Container Name" 201 | }, 202 | 203 | "oContainerEndpoint": { 204 | "Value": { "Fn::GetAtt": [ "rContainer", "Outputs.oContainerEndpoint" ] }, 205 | "Description": "MediaStore Container Endpoint" 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/vod-helpers/CFDistribution.template.ejs: -------------------------------------------------------------------------------- 1 | Description: CloudFront Distribution for output bucket 2 | 3 | Parameters: 4 | pBucketUrl: 5 | Type: String 6 | Description: ProjectName 7 | Default: DefaultName 8 | pOriginAccessIdentity: 9 | Type: String 10 | Description: Policy for bucket 11 | Default: NA 12 | <% if (props.contentDeliveryNetwork.signedKey) { %> 13 | pProjectName: 14 | Type: String 15 | Description: Name for public key 16 | Default: DefaultName 17 | <% } %> 18 | 19 | Resources: 20 | rCloudFrontDist: 21 | Type: AWS::CloudFront::Distribution 22 | Properties: 23 | Tags: 24 | - Key: amplify-video 25 | Value: amplify-video 26 | DistributionConfig: 27 | DefaultCacheBehavior: 28 | ForwardedValues: 29 | QueryString: false 30 | Cookies: 31 | Forward: none 32 | Headers: 33 | - 'Origin' 34 | - 'Access-Control-Request-Method' 35 | - 'Access-Control-Request-Headers' 36 | AllowedMethods: 37 | - GET 38 | - HEAD 39 | - OPTIONS 40 | TargetOriginId: "vodS3Origin" 41 | ViewerProtocolPolicy: "allow-all" 42 | <% if (props.contentDeliveryNetwork.signedKey) { %> 43 | TrustedKeyGroups: 44 | - !Ref rCloudFrontKeyGroup 45 | <% } %> 46 | Origins: 47 | - 48 | DomainName: !Ref pBucketUrl 49 | Id: vodS3Origin 50 | S3OriginConfig: 51 | OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${pOriginAccessIdentity}" 52 | Enabled: 'true' 53 | PriceClass: PriceClass_All 54 | <% if (props.contentDeliveryNetwork.signedKey) { %> 55 | <%= props.contentDeliveryNetwork.rPublicName %>: 56 | Type: AWS::CloudFront::PublicKey 57 | Properties: 58 | PublicKeyConfig: 59 | CallerReference: <%= props.contentDeliveryNetwork.publicKeyName %> 60 | Name: <%= props.contentDeliveryNetwork.publicKeyName %> 61 | EncodedKey: "<%= props.contentDeliveryNetwork.publicKey %>" 62 | rCloudFrontKeyGroup: 63 | Type: AWS::CloudFront::KeyGroup 64 | Properties: 65 | KeyGroupConfig: 66 | Name: !Sub "${pProjectName}-KeyGroup" 67 | Items: 68 | - !Ref <%= props.contentDeliveryNetwork.rPublicName %> 69 | <% } %> 70 | Outputs: 71 | <% if (props.contentDeliveryNetwork.signedKey) { %> 72 | oPemId: 73 | Value: !Ref <%= props.contentDeliveryNetwork.rPublicName %> 74 | Description: Pem Key Id 75 | <% } %> 76 | oCFDomain: 77 | Value: !GetAtt rCloudFrontDist.DomainName 78 | Description: Domain for our videos 79 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/vod-helpers/CFTokenGen.template: -------------------------------------------------------------------------------- 1 | Description: Generating tokens for CF 2 | 3 | Parameters: 4 | pS3: 5 | Type: String 6 | Description: Store template and lambda package 7 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" 8 | Default: s3default 9 | pFunctionName: 10 | Type: String 11 | Description: ProjectName 12 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" 13 | Default: TokenGen 14 | pSecretPemArn: 15 | Type: String 16 | Description: ProjectName 17 | Default: TokenGen 18 | pPemID: 19 | Type: String 20 | Description: Store template and lambda package 21 | Default: s3default 22 | pSecretPem: 23 | Type: String 24 | Description: Store template and lambda package 25 | Default: vod-helpers 26 | pDomainName: 27 | Type: String 28 | Description: ProjectName 29 | Default: DefaultName 30 | pFunctionHash: 31 | Type: String 32 | Description: FunctionHash 33 | Default: default 34 | 35 | Resources: 36 | TokenGen: 37 | Type: AWS::Lambda::Function 38 | Properties: 39 | FunctionName: !Ref pFunctionName 40 | Description: Sends a notification when a new object is put into the bucket 41 | Handler: index.handler 42 | Role: !GetAtt LambdaExecutionRole.Arn 43 | Runtime: nodejs14.x 44 | Timeout: 5 45 | Code: 46 | S3Bucket: !Ref pS3 47 | S3Key: !Sub 48 | - vod-helpers/CloudFrontTokenGen-${hash}.zip 49 | - { hash: !Ref pFunctionHash } 50 | Environment: 51 | Variables: 52 | PemID: !Ref pPemID 53 | SecretPem: !Ref pSecretPem 54 | Host: !Ref pDomainName 55 | 56 | LambdaExecutionRole: 57 | Type: AWS::IAM::Role 58 | Properties: 59 | AssumeRolePolicyDocument: 60 | Version: '2012-10-17' 61 | Statement: 62 | - Effect: Allow 63 | Principal: 64 | Service: 65 | - lambda.amazonaws.com 66 | Action: 67 | - 'sts:AssumeRole' 68 | Path: / 69 | Policies: 70 | - PolicyName: TokenGenPolicy 71 | PolicyDocument: 72 | Version: '2012-10-17' 73 | Statement: 74 | - Effect: Allow 75 | Action: 76 | - logs:CreateLogGroup 77 | - logs:CreateLogStream 78 | - logs:PutLogEvents 79 | Resource: 80 | - !Join ["", ["arn:aws:logs:", Ref: "AWS::Region", ":", Ref: "AWS::AccountId", ":log-group:/aws/lambda/*"]] 81 | - Effect: Allow 82 | Action: 83 | - secretsmanager:GetSecretValue 84 | Resource: 85 | - !Ref pSecretPemArn -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/vod-helpers/CreateJobTemplate.template.ejs: -------------------------------------------------------------------------------- 1 | <%- templateProps.yamlTemplate %> -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/vod-helpers/InputTriggerLambda.template: -------------------------------------------------------------------------------- 1 | Description: S3 Workflow 2 | 3 | Parameters: 4 | pS3: 5 | Type: String 6 | Description: Store template and lambda package 7 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" 8 | Default: s3default 9 | pSourceFolder: 10 | Type: String 11 | Description: Store template and lambda package 12 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" 13 | Default: vod-helpers 14 | pInputS3: 15 | Type: String 16 | Description: ProjectName 17 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" 18 | Default: DefaultName 19 | pInputS3Arn: 20 | Type: String 21 | Description: Input S3 Arn 22 | Default: arn-default 23 | pOutputS3: 24 | Type: String 25 | Description: ProjectName 26 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" 27 | Default: DefaultName 28 | pOutputS3Arn: 29 | Type: String 30 | Description: Output S3 Arn 31 | Default: arn-default 32 | pFunctionName: 33 | Type: String 34 | Description: ProjectName 35 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" 36 | Default: BucketWatcher 37 | pMediaConvertTemplate: 38 | Type: String 39 | Description: MediaConvert Template Arn 40 | Default: arn-default 41 | pFunctionHash: 42 | Type: String 43 | Description: FunctionHash 44 | Default: default 45 | env: 46 | Type: String 47 | Description: The environment name. e.g. Dev, Test, or Production. 48 | Default: NONE 49 | GraphQLAPIId: 50 | Type: String 51 | Description: API ID 52 | Default: NONE 53 | GraphQLEndpoint: 54 | Type: String 55 | Description: API Endpoint URL 56 | Default: NONE 57 | pTemplateType: 58 | Type: String 59 | Description: Template type 60 | Default: NONE 61 | 62 | Resources: 63 | BucketWatcher: 64 | Type: AWS::Lambda::Function 65 | Properties: 66 | FunctionName: !Ref pFunctionName 67 | Description: Sends a notification when a new object is put into the bucket 68 | Handler: index.handler 69 | Role: !GetAtt LambdaExecutionRole.Arn 70 | Runtime: nodejs14.x 71 | Timeout: 30 72 | Code: 73 | S3Bucket: !Ref pS3 74 | S3Key: !Sub 75 | - vod-helpers/InputLambda-${hash}.zip 76 | - { hash: !Ref pFunctionHash } 77 | Environment: 78 | Variables: 79 | ARN_TEMPLATE: !Ref pMediaConvertTemplate 80 | MC_ROLE: !GetAtt MediaConvertRole.Arn 81 | OUTPUT_BUCKET: !Ref pOutputS3 82 | ENV: !Ref env 83 | GRAPHQLID: !Ref GraphQLAPIId 84 | GRAPHQLEP: !Ref GraphQLEndpoint 85 | TEMPLATE_TYPE: !Ref pTemplateType 86 | 87 | LambdaExecutionRole: 88 | Type: AWS::IAM::Role 89 | Properties: 90 | AssumeRolePolicyDocument: 91 | Version: '2012-10-17' 92 | Statement: 93 | - Effect: Allow 94 | Principal: 95 | Service: 96 | - lambda.amazonaws.com 97 | Action: 98 | - 'sts:AssumeRole' 99 | Path: / 100 | Policies: 101 | - PolicyName: S3PolicyTesting 102 | PolicyDocument: 103 | Version: '2012-10-17' 104 | Statement: 105 | - Effect: Allow 106 | Action: 107 | - logs:CreateLogGroup 108 | - logs:CreateLogStream 109 | - logs:PutLogEvents 110 | Resource: 111 | - !Join ["", ["arn:aws:logs:", Ref: "AWS::Region", ":", Ref: "AWS::AccountId", ":log-group:/aws/lambda/*"]] 112 | - Effect: Allow 113 | Action: 114 | - mediaconvert:CreateJob 115 | - mediaconvert:CreateJobTemplate 116 | - mediaconvert:CreatePreset 117 | - mediaconvert:DeleteJobTemplate 118 | - mediaconvert:DeletePreset 119 | - mediaconvert:DescribeEndpoints 120 | - mediaconvert:GetJob 121 | - mediaconvert:GetJobTemplate 122 | - mediaconvert:GetQueue 123 | - mediaconvert:GetPreset 124 | - mediaconvert:ListJobTemplates 125 | - mediaconvert:ListJobs 126 | - mediaconvert:ListQueues 127 | - mediaconvert:ListPresets 128 | - mediaconvert:UpdateJobTemplate 129 | Resource: 130 | - !Join ["", ["arn:aws:mediaconvert:", Ref: "AWS::Region", ":", Ref: "AWS::AccountId", ":*"]] 131 | - Effect: Allow 132 | Action: 133 | - iam:PassRole 134 | Resource: 135 | - !GetAtt MediaConvertRole.Arn 136 | MediaConvertRole: 137 | Type: AWS::IAM::Role 138 | Properties: 139 | AssumeRolePolicyDocument: 140 | Version: 2012-10-17 141 | Statement: 142 | - Effect: Allow 143 | Principal: 144 | Service: 145 | - "mediaconvert.amazonaws.com" 146 | Action: 147 | - sts:AssumeRole 148 | Policies: 149 | - PolicyName: !Sub "${AWS::StackName}-mediatranscode-role" 150 | PolicyDocument: 151 | Statement: 152 | - Effect: Allow 153 | Action: 154 | - s3:GetObject 155 | - s3:PutObject 156 | Resource: 157 | - !Sub "${pInputS3Arn}/*" 158 | - !Sub "${pOutputS3Arn}/*" 159 | - Effect: Allow 160 | Action: 161 | - "execute-api:Invoke" 162 | Resource: 163 | - !Join ["", ["arn:aws:execute-api:", Ref: "AWS::Region", ":", Ref: "AWS::AccountId", ":*"]] 164 | 165 | Outputs: 166 | oLambdaFunction: 167 | Value: !GetAtt BucketWatcher.Arn 168 | Description: Watching s3 buckets all day -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/vod-helpers/LambdaFunctions/CloudFrontTokenGen/index.js: -------------------------------------------------------------------------------- 1 | const url = require('url'); 2 | /* eslint-disable */ 3 | const aws = require('aws-sdk'); 4 | var globalPem; 5 | /* eslint-enable */ 6 | 7 | async function handler(event) { 8 | const response = await signPath(event.source.id); 9 | return response; 10 | } 11 | 12 | async function sign(pathURL) { 13 | const epoch = Math.floor(new Date(new Date().getTime() + (3600 * 1000)).getTime() / 1000); 14 | const mkSignPolicy = `{"Statement":[{"Resource":"${pathURL}","Condition":{"DateLessThan":{"AWS:EpochTime":${epoch}}}}]}`; 15 | if (globalPem === undefined) { 16 | await getPemKey(process.env.SecretPem); 17 | } 18 | const signer = new aws.CloudFront.Signer(process.env.PemID, globalPem); 19 | const params = {}; 20 | params.url = pathURL; 21 | params.policy = mkSignPolicy; 22 | 23 | return signer.getSignedUrl(params); 24 | } 25 | 26 | async function getPemKey(pemId) { 27 | const secretsManager = new aws.SecretsManager({ apiVersion: '2017-10-17' }); 28 | const secret = await secretsManager.getSecretValue({ SecretId: pemId }).promise(); 29 | globalPem = secret.SecretBinary; 30 | } 31 | 32 | 33 | async function signPath(id) { 34 | /* use wildcard if no specific file is specified */ 35 | const videoPath = `${id}/*`; 36 | const host = process.env.Host; 37 | const tobeSigned = url.format({ 38 | protocol: 'https:', slashes: true, host, pathname: videoPath, 39 | }); 40 | const signedUrl = await sign(tobeSigned); 41 | const urlParams = signedUrl.replace(`https://${host}/${videoPath}`, ''); 42 | 43 | return urlParams; 44 | } 45 | 46 | module.exports = { 47 | signPath, 48 | handler, 49 | }; 50 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/vod-helpers/LambdaFunctions/InputLambda/dash_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "OutputGroups": [ 3 | { 4 | "Name": "DASH ISO", 5 | "Outputs": [], 6 | "OutputGroupSettings": { 7 | "Type": "DASH_ISO_GROUP_SETTINGS", 8 | "DashIsoGroupSettings": { 9 | "SegmentLength": 15, 10 | "Destination": "s3:///output/", 11 | "FragmentLength": 2 12 | } 13 | } 14 | } 15 | ], 16 | "Inputs": [ 17 | { 18 | "AudioSelectors": { 19 | "Audio Selector 1": { 20 | "DefaultSelection": "DEFAULT" 21 | } 22 | }, 23 | "VideoSelector": {}, 24 | "TimecodeSource": "ZEROBASED", 25 | "FileInput": "s3key" 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/vod-helpers/LambdaFunctions/InputLambda/hls_dash_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "OutputGroups": [ 3 | { 4 | "Name": "DASH ISO", 5 | "Outputs": [], 6 | "OutputGroupSettings": { 7 | "Type": "DASH_ISO_GROUP_SETTINGS", 8 | "DashIsoGroupSettings": { 9 | "SegmentLength": 15, 10 | "Destination": "s3:///output/", 11 | "FragmentLength": 2 12 | } 13 | } 14 | }, 15 | { 16 | "Name": "Apple HLS", 17 | "Outputs": [], 18 | "OutputGroupSettings": { 19 | "Type": "HLS_GROUP_SETTINGS", 20 | "HlsGroupSettings": { 21 | "ManifestDurationFormat": "INTEGER", 22 | "SegmentLength": 3, 23 | "TimedMetadataId3Period": 10, 24 | "CaptionLanguageSetting": "OMIT", 25 | "Destination": "s3:///output/", 26 | "TimedMetadataId3Frame": "PRIV", 27 | "CodecSpecification": "RFC_4281", 28 | "OutputSelection": "MANIFESTS_AND_SEGMENTS", 29 | "ProgramDateTimePeriod": 600, 30 | "MinSegmentLength": 0, 31 | "DirectoryStructure": "SINGLE_DIRECTORY", 32 | "ProgramDateTime": "EXCLUDE", 33 | "SegmentControl": "SEGMENTED_FILES", 34 | "ManifestCompression": "NONE", 35 | "ClientCache": "ENABLED", 36 | "StreamInfResolution": "INCLUDE" 37 | } 38 | } 39 | } 40 | ], 41 | "AdAvailOffset": 0, 42 | "Inputs": [ 43 | { 44 | "AudioSelectors": { 45 | "Audio Selector 1": { 46 | "Offset": 0, 47 | "DefaultSelection": "DEFAULT", 48 | "ProgramSelection": 1 49 | } 50 | }, 51 | "VideoSelector": { 52 | "ColorSpace": "FOLLOW" 53 | }, 54 | "FilterEnable": "AUTO", 55 | "PsiControl": "USE_PSI", 56 | "FilterStrength": 0, 57 | "DeblockFilter": "DISABLED", 58 | "DenoiseFilter": "DISABLED", 59 | "TimecodeSource": "ZEROBASED", 60 | "FileInput": "s3key" 61 | } 62 | ] 63 | } -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/vod-helpers/LambdaFunctions/InputLambda/hls_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "OutputGroups": [ 3 | { 4 | "Name": "Apple HLS", 5 | "Outputs": [], 6 | "OutputGroupSettings": { 7 | "Type": "HLS_GROUP_SETTINGS", 8 | "HlsGroupSettings": { 9 | "ManifestDurationFormat": "INTEGER", 10 | "SegmentLength": 3, 11 | "TimedMetadataId3Period": 10, 12 | "CaptionLanguageSetting": "OMIT", 13 | "Destination": "s3:///output/", 14 | "TimedMetadataId3Frame": "PRIV", 15 | "CodecSpecification": "RFC_4281", 16 | "OutputSelection": "MANIFESTS_AND_SEGMENTS", 17 | "ProgramDateTimePeriod": 600, 18 | "MinSegmentLength": 0, 19 | "DirectoryStructure": "SINGLE_DIRECTORY", 20 | "ProgramDateTime": "EXCLUDE", 21 | "SegmentControl": "SEGMENTED_FILES", 22 | "ManifestCompression": "NONE", 23 | "ClientCache": "ENABLED", 24 | "StreamInfResolution": "INCLUDE" 25 | } 26 | } 27 | } 28 | ], 29 | "AdAvailOffset": 0, 30 | "Inputs": [ 31 | { 32 | "AudioSelectors": { 33 | "Audio Selector 1": { 34 | "Offset": 0, 35 | "DefaultSelection": "DEFAULT", 36 | "ProgramSelection": 1 37 | } 38 | }, 39 | "VideoSelector": { 40 | "ColorSpace": "FOLLOW" 41 | }, 42 | "FilterEnable": "AUTO", 43 | "PsiControl": "USE_PSI", 44 | "FilterStrength": 0, 45 | "DeblockFilter": "DISABLED", 46 | "DenoiseFilter": "DISABLED", 47 | "TimecodeSource": "ZEROBASED", 48 | "FileInput": "s3key" 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/vod-helpers/LambdaFunctions/InputLambda/index.js: -------------------------------------------------------------------------------- 1 | // Load the AWS SDK for Node.js 2 | // eslint-disable-next-line import/no-extraneous-dependencies 3 | /* eslint-disable */ 4 | const AWS = require('aws-sdk'); 5 | /* eslint-enable */ 6 | const hlsJobSettings = require('./hls_settings.json'); 7 | const dashJobSettings = require('./dash_settings.json'); 8 | const hlsdashJobSettings = require('./hls_dash_settings.json'); 9 | // Set the region 10 | 11 | exports.handler = async (event) => { 12 | AWS.config.update({ region: event.awsRegion }); 13 | console.log(event); 14 | if (event.Records[0].eventName.includes('ObjectCreated')) { 15 | await createJob(event.Records[0].s3); 16 | const response = { 17 | statusCode: 200, 18 | body: JSON.stringify(`Transcoding your file: ${event.Records[0].s3.object.key}`), 19 | }; 20 | return response; 21 | } 22 | }; 23 | 24 | // Function to submit job to Elemental MediaConvert 25 | async function createJob(eventObject) { 26 | let mcClient = new AWS.MediaConvert(); 27 | if (!AWS.config.mediaconvert) { 28 | try { 29 | const endpoints = await mcClient.describeEndpoints().promise(); 30 | AWS.config.mediaconvert = { endpoint: endpoints.Endpoints[0].Url }; 31 | // Override so config applies 32 | mcClient = new AWS.MediaConvert(); 33 | } catch (e) { 34 | console.log(e); 35 | return; 36 | } 37 | } 38 | 39 | const queueParams = { 40 | Name: 'Default', /* required */ 41 | }; 42 | const AddedKey = eventObject.object.key; 43 | // Get the name of the file passed in the event without the extension 44 | const FileName = AddedKey.split('.').slice(0, -1).join('.'); 45 | const Bucket = eventObject.bucket.name; 46 | const outputBucketName = process.env.OUTPUT_BUCKET; 47 | const hlsRendition = 'hls'; 48 | const dashRendition = 'dash'; 49 | 50 | // Set the output to have the filename (without extension) as a folder depending 51 | // on the type of rendition this is required as the job json for HLS differs from DASH 52 | const outputType = process.env.TEMPLATE_TYPE; 53 | 54 | let jobSettings = {}; 55 | 56 | const outputTypeList = outputType.split(','); 57 | 58 | 59 | if (outputTypeList.length === 1) { 60 | if (outputTypeList[0] === 'HLS') { 61 | hlsJobSettings.OutputGroups[0].OutputGroupSettings.HlsGroupSettings.Destination = `s3://${outputBucketName}/${FileName}/`; 62 | hlsJobSettings.Inputs[0].FileInput = `s3://${Bucket}/${decodeURIComponent(AddedKey.replace(/\+/g, ' '))}`; 63 | jobSettings = hlsJobSettings; 64 | } else if (outputTypeList[0] === 'DASH') { 65 | dashJobSettings.OutputGroups[0].OutputGroupSettings.DashIsoGroupSettings.Destination = `s3://${outputBucketName}/${FileName}/`; 66 | dashJobSettings.Inputs[0].FileInput = `s3://${Bucket}/${decodeURIComponent(AddedKey.replace(/\+/g, ' '))}`; 67 | jobSettings = dashJobSettings; 68 | } 69 | } else { 70 | for (let counter = 0; counter < outputTypeList.length; counter++) { 71 | if (outputTypeList[counter] === 'HLS') { 72 | // iterate through the outputGroups and set the appropriate output file paths 73 | const outputGroupsLengths = hlsdashJobSettings.OutputGroups.length; 74 | 75 | for (let outputGroupsLengthsCounter = 0; 76 | outputGroupsLengthsCounter < outputGroupsLengths; outputGroupsLengthsCounter++) { 77 | if (hlsdashJobSettings.OutputGroups[outputGroupsLengthsCounter].OutputGroupSettings.Type.includes('HLS')) { 78 | hlsdashJobSettings.OutputGroups[outputGroupsLengthsCounter].OutputGroupSettings.HlsGroupSettings.Destination = `s3://${outputBucketName}/${FileName}/${hlsRendition}/`; 79 | } 80 | } 81 | } 82 | 83 | if (outputTypeList[counter] === 'DASH') { 84 | // iterate through the outputGroups and set the appropriate output file paths 85 | const outputGroupsLengths = hlsdashJobSettings.OutputGroups.length; 86 | 87 | for (let outputGroupsLengthsCounter = 0; 88 | outputGroupsLengthsCounter < outputGroupsLengths; outputGroupsLengthsCounter++) { 89 | if (hlsdashJobSettings.OutputGroups[outputGroupsLengthsCounter].OutputGroupSettings.Type.includes('DASH')) { 90 | hlsdashJobSettings.OutputGroups[outputGroupsLengthsCounter].OutputGroupSettings.DashIsoGroupSettings.Destination = `s3://${outputBucketName}/${FileName}/${dashRendition}/`; 91 | } 92 | } 93 | } 94 | } 95 | 96 | hlsdashJobSettings.Inputs[0].FileInput = `s3://${Bucket}/${decodeURIComponent(AddedKey.replace(/\+/g, ' '))}`; 97 | 98 | jobSettings = hlsdashJobSettings; 99 | } 100 | 101 | 102 | let queueARN = ''; 103 | if (process.env.QUEUE_ARN) { 104 | queueARN = process.env.QUEUE_ARN; 105 | } else { 106 | const q = await mcClient.getQueue(queueParams, (err, data) => { 107 | if (err) console.log(err, err.stack); // an error occurred 108 | else console.log(data); 109 | }).promise(); 110 | queueARN = q.Queue.Arn; 111 | } 112 | 113 | const jobParams = { 114 | JobTemplate: process.env.ARN_TEMPLATE, 115 | Queue: queueARN, 116 | UserMetadata: {}, 117 | Role: process.env.MC_ROLE, 118 | Settings: jobSettings, 119 | Tags: { 'amplify-video': 'amplify-video' }, 120 | }; 121 | await mcClient.createJob(jobParams).promise(); 122 | } 123 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/vod-helpers/LambdaFunctions/InputLambda/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "OutputGroups": [ 3 | { 4 | "Name": "Apple HLS", 5 | "Outputs": [], 6 | "OutputGroupSettings": { 7 | "Type": "HLS_GROUP_SETTINGS", 8 | "HlsGroupSettings": { 9 | "ManifestDurationFormat": "INTEGER", 10 | "SegmentLength": 3, 11 | "TimedMetadataId3Period": 10, 12 | "CaptionLanguageSetting": "OMIT", 13 | "Destination": "s3:///output/", 14 | "TimedMetadataId3Frame": "PRIV", 15 | "CodecSpecification": "RFC_4281", 16 | "OutputSelection": "MANIFESTS_AND_SEGMENTS", 17 | "ProgramDateTimePeriod": 600, 18 | "MinSegmentLength": 0, 19 | "DirectoryStructure": "SINGLE_DIRECTORY", 20 | "ProgramDateTime": "EXCLUDE", 21 | "SegmentControl": "SEGMENTED_FILES", 22 | "ManifestCompression": "NONE", 23 | "ClientCache": "ENABLED", 24 | "StreamInfResolution": "INCLUDE" 25 | } 26 | } 27 | } 28 | ], 29 | "AdAvailOffset": 0, 30 | "Inputs": [ 31 | { 32 | "AudioSelectors": { 33 | "Audio Selector 1": { 34 | "Offset": 0, 35 | "DefaultSelection": "DEFAULT", 36 | "ProgramSelection": 1 37 | } 38 | }, 39 | "VideoSelector": { 40 | "ColorSpace": "FOLLOW" 41 | }, 42 | "FilterEnable": "AUTO", 43 | "PsiControl": "USE_PSI", 44 | "FilterStrength": 0, 45 | "DeblockFilter": "DISABLED", 46 | "DenoiseFilter": "DISABLED", 47 | "TimecodeSource": "ZEROBASED", 48 | "FileInput": "s3key" 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/vod-helpers/LambdaFunctions/MediaConvertStatusLambda/index.js: -------------------------------------------------------------------------------- 1 | console.log('Loading function'); 2 | 3 | exports.handler = async (event, context) => { 4 | // console.log('Received event:', JSON.stringify(event, null, 2)); 5 | const message = event.Records[0].Sns.Message; 6 | console.log('From SNS:', message); 7 | console.log('context ', context); 8 | return message; 9 | }; 10 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/vod-helpers/LambdaFunctions/OutputLambda/index.js.ejs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const AWS = require('aws-sdk'); 3 | /* eslint-enable */ 4 | const s3 = new AWS.S3({}); 5 | 6 | /* eslint-disable */ 7 | exports.handler = function (event, context) { 8 | /* eslint-enable */ 9 | 10 | /* 11 | Function that triggers on the output bucket. 12 | 13 | event.Records contains an array of S3 records that you can take action on. 14 | */ 15 | 16 | <% if (!props.contentDeliveryNetwork.enableDistribution) { %> 17 | event.Records.forEach((s3Record) => { 18 | console.log(s3Record.s3.object.key); 19 | const objectKey = s3Record.s3.object.key; 20 | const bucketName = s3Record.s3.bucket.name; 21 | const params = { 22 | Bucket: bucketName, 23 | Key: objectKey, 24 | ACL: 'public-read', 25 | }; 26 | s3.putObjectAcl(params, (err, data) => { 27 | if (err) { 28 | console.log(err); 29 | } else { 30 | console.log(data); 31 | } 32 | }); 33 | }); 34 | <% } %> 35 | }; -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/vod-helpers/LambdaFunctions/SetupTriggerLambda/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const AWS = require('aws-sdk'); 3 | /* eslint-enable */ 4 | const s3 = new AWS.S3({}); 5 | 6 | /* eslint-disable */ 7 | exports.handler = function (event, context) { 8 | /* eslint-enable */ 9 | const config = event.ResourceProperties; 10 | 11 | const responseData = {}; 12 | 13 | switch (event.RequestType) { 14 | case 'Create': 15 | createNotifications(config); 16 | break; 17 | case 'Update': 18 | createNotifications(config); 19 | break; 20 | case 'Delete': 21 | deleteNotifications(config); 22 | break; 23 | default: 24 | console.log('No changes'); 25 | } 26 | 27 | const response = sendResponse(event, context, 'SUCCESS', responseData); 28 | console.log('CFN STATUS:: ', response); 29 | }; 30 | 31 | function sendResponse(event, context, responseStatus, responseData) { 32 | const responseBody = JSON.stringify({ 33 | Status: responseStatus, 34 | Reason: `See the details in CloudWatch Log Stream: ${context.logStreamName}`, 35 | PhysicalResourceId: context.logStreamName, 36 | StackId: event.StackId, 37 | RequestId: event.RequestId, 38 | LogicalResourceId: event.LogicalResourceId, 39 | Data: responseData, 40 | }); 41 | 42 | console.log('RESPONSE BODY:\n', responseBody); 43 | 44 | const https = require('https'); 45 | const url = require('url'); 46 | 47 | const parsedUrl = url.parse(event.ResponseURL); 48 | const options = { 49 | hostname: parsedUrl.hostname, 50 | port: 443, 51 | path: parsedUrl.path, 52 | method: 'PUT', 53 | headers: { 54 | 'content-type': '', 55 | 'content-length': responseBody.length, 56 | }, 57 | }; 58 | 59 | console.log('SENDING RESPONSE...\n'); 60 | 61 | const request = https.request(options, (response) => { 62 | console.log(`STATUS: ${response.statusCode}`); 63 | console.log(`HEADERS: ${JSON.stringify(response.headers)}`); 64 | // Tell AWS Lambda that the function execution is done 65 | context.done(); 66 | }); 67 | 68 | request.on('error', (error) => { 69 | console.log(`sendResponse Error:${error}`); 70 | // Tell AWS Lambda that the function execution is done 71 | context.done(); 72 | }); 73 | 74 | // write data to request body 75 | request.write(responseBody); 76 | request.end(); 77 | } 78 | 79 | function createNotifications(config) { 80 | const LambdaFunctionConfig = []; 81 | 82 | Object.values(config.TriggerSuffix).forEach((suffix) => { 83 | const suffixConfigure = { 84 | Events: ['s3:ObjectCreated:*'], 85 | LambdaFunctionArn: config.IngestArn, 86 | Filter: { 87 | Key: { 88 | FilterRules: [{ 89 | Name: 'suffix', 90 | Value: suffix, 91 | }], 92 | }, 93 | }, 94 | }; 95 | LambdaFunctionConfig.push(suffixConfigure); 96 | }); 97 | const params = { 98 | Bucket: config.BucketName, 99 | NotificationConfiguration: { 100 | }, 101 | }; 102 | params.NotificationConfiguration.LambdaFunctionConfigurations = LambdaFunctionConfig; 103 | 104 | s3.putBucketNotificationConfiguration(params, (err, data) => { 105 | if (err) console.log(err, err.stack); 106 | else console.log(data); 107 | }); 108 | } 109 | 110 | function deleteNotifications(config) { 111 | console.log(config); 112 | // Do nothing for now 113 | } 114 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/vod-helpers/OutputTriggerLambda.template: -------------------------------------------------------------------------------- 1 | Description: S3 Workflow 2 | 3 | Parameters: 4 | pS3: 5 | Type: String 6 | Description: Store template and lambda package 7 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" 8 | Default: s3default 9 | pSourceFolder: 10 | Type: String 11 | Description: Store template and lambda package 12 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" 13 | Default: vod-helpers 14 | pOutputS3: 15 | Type: String 16 | Description: ProjectName 17 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" 18 | Default: DefaultName 19 | pOutputS3Arn: 20 | Type: String 21 | Description: Output S3 Arn 22 | Default: arn-default 23 | pFunctionName: 24 | Type: String 25 | Description: ProjectName 26 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" 27 | Default: BucketWatcher 28 | pFunctionHash: 29 | Type: String 30 | Description: FunctionHash 31 | Default: default 32 | env: 33 | Type: String 34 | Description: The environment name. e.g. Dev, Test, or Production. 35 | Default: NONE 36 | GraphQLAPIId: 37 | Type: String 38 | Description: API ID 39 | Default: NONE 40 | GraphQLEndpoint: 41 | Type: String 42 | Description: API Endpoint URL 43 | Default: NONE 44 | 45 | Resources: 46 | BucketWatcher: 47 | Type: AWS::Lambda::Function 48 | Properties: 49 | FunctionName: !Ref pFunctionName 50 | Description: Sends a notification when a new object is put into the bucket 51 | Handler: index.handler 52 | Role: !GetAtt LambdaExecutionRole.Arn 53 | Runtime: nodejs14.x 54 | Timeout: 30 55 | Environment: 56 | Variables: 57 | ENV: !Ref env 58 | GRAPHQLID: !Ref GraphQLAPIId 59 | GRAPHQLEP: !Ref GraphQLEndpoint 60 | Code: 61 | S3Bucket: !Ref pS3 62 | S3Key: !Sub 63 | - vod-helpers/OutputLambda-${hash}.zip 64 | - { hash: !Ref pFunctionHash } 65 | 66 | LambdaExecutionRole: 67 | Type: AWS::IAM::Role 68 | Properties: 69 | AssumeRolePolicyDocument: 70 | Version: '2012-10-17' 71 | Statement: 72 | - Effect: Allow 73 | Principal: 74 | Service: 75 | - lambda.amazonaws.com 76 | Action: 77 | - 'sts:AssumeRole' 78 | Path: / 79 | Policies: 80 | - PolicyName: S3PolicyTesting 81 | PolicyDocument: 82 | Version: '2012-10-17' 83 | Statement: 84 | - Effect: Allow 85 | Action: 86 | - logs:CreateLogGroup 87 | - logs:CreateLogStream 88 | - logs:PutLogEvents 89 | Resource: 90 | - !Join ["", ["arn:aws:logs:", Ref: "AWS::Region", ":", Ref: "AWS::AccountId", ":log-group:/aws/lambda/*"]] 91 | - Effect: Allow 92 | Action: 93 | - s3:PutObjectAcl 94 | Resource: 95 | - !Sub 96 | - ${bucketArn}/* 97 | - {bucketArn: !Ref pOutputS3Arn} 98 | 99 | Outputs: 100 | oLambdaFunction: 101 | Value: !GetAtt BucketWatcher.Arn 102 | Description: Watching s3 buckets all day -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/vod-helpers/S3InputBucket.template.ejs: -------------------------------------------------------------------------------- 1 | Description: S3 Workflow 2 | 3 | Parameters: 4 | pBucketName: 5 | Type: String 6 | Description: ProjectName 7 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" 8 | Default: DefaultName 9 | <% if (props.permissions && props.permissions.permissionSchema.includes('any')) { %> 10 | pPolicyName: 11 | Type: String 12 | Description: Policy name for allowing uploads from all auth users 13 | Default: S3UploadPolicy\ 14 | <% } -%> 15 | authRoleName: 16 | Type: String 17 | Description: Name of authRole 18 | Default: NONE 19 | 20 | Resources: 21 | InputBucket: 22 | Type: AWS::S3::Bucket 23 | DeletionPolicy: Retain 24 | Properties: 25 | BucketName: !Ref pBucketName 26 | CorsConfiguration: 27 | CorsRules: 28 | - AllowedHeaders: ['*'] 29 | AllowedMethods: ['GET', 'HEAD', 'PUT', 'POST', 'DELETE'] 30 | AllowedOrigins: ['*'] 31 | ExposedHeaders: ['x-amz-server-side-encryption', 'x-amz-request-id', 'x-amz-id-2', 'ETag'] 32 | MaxAge: '3000' 33 | <% if (props.permissions && props.permissions.permissionSchema.includes('any')) { %> 34 | UploadPolicy: 35 | Type: AWS::IAM::Policy 36 | Properties: 37 | PolicyName: !Ref pPolicyName 38 | Roles: 39 | - !Ref authRoleName 40 | PolicyDocument: 41 | Version: '2012-10-17' 42 | Statement: 43 | - Effect: Allow 44 | Action: 45 | - s3:PutObject 46 | Resource: !Sub 47 | - "${bucketArn}/*" 48 | - { bucketArn: !GetAtt InputBucket.Arn } 49 | <% } -%> 50 | 51 | Outputs: 52 | oInputBucketArn: 53 | Value: !GetAtt InputBucket.Arn 54 | Description: BucketArn 55 | oInputBucketName: 56 | Value: !Ref InputBucket 57 | Description: S3 Bucket Created -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/vod-helpers/S3OutputBucket.template: -------------------------------------------------------------------------------- 1 | Description: S3 Workflow 2 | 3 | Parameters: 4 | pBucketName: 5 | Type: String 6 | Description: ProjectName 7 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" 8 | Default: DefaultName 9 | pCloudfrontEnabled: 10 | Type: String 11 | Description: Check if CF is enabled 12 | Default: "true" 13 | authRoleName: 14 | Type: String 15 | Description: Name of authRole 16 | Default: NONE 17 | 18 | Conditions: 19 | CreateCloudfront: !Equals [!Ref pCloudfrontEnabled, "true"] 20 | 21 | Resources: 22 | OutputBucket: 23 | Type: AWS::S3::Bucket 24 | DeletionPolicy: Retain 25 | Properties: 26 | BucketName: !Ref pBucketName 27 | CorsConfiguration: 28 | CorsRules: 29 | - AllowedHeaders: ['*'] 30 | AllowedMethods: ['GET', 'HEAD', 'PUT', 'POST', 'DELETE'] 31 | AllowedOrigins: ['*'] 32 | ExposedHeaders: ['x-amz-server-side-encryption', 'x-amz-request-id', 'x-amz-id-2', 'ETag'] 33 | MaxAge: '3000' 34 | 35 | S3AuthGet: 36 | Type: AWS::IAM::Policy 37 | Properties: 38 | PolicyName: AuthGet 39 | Roles: 40 | - !Ref authRoleName 41 | PolicyDocument: 42 | Version: 2012-10-17 43 | Statement: 44 | - Effect: Allow 45 | Action: 46 | - "s3:GetObject" 47 | Resource: 48 | - !Join 49 | - '' 50 | - - 'arn:aws:s3:::' 51 | - !Ref OutputBucket 52 | - "/${cognito-identity.amazonaws.com:sub}/*" 53 | 54 | OriginAccessIdentity: 55 | Type: "AWS::CloudFront::CloudFrontOriginAccessIdentity" 56 | Condition: CreateCloudfront 57 | Properties: 58 | CloudFrontOriginAccessIdentityConfig: 59 | Comment: !Sub "OAI created by ${AWS::StackName} in ${AWS::Region}" 60 | 61 | S3Policy: 62 | Type: AWS::S3::BucketPolicy 63 | Condition: CreateCloudfront 64 | Properties: 65 | Bucket: !Ref pBucketName 66 | PolicyDocument: 67 | Statement: 68 | - Action: 69 | - "s3:getObject" 70 | Effect: Allow 71 | Resource: !Sub "arn:aws:s3:::${OutputBucket}/*" 72 | Principal: 73 | AWS: !Sub "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${OriginAccessIdentity}" 74 | 75 | Outputs: 76 | oOutputBucketArn: 77 | Value: !GetAtt OutputBucket.Arn 78 | Description: BucketArn 79 | oOutputBucketName: 80 | Value: !Ref OutputBucket 81 | Description: S3 Bucket Created 82 | oOriginAccessIdentity: 83 | Condition: CreateCloudfront 84 | Value: !Ref OriginAccessIdentity 85 | Description: Origin Access Identity for Cloudfront 86 | oOutputUrl: 87 | Value: !GetAtt OutputBucket.RegionalDomainName 88 | Description: URL for the bucket -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/vod-helpers/S3TriggerSetup.template: -------------------------------------------------------------------------------- 1 | Description: S3 Workflow 2 | 3 | Parameters: 4 | pS3: 5 | Type: String 6 | Description: Store template and lambda package 7 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" 8 | Default: amazonbooth 9 | pSourceFolder: 10 | Type: String 11 | Description: Store template and lambda package 12 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" 13 | Default: vod-helpers 14 | pInputS3: 15 | Type: String 16 | Description: ProjectName 17 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" 18 | Default: DefaultName 19 | pInputS3Arn: 20 | Type: String 21 | Description: Input S3 Arn 22 | Default: arn-default 23 | pOutputS3: 24 | Type: String 25 | Description: ProjectName 26 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" 27 | Default: DefaultName 28 | pOutputS3Arn: 29 | Type: String 30 | Description: Input S3 Arn 31 | Default: arn-default 32 | pInputTriggerLambda: 33 | Type: String 34 | Description: Arn for lambda for triggering 35 | Default: arn-default 36 | pOutputTriggerLambda: 37 | Type: String 38 | Description: Arn for lambda for triggering 39 | Default: arn-default 40 | pFunctionName: 41 | Type: String 42 | Description: Name of function 43 | Default: arn-default 44 | pFunctionHash: 45 | Type: String 46 | Description: FunctionHash 47 | Default: default 48 | 49 | 50 | Resources: 51 | rS3InputTriggerPermissions: 52 | Type: AWS::Lambda::Permission 53 | Properties: 54 | FunctionName: !Ref pInputTriggerLambda 55 | Action: lambda:InvokeFunction 56 | Principal: s3.amazonaws.com 57 | SourceAccount: !Ref AWS::AccountId 58 | rS3OutputTriggerPermissions: 59 | Type: AWS::Lambda::Permission 60 | Properties: 61 | FunctionName: !Ref pOutputTriggerLambda 62 | Action: lambda:InvokeFunction 63 | Principal: s3.amazonaws.com 64 | SourceAccount: !Ref AWS::AccountId 65 | rCreateSetupTriggerLambda: 66 | Type: AWS::Lambda::Function 67 | Properties: 68 | FunctionName: !Ref pFunctionName 69 | Description: Sends a notification when a new object is put into the bucket 70 | Handler: index.handler 71 | Role: !GetAtt rSetupTriggerLambdaRole.Arn 72 | Runtime: nodejs14.x 73 | Timeout: 30 74 | Code: 75 | S3Bucket: !Ref pS3 76 | S3Key: !Sub 77 | - vod-helpers/SetupTriggerLambda-${hash}.zip 78 | - { hash: !Ref pFunctionHash } 79 | 80 | rCreateInputTrigger: 81 | Type: "Custom::InputTrigger" 82 | Properties: 83 | ServiceToken: !GetAtt rCreateSetupTriggerLambda.Arn 84 | BucketName: !Ref pInputS3 85 | IngestArn: !Ref pInputTriggerLambda 86 | BucketFunction: "Input" 87 | TriggerSuffix: 88 | - ".mpg" 89 | - ".mp4" 90 | - ".m2ts" 91 | - ".mov" 92 | 93 | rCreateOutputTrigger: 94 | Type: "Custom::OutputTrigger" 95 | Properties: 96 | ServiceToken: !GetAtt rCreateSetupTriggerLambda.Arn 97 | BucketName: !Ref pOutputS3 98 | IngestArn: !Ref pOutputTriggerLambda 99 | BucketFunction: "Output" 100 | TriggerSuffix: 101 | - ".m3u8" 102 | - ".ts" 103 | 104 | rSetupTriggerLambdaRole: 105 | Type: AWS::IAM::Role 106 | Properties: 107 | AssumeRolePolicyDocument: 108 | Version: 2012-10-17 109 | Statement: 110 | - 111 | Effect: Allow 112 | Principal: 113 | Service: 114 | - lambda.amazonaws.com 115 | Action: 116 | - sts:AssumeRole 117 | Policies: 118 | - 119 | PolicyName: !Sub "${AWS::StackName}-internal-trigger-role" 120 | PolicyDocument: 121 | Statement: 122 | - 123 | Effect: Allow 124 | Action: 125 | - s3:PutBucketNotification 126 | - s3:PutObject 127 | - s3:putObjectAcl 128 | Resource: 129 | - !Ref pInputS3Arn 130 | - !Ref pOutputS3Arn 131 | - 132 | Effect: Allow 133 | Action: 134 | - logs:CreateLogGroup 135 | - logs:CreateLogStream 136 | - logs:DescribeLogStreams 137 | - logs:PutLogEvents 138 | Resource: 139 | - arn:aws:logs:*:*:* -------------------------------------------------------------------------------- /provider-utils/awscloudformation/cloudformation-templates/vod-helpers/SnsSetup.template.ejs: -------------------------------------------------------------------------------- 1 | Description: 2 | "MediaConvert notifications sent to SNS and then forwarded to Lambda" 3 | 4 | Parameters: 5 | env: 6 | Type: String 7 | Description: The environment name. e.g. Dev, Test, or Production. 8 | Default: NONE 9 | pS3: 10 | Type: String 11 | Description: Store template and lambda package 12 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" 13 | Default: amazonbooth 14 | pSourceFolder: 15 | Type: String 16 | Description: Store template and lambda package 17 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" 18 | Default: vod-helpers 19 | pFunctionName: 20 | Type: String 21 | Description: Name of function 22 | Default: arn-default 23 | pFunctionHash: 24 | Type: String 25 | Description: FunctionHash 26 | Default: default 27 | pSnsTopicName: 28 | Type: String 29 | Description: Topic Name 30 | Default: default 31 | <% if (props.parameters && props.parameters.GraphQLAPIId) { -%> 32 | GraphQLAPIId: 33 | Type: String 34 | Description: GraphQL API Id 35 | GraphQLEndpoint: 36 | Type: String 37 | Description: GraphQL endpoint 38 | <% } -%> 39 | 40 | 41 | Resources: 42 | 43 | MediaConvertNotificationsSNS: 44 | Type: AWS::SNS::Topic 45 | 46 | MediaConvertEventsRule: 47 | Type: AWS::Events::Rule 48 | Properties: 49 | Description: "Event rule for MediaConvert" 50 | EventPattern: 51 | source: 52 | - aws.mediaconvert 53 | detail-type: 54 | - "MediaConvert Job State Change" 55 | State: ENABLED 56 | Targets: 57 | - Arn: !Ref MediaConvertNotificationsSNS 58 | Id: MediaConvertNotificationsSNS 59 | 60 | rMediaConvertNotificationsSNSPolicy: 61 | Type: 'AWS::SNS::TopicPolicy' 62 | Properties: 63 | Topics: 64 | - !Ref MediaConvertNotificationsSNS 65 | PolicyDocument: 66 | Version: '2012-10-17' 67 | Statement: 68 | - Effect: Allow 69 | Sid: "1" 70 | Action: 'sns:Publish' 71 | Resource: !Ref MediaConvertNotificationsSNS 72 | Principal: 73 | AWS: '*' 74 | Condition: 75 | ArnLike: 76 | AWS:SourceArn: !Sub 'arn:aws:*:*:${AWS::AccountId}:*' 77 | - Effect: Allow 78 | Sid: "2" 79 | Action: "sns:Publish" 80 | Principal: 81 | Service : 'events.amazonaws.com' 82 | Resource: !Ref MediaConvertNotificationsSNS 83 | <% if (props.sns.snsFunction) { -%> 84 | rMediaconvertStatusLambda: 85 | Type: AWS::Lambda::Function 86 | Properties: 87 | FunctionName: !Ref pFunctionName 88 | Description: Invoked on MediaConvert status events 89 | Handler: index.handler 90 | Role: !GetAtt rMediaconvertStatusLambdaRole.Arn 91 | Runtime: nodejs14.x 92 | Timeout: 30 93 | Code: 94 | S3Bucket: !Ref pS3 95 | S3Key: !Sub 96 | - vod-helpers/MediaConvertStatusLambda-${hash}.zip 97 | - { hash: !Ref pFunctionHash } 98 | <% if (props.parameters && props.parameters.GraphQLAPIId) { -%> 99 | Environment: 100 | Variables: 101 | GraphQLAPIId: !Ref GraphQLAPIId 102 | GraphQLEP: !Ref GraphQLEndpoint 103 | <% } -%> 104 | 105 | rMediaconvertStatusLambdaRole: 106 | Type: AWS::IAM::Role 107 | Properties: 108 | AssumeRolePolicyDocument: 109 | Version: 2012-10-17 110 | Statement: 111 | - 112 | Effect: Allow 113 | Principal: 114 | Service: 115 | - lambda.amazonaws.com 116 | Action: 117 | - sts:AssumeRole 118 | Policies: 119 | - 120 | PolicyName: !Sub "${AWS::AccountId}-mediaconvert-status-processing-role" 121 | PolicyDocument: 122 | Statement: 123 | - 124 | Effect: Allow 125 | Action: 126 | - logs:CreateLogGroup 127 | - logs:CreateLogStream 128 | - logs:DescribeLogStreams 129 | - logs:PutLogEvents 130 | Resource: 131 | - arn:aws:logs:*:*:* 132 | 133 | rSNSLambdaPermissions: 134 | Type: AWS::Lambda::Permission 135 | Properties: 136 | FunctionName: !Ref rMediaconvertStatusLambda 137 | Action: lambda:InvokeFunction 138 | Principal: sns.amazonaws.com 139 | SourceArn: !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:* 140 | 141 | rSNSLambdaSubscription: 142 | Type: AWS::SNS::Subscription 143 | Properties: 144 | Protocol: lambda 145 | Endpoint: !GetAtt rMediaconvertStatusLambda.Arn 146 | TopicArn: !Ref MediaConvertNotificationsSNS 147 | 148 | <% } -%> -------------------------------------------------------------------------------- /provider-utils/awscloudformation/default-values/livestream-defaults.json: -------------------------------------------------------------------------------- 1 | { 2 | "advanced": { 3 | "advancedChoice": false, 4 | "gopSize": "1", 5 | "gopPerSegment": "2", 6 | "segsPerPlist": "3" 7 | }, 8 | "mediaLive": { 9 | "securityGroup": "0.0.0.0/0", 10 | "ingestType": "RTMP_PUSH", 11 | "encodingProfile": "FULL", 12 | "autoStart": "YES" 13 | }, 14 | "mediaPackage": { 15 | "endpoints": "HLS,DASH", 16 | "startOverWindow": "86400" 17 | }, 18 | "mediaStorage": { 19 | "storageType": "mStore" 20 | }, 21 | "cloudFront": { 22 | "enableDistrubtion": "YES", 23 | "priceClass": "PriceClass_100", 24 | "sBucketLogs": "", 25 | "sLogPrefix": "cf_logs/" 26 | } 27 | } -------------------------------------------------------------------------------- /provider-utils/awscloudformation/default-values/vod-defaults.json: -------------------------------------------------------------------------------- 1 | { 2 | "encoder": { 3 | "encodingTemplate": "Amplify_Video_HLS.json" 4 | }, 5 | "contentDeliveryNetwork": { 6 | "enableDistribution": true 7 | }, 8 | "contentManagementSystem": { 9 | "enableCMS":true, 10 | "overrideSchema":true 11 | }, 12 | "snsTopic":{ 13 | "createSnsTopic" : false, 14 | "enableSnsFunction" : false 15 | } 16 | } -------------------------------------------------------------------------------- /provider-utils/awscloudformation/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const chalk = require('chalk'); 3 | const { buildTemplates } = require('./utils/video-staging'); 4 | const { liveStartStop } = require('./utils/livestream-startstop'); 5 | 6 | let serviceMetadata; 7 | 8 | async function addResource(context, service, options) { 9 | serviceMetadata = context.amplify.readJsonFile(`${__dirname}/../supported-services.json`)[service]; 10 | const targetDir = context.amplify.pathManager.getBackendDirPath(); 11 | const { serviceWalkthroughFilename, defaultValuesFilename } = serviceMetadata; 12 | const serviceWalkthroughSrc = `${__dirname}/service-walkthroughs/${serviceWalkthroughFilename}`; 13 | const { serviceQuestions } = require(serviceWalkthroughSrc); 14 | const result = await serviceQuestions(context, options, defaultValuesFilename); 15 | context.amplify.updateamplifyMetaAfterResourceAdd( 16 | 'video', 17 | result.shared.resourceName, 18 | options, 19 | ); 20 | if (!fs.existsSync(`${targetDir}/video/${result.shared.resourceName}/`)) { 21 | fs.mkdirSync(`${targetDir}/video/${result.shared.resourceName}/`, { recursive: true }); 22 | } 23 | if (result.parameters !== undefined) { 24 | await fs.writeFileSync(`${targetDir}/video/${result.shared.resourceName}/parameters.json`, JSON.stringify(result.parameters, null, 4)); 25 | } 26 | await fs.writeFileSync(`${targetDir}/video/${result.shared.resourceName}/props.json`, JSON.stringify(result, null, 4)); 27 | await buildTemplates(context, result); 28 | } 29 | 30 | async function updateResource(context, service, options, resourceName) { 31 | serviceMetadata = context.amplify.readJsonFile(`${__dirname}/../supported-services.json`)[service]; 32 | const targetDir = context.amplify.pathManager.getBackendDirPath(); 33 | const { serviceWalkthroughFilename, defaultValuesFilename } = serviceMetadata; 34 | const serviceWalkthroughSrc = `${__dirname}/service-walkthroughs/${serviceWalkthroughFilename}`; 35 | const { serviceQuestions } = require(serviceWalkthroughSrc); 36 | const result = await serviceQuestions(context, options, defaultValuesFilename, resourceName); 37 | if (result.parameters !== undefined) { 38 | await fs.writeFileSync(`${targetDir}/video/${result.shared.resourceName}/parameters.json`, JSON.stringify(result.parameters, null, 4)); 39 | } 40 | await fs.writeFileSync(`${targetDir}/video/${result.shared.resourceName}/props.json`, JSON.stringify(result, null, 4)); 41 | await buildTemplates(context, result); 42 | context.print.success(`Successfully updated ${result.shared.resourceName}`); 43 | } 44 | 45 | async function livestreamStartStop(context, service, options, resourceName, start) { 46 | const { amplify } = context; 47 | const amplifyMeta = amplify.getProjectMeta(); 48 | 49 | if (amplifyMeta.video[resourceName].output) { 50 | const resourceId = amplifyMeta.video[resourceName].output.oMediaLiveChannelId; 51 | await liveStartStop(context, options, resourceId, start); 52 | } else { 53 | context.print.warning(chalk`{bold You have not pushed ${resourceName} to the cloud yet.}`); 54 | } 55 | } 56 | 57 | module.exports = { 58 | addResource, 59 | updateResource, 60 | livestreamStartStop, 61 | }; 62 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/obs-templates/basic.ini: -------------------------------------------------------------------------------- 1 | [General] 2 | Name=Template 3 | 4 | [Video] 5 | BaseCX=1280 6 | BaseCY=800 7 | OutputCX=853 8 | OutputCY=533 9 | FPSType=0 10 | FPSCommon=30 11 | 12 | [SimpleOutput] 13 | VBitrate=1600 14 | StreamEncoder=x264 15 | RecQuality=Stream 16 | 17 | [Output] 18 | Mode=Simple 19 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/obs-templates/hd-ivs.ini: -------------------------------------------------------------------------------- 1 | [General] 2 | Name=Template 3 | 4 | [Video] 5 | BaseCX=1920 6 | BaseCY=1080 7 | OutputCX=1920 8 | OutputCY=1080 9 | FPSType=0 10 | FPSCommon=30 11 | ColorFormat=NV12 12 | 13 | [SimpleOutput] 14 | VBitrate=8500 15 | StreamEncoder=x264 16 | RecQuality=Stream -------------------------------------------------------------------------------- /provider-utils/awscloudformation/obs-templates/sd-ivs.ini: -------------------------------------------------------------------------------- 1 | [General] 2 | Name=Template 3 | 4 | [Video] 5 | BaseCX=640 6 | BaseCY=480 7 | OutputCX=640 8 | OutputCY=480 9 | FPSType=0 10 | FPSCommon=30 11 | ColorFormat=NV12 12 | 13 | [SimpleOutput] 14 | VBitrate=1500 15 | StreamEncoder=x264 16 | RecQuality=Stream -------------------------------------------------------------------------------- /provider-utils/awscloudformation/schemas/schema.graphql.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | type VodAsset @model (subscriptions: {level: public}) 4 | @auth( 5 | rules: [ 6 | <% if (locals.permissions && locals.permissions.permissionSchema.includes("any")) { -%> 7 | {allow: owner, ownerField: "owner", operations: [create, update, delete, read] }, 8 | <% } -%> 9 | <% if (locals.permissions && locals.permissions.permissionSchema.includes("admin")) { -%> 10 | {allow: groups, groups:["Admin"], operations: [create, update, delete, read]}, 11 | <% } -%> 12 | <% if (!locals.permissions) { -%> 13 | {allow: groups, groups:["Admin"], operations: [create, update, delete, read]}, 14 | <% } -%> 15 | {allow: private, operations: [read]} 16 | ] 17 | ) 18 | { 19 | id:ID! 20 | title:String! 21 | description:String! 22 | 23 | #DO NOT EDIT 24 | video:VideoObject @connection 25 | } 26 | 27 | #DO NOT EDIT 28 | type VideoObject @model 29 | @auth( 30 | rules: [ 31 | <% if (locals.permissions && locals.permissions.permissionSchema.includes("any")) { -%> 32 | {allow: owner, ownerField: "owner", operations: [create, update, delete, read] }, 33 | <% } -%> 34 | <% if (locals.permissions && locals.permissions.permissionSchema.includes("admin")) { -%> 35 | {allow: groups, groups:["Admin"], operations: [create, update, delete, read]}, 36 | <% } -%> 37 | <% if (!locals.permissions) { -%> 38 | {allow: groups, groups:["Admin"], operations: [create, update, delete, read]}, 39 | <% } -%> 40 | {allow: private, operations: [read]} 41 | ] 42 | ) 43 | { 44 | id:ID! 45 | <% if (contentDeliveryNetwork.signedKey) { -%> 46 | token: String @function(name: "<%= contentDeliveryNetwork.functionNameSchema %>") 47 | <% } -%> 48 | } 49 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/service-walkthroughs/ivs-push.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer'); 2 | const question = require('../../ivs-questions.json'); 3 | const headlessMode = require('../utils/headless-mode'); 4 | 5 | module.exports = { 6 | serviceQuestions, 7 | }; 8 | 9 | async function serviceQuestions(context, options, defaultValuesFilename, resourceName) { 10 | const { amplify } = context; 11 | const projectMeta = context.amplify.getProjectMeta(); 12 | // const projectDetails = context.amplify.getProjectDetails(); 13 | // const defaultLocation = 14 | // path.resolve(`${__dirname}/../default-values/${defaultValuesFilename}`); 15 | // const defaults = JSON.parse(fs.readFileSync(`${defaultLocation}`)); 16 | // const targetDir = amplify.pathManager.getBackendDirPath(); 17 | const props = {}; 18 | let nameDict = {}; 19 | 20 | const { payload } = context.parameters.options; 21 | const args = payload ? JSON.parse(payload) : {}; 22 | 23 | const nameProject = [ 24 | { 25 | type: question.resourceName.type, 26 | name: question.resourceName.key, 27 | message: question.resourceName.question, 28 | validate: amplify.inputValidation(question.resourceName), 29 | default: question.resourceName.default, 30 | when(answers) { 31 | return headlessMode.autoAnswer({ 32 | context, 33 | answers, 34 | key: question.resourceName.key, 35 | value: args.resourceName ? args.resourceName : question.resourceName.default, 36 | }); 37 | }, 38 | }, 39 | ]; 40 | 41 | if (resourceName) { 42 | nameDict.resourceName = resourceName; 43 | props.shared = nameDict; 44 | } else { 45 | nameDict = await inquirer.prompt(nameProject); 46 | props.shared = nameDict; 47 | } 48 | props.shared.bucket = projectMeta.providers.awscloudformation.DeploymentBucketName; 49 | const createChannel = [ 50 | { 51 | type: question.channelQuality.type, 52 | name: question.channelQuality.key, 53 | message: question.channelQuality.question, 54 | choices: question.channelQuality.options, 55 | default: question.channelQuality.default, 56 | when(answers) { 57 | return headlessMode.autoAnswer({ 58 | context, 59 | answers, 60 | key: question.channelQuality.key, 61 | value: args.channelQuality ? args.channelQuality : question.channelQuality.default, 62 | }); 63 | }, 64 | }, 65 | { 66 | type: question.channelLatency.type, 67 | name: question.channelLatency.key, 68 | message: question.channelLatency.question, 69 | choices: question.channelLatency.options, 70 | default: question.channelLatency.default, 71 | when(answers) { 72 | return headlessMode.autoAnswer({ 73 | context, 74 | answers, 75 | key: question.channelLatency.key, 76 | value: args.channelLatency ? args.channelLatency : question.channelLatency.default, 77 | }); 78 | }, 79 | }, 80 | ]; 81 | 82 | const channelQuestions = await inquirer.prompt(createChannel); 83 | props.channel = channelQuestions; 84 | 85 | return props; 86 | } 87 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/service-walkthroughs/vod-roles.js: -------------------------------------------------------------------------------- 1 | function generateIAMAdmin(resourceName, bucketName) { 2 | const admin = { 3 | groupName: 'Admin', 4 | precedence: 1, 5 | customPolicies: [], 6 | }; 7 | admin.customPolicies.push(generateIAMAdminPolicy(resourceName, bucketName)); 8 | return admin; 9 | } 10 | 11 | function generateIAMAdminPolicy(resourceName, bucketName) { 12 | const adminPolicy = { 13 | PolicyName: `${resourceName}-admin-group-policy`, 14 | PolicyDocument: { 15 | Version: '2012-10-17', 16 | Statement: [ 17 | { 18 | Sid: 'VisualEditor0', 19 | Effect: 'Allow', 20 | Action: [ 21 | 's3:PutObject', 22 | 's3:DeleteObject', 23 | ], 24 | Resource: `arn:aws:s3:::${bucketName}/*`, 25 | }, 26 | ], 27 | }, 28 | }; 29 | return adminPolicy; 30 | } 31 | 32 | module.exports = { 33 | generateIAMAdmin, 34 | generateIAMAdminPolicy, 35 | }; 36 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/templates/Amplify_Video_DASH.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "Default DASH Adaptive Bitrate", 3 | "Category": "Amplify-Video", 4 | "Name": "Amplify_Video_DASH", 5 | "Settings": { 6 | "TimecodeConfig": { 7 | "Source": "ZEROBASED" 8 | }, 9 | "OutputGroups": [ 10 | { 11 | "Name": "DASH ISO", 12 | "Outputs": [ 13 | { 14 | "ContainerSettings": { 15 | "Container": "MPD" 16 | }, 17 | "VideoDescription": { 18 | "CodecSettings": { 19 | "Codec": "H_264", 20 | "H264Settings": { 21 | "Bitrate": 3000000 22 | } 23 | } 24 | }, 25 | "NameModifier": "_3000" 26 | }, 27 | { 28 | "ContainerSettings": { 29 | "Container": "MPD" 30 | }, 31 | "VideoDescription": { 32 | "Height": 540, 33 | "CodecSettings": { 34 | "Codec": "H_264", 35 | "H264Settings": { 36 | "Bitrate": 1500000 37 | } 38 | } 39 | }, 40 | "NameModifier": "_1500" 41 | }, 42 | { 43 | "ContainerSettings": { 44 | "Container": "MPD" 45 | }, 46 | "VideoDescription": { 47 | "Height": 432, 48 | "CodecSettings": { 49 | "Codec": "H_264", 50 | "H264Settings": { 51 | "Bitrate": 750000 52 | } 53 | } 54 | }, 55 | "NameModifier": "_750" 56 | }, 57 | { 58 | "ContainerSettings": { 59 | "Container": "MPD" 60 | }, 61 | "VideoDescription": { 62 | "Height": 360, 63 | "CodecSettings": { 64 | "Codec": "H_264", 65 | "H264Settings": { 66 | "Bitrate": 325000 67 | } 68 | } 69 | }, 70 | "NameModifier": "_325" 71 | }, 72 | { 73 | "ContainerSettings": { 74 | "Container": "MPD" 75 | }, 76 | "AudioDescriptions": [ 77 | { 78 | "AudioSourceName": "Audio Selector 1", 79 | "CodecSettings": { 80 | "Codec": "AAC", 81 | "AacSettings": { 82 | "Bitrate": 96000, 83 | "CodingMode": "CODING_MODE_2_0", 84 | "SampleRate": 48000 85 | } 86 | } 87 | } 88 | ] 89 | } 90 | ], 91 | "OutputGroupSettings": { 92 | "Type": "DASH_ISO_GROUP_SETTINGS", 93 | "DashIsoGroupSettings": { 94 | "SegmentLength": 15, 95 | "Destination": "s3://outputDash/", 96 | "FragmentLength": 2, 97 | "SegmentControl": "SINGLE_FILE", 98 | "HbbtvCompliance": "HBBTV_1_5" 99 | } 100 | } 101 | } 102 | ], 103 | "Inputs": [ 104 | { 105 | "AudioSelectors": { 106 | "Audio Selector 1": { 107 | "DefaultSelection": "DEFAULT" 108 | } 109 | }, 110 | "VideoSelector": {}, 111 | "TimecodeSource": "ZEROBASED" 112 | } 113 | ] 114 | }, 115 | "AccelerationSettings": { 116 | "Mode": "DISABLED" 117 | }, 118 | "StatusUpdateInterval": "SECONDS_60", 119 | "Priority": 0 120 | } -------------------------------------------------------------------------------- /provider-utils/awscloudformation/templates/Amplify_Video_System_Accelerated_Ott_Hls_Ts_Avc_Aac.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "System Encoding Template With Accelerated Transcoding (Apple HLS @ 1080p30)", 3 | "Category": "OTT-HLS", 4 | "Name": "Amplify_Video_Ott_Hls_Ts_Avc_Aac", 5 | "Settings": { 6 | "TimecodeConfig": { 7 | "Source": "ZEROBASED" 8 | }, 9 | "OutputGroups": [ 10 | { 11 | "Name": "Apple HLS", 12 | "Outputs": [ 13 | { 14 | "Preset": "System-Ott_Hls_Ts_Avc_Aac_16x9_480x270p_15Hz_0.4Mbps", 15 | "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_480x270p_15Hz_400Kbps" 16 | }, 17 | { 18 | "Preset": "System-Ott_Hls_Ts_Avc_Aac_16x9_640x360p_30Hz_0.6Mbps", 19 | "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_640x360p_30Hz_600Kbps" 20 | }, 21 | { 22 | "Preset": "System-Ott_Hls_Ts_Avc_Aac_16x9_640x360p_30Hz_1.2Mbps", 23 | "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_640x360p_30Hz_1200Kbps" 24 | }, 25 | { 26 | "Preset": "System-Ott_Hls_Ts_Avc_Aac_16x9_960x540p_30Hz_3.5Mbps", 27 | "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_960x540p_30Hz_3500Kbps" 28 | }, 29 | { 30 | "Preset": "System-Ott_Hls_Ts_Avc_Aac_16x9_1280x720p_30Hz_3.5Mbps", 31 | "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_1280x720p_30Hz_3500Kbps" 32 | }, 33 | { 34 | "Preset": "System-Ott_Hls_Ts_Avc_Aac_16x9_1280x720p_30Hz_5.0Mbps", 35 | "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_1280x720p_30Hz_5000Kbps" 36 | }, 37 | { 38 | "Preset": "System-Ott_Hls_Ts_Avc_Aac_16x9_1280x720p_30Hz_6.5Mbps", 39 | "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_1280x720p_30Hz_6500Kbps" 40 | }, 41 | { 42 | "Preset": "System-Ott_Hls_Ts_Avc_Aac_16x9_1920x1080p_30Hz_8.5Mbps", 43 | "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_1920x1080p_30Hz_8500Kbps" 44 | } 45 | ], 46 | "OutputGroupSettings": { 47 | "Type": "HLS_GROUP_SETTINGS", 48 | "HlsGroupSettings": { 49 | "ManifestDurationFormat": "INTEGER", 50 | "SegmentLength": 3, 51 | "TimedMetadataId3Period": 10, 52 | "CaptionLanguageSetting": "OMIT", 53 | "TimedMetadataId3Frame": "PRIV", 54 | "CodecSpecification": "RFC_4281", 55 | "OutputSelection": "MANIFESTS_AND_SEGMENTS", 56 | "ProgramDateTimePeriod": 600, 57 | "MinSegmentLength": 0, 58 | "DirectoryStructure": "SINGLE_DIRECTORY", 59 | "ProgramDateTime": "EXCLUDE", 60 | "SegmentControl": "SEGMENTED_FILES", 61 | "ManifestCompression": "NONE", 62 | "ClientCache": "ENABLED", 63 | "StreamInfResolution": "INCLUDE" 64 | } 65 | } 66 | } 67 | ], 68 | "AdAvailOffset": 0, 69 | "Inputs": [ 70 | { 71 | "AudioSelectors": { 72 | "Audio Selector 1": { 73 | "Offset": 0, 74 | "DefaultSelection": "DEFAULT", 75 | "ProgramSelection": 1 76 | } 77 | }, 78 | "VideoSelector": { 79 | "ColorSpace": "FOLLOW", 80 | "Rotate": "DEGREE_0", 81 | "AlphaBehavior": "DISCARD" 82 | }, 83 | "FilterEnable": "AUTO", 84 | "PsiControl": "USE_PSI", 85 | "FilterStrength": 0, 86 | "DeblockFilter": "DISABLED", 87 | "DenoiseFilter": "DISABLED", 88 | "TimecodeSource": "ZEROBASED" 89 | } 90 | ] 91 | }, 92 | "AccelerationSettings": { 93 | "Mode": "PREFERRED" 94 | }, 95 | "StatusUpdateInterval": "SECONDS_60", 96 | "Priority": 0 97 | } -------------------------------------------------------------------------------- /provider-utils/awscloudformation/templates/Amplify_Video_System_Ott_Hls_Ts_Avc_Aac.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "System Encoding Template (Apple HLS @ 1080p30)", 3 | "Category": "OTT-HLS", 4 | "Name": "Amplify_Video_Ott_Hls_Ts_Avc_Aac", 5 | "Settings": { 6 | "OutputGroups": [ 7 | { 8 | "Name": "Apple HLS", 9 | "Outputs": [ 10 | { 11 | "Preset": "System-Ott_Hls_Ts_Avc_Aac_16x9_480x270p_15Hz_0.4Mbps", 12 | "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_480x270p_15Hz_400Kbps" 13 | }, 14 | { 15 | "Preset": "System-Ott_Hls_Ts_Avc_Aac_16x9_640x360p_30Hz_0.6Mbps", 16 | "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_640x360p_30Hz_600Kbps" 17 | }, 18 | { 19 | "Preset": "System-Ott_Hls_Ts_Avc_Aac_16x9_640x360p_30Hz_1.2Mbps", 20 | "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_640x360p_30Hz_1200Kbps" 21 | }, 22 | { 23 | "Preset": "System-Ott_Hls_Ts_Avc_Aac_16x9_960x540p_30Hz_3.5Mbps", 24 | "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_960x540p_30Hz_3500Kbps" 25 | }, 26 | { 27 | "Preset": "System-Ott_Hls_Ts_Avc_Aac_16x9_1280x720p_30Hz_3.5Mbps", 28 | "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_1280x720p_30Hz_3500Kbps" 29 | }, 30 | { 31 | "Preset": "System-Ott_Hls_Ts_Avc_Aac_16x9_1280x720p_30Hz_5.0Mbps", 32 | "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_1280x720p_30Hz_5000Kbps" 33 | }, 34 | { 35 | "Preset": "System-Ott_Hls_Ts_Avc_Aac_16x9_1280x720p_30Hz_6.5Mbps", 36 | "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_1280x720p_30Hz_6500Kbps" 37 | }, 38 | { 39 | "Preset": "System-Ott_Hls_Ts_Avc_Aac_16x9_1920x1080p_30Hz_8.5Mbps", 40 | "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_1920x1080p_30Hz_8500Kbps" 41 | } 42 | ], 43 | "OutputGroupSettings": { 44 | "Type": "HLS_GROUP_SETTINGS", 45 | "HlsGroupSettings": { 46 | "ManifestDurationFormat": "INTEGER", 47 | "SegmentLength": 3, 48 | "TimedMetadataId3Period": 10, 49 | "CaptionLanguageSetting": "OMIT", 50 | "TimedMetadataId3Frame": "PRIV", 51 | "CodecSpecification": "RFC_4281", 52 | "OutputSelection": "MANIFESTS_AND_SEGMENTS", 53 | "ProgramDateTimePeriod": 600, 54 | "MinSegmentLength": 0, 55 | "DirectoryStructure": "SINGLE_DIRECTORY", 56 | "ProgramDateTime": "EXCLUDE", 57 | "SegmentControl": "SEGMENTED_FILES", 58 | "ManifestCompression": "NONE", 59 | "ClientCache": "ENABLED", 60 | "StreamInfResolution": "INCLUDE" 61 | } 62 | } 63 | } 64 | ], 65 | "AdAvailOffset": 0 66 | }, 67 | "AccelerationSettings": { 68 | "Mode": "DISABLED" 69 | } 70 | } -------------------------------------------------------------------------------- /provider-utils/awscloudformation/utils/exports-templates/aws-video-exports.js.ejs: -------------------------------------------------------------------------------- 1 | // WARNING: DO NOT EDIT. This file is automatically generated by AWS Amplify. It will be overwritten. 2 | 3 | const awsvideoconfig = <%- JSON.stringify(props, null, 4) %>; 4 | 5 | export default awsvideoconfig; -------------------------------------------------------------------------------- /provider-utils/awscloudformation/utils/get-aws.js: -------------------------------------------------------------------------------- 1 | function getAWSConfig(context, options) { 2 | let provider; 3 | if (typeof context.amplify.getPluginInstance === 'function') { 4 | provider = context.amplify.getPluginInstance(context, options.providerPlugin); 5 | } else { 6 | context.print.warning('Falling back to old version of getting AWS SDK. If you see this error you are running an old version of Amplify. Please update as soon as possible!'); 7 | provider = getPluginInstanceShim(context, options.providerPlugin); 8 | } 9 | 10 | return provider; 11 | } 12 | 13 | /* 14 | Shim for old versions of amplify. 15 | */ 16 | function getPluginInstanceShim(context, pluginName) { 17 | const { plugins } = context.runtime; 18 | const pluginObj = plugins.find((plugin) => { 19 | const nameSplit = plugin.name.split('-'); 20 | return (nameSplit[nameSplit.length - 1] === pluginName); 21 | }); 22 | if (pluginObj) { 23 | return require(pluginObj.directory); 24 | } 25 | } 26 | 27 | module.exports = { 28 | getAWSConfig, 29 | }; 30 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/utils/headless-mode.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process'); 2 | 3 | function autoAnswer({ 4 | context, answers, key, value, 5 | }) { 6 | if (context.parameters.options.payload) { 7 | answers[key] = value; 8 | } else { 9 | return true; 10 | } 11 | } 12 | 13 | async function exec(command, args, verbose = true) { 14 | return new Promise((resolve, reject) => { 15 | const childProcess = spawn(command, args); 16 | let output = ''; 17 | 18 | childProcess.stdout.on('data', (stdout) => { 19 | output += stdout.toString(); 20 | if (verbose) { 21 | console.log(stdout.toString()); 22 | } 23 | }); 24 | 25 | childProcess.stderr.on('data', (stderr) => { 26 | console.log(stderr.toString()); 27 | }); 28 | 29 | childProcess.on('exit', () => { 30 | resolve(output); 31 | }); 32 | 33 | childProcess.on('close', async (code) => { 34 | if (verbose) { 35 | console.log(`child process exited with code ${code}`); 36 | } 37 | if (code !== 0) { 38 | reject(new Error('Something went wrong, check above')); 39 | } 40 | resolve(); 41 | }); 42 | }); 43 | } 44 | 45 | module.exports = { 46 | autoAnswer, 47 | exec, 48 | }; 49 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/utils/livestream-obs.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const fs = require('fs'); 3 | const ini = require('ini'); 4 | 5 | module.exports = { 6 | setupOBS, 7 | }; 8 | 9 | async function setupOBS(context, resourceName) { 10 | const { amplify } = context; 11 | const amplifyMeta = amplify.getProjectMeta(); 12 | if ('output' in amplifyMeta.video[resourceName]) { 13 | await createConfig(context, amplifyMeta.video[resourceName], resourceName); 14 | } else { 15 | context.print.warning(chalk`{bold You have not pushed ${resourceName} to the cloud yet.}`); 16 | } 17 | } 18 | 19 | async function createConfig(context, projectConfig, projectName) { 20 | // check for obs installation! 21 | let profileDir = ''; 22 | if (process.platform === 'darwin') { 23 | profileDir = `${process.env.HOME}/Library/Application Support/obs-studio/basic/profiles/`; 24 | } else if (process.platform === 'win32') { 25 | profileDir = `${process.env.APPDATA}/obs-studio/basic/profiles/`; 26 | } else { 27 | profileDir = ''; 28 | } 29 | 30 | if (!fs.existsSync(profileDir)) { 31 | // Ask if they want to continue later 32 | context.print.info('OBS profile not folder not found. Switching to project folder.'); 33 | profileDir = `${process.env.PWD}/OBS/`; 34 | fs.mkdirSync(profileDir); 35 | } 36 | 37 | profileDir = `${profileDir + projectName}/`; 38 | 39 | if (!fs.existsSync(profileDir)) { 40 | fs.mkdirSync(profileDir); 41 | } 42 | 43 | if (projectConfig.serviceType === 'livestream') { 44 | generateINILive(projectName, profileDir); 45 | generateServiceLive(profileDir, projectConfig.output.oMediaLivePrimaryIngestUrl); 46 | } else if (projectConfig.serviceType === 'ivs') { 47 | const targetDir = context.amplify.pathManager.getBackendDirPath(); 48 | const props = JSON.parse(fs.readFileSync(`${targetDir}/video/${projectName}/props.json`)); 49 | generateINIIVS(projectName, profileDir, props); 50 | generateServiceIVS(profileDir, projectConfig.output); 51 | } 52 | 53 | context.print.success('\nConfiguration complete.'); 54 | context.print.blue(chalk`Open OBS and select {bold ${projectName}} profile to use the generated profile for OBS`); 55 | } 56 | 57 | function generateINIIVS(projectName, directory, props) { 58 | let iniBasic; 59 | if (props.channel.channelQuality === 'STANDARD') { 60 | iniBasic = ini.parse(fs.readFileSync(`${__dirname}/../obs-templates/hd-ivs.ini`, 'utf-8')); 61 | } else { 62 | iniBasic = ini.parse(fs.readFileSync(`${__dirname}/../obs-templates/sd-ivs.ini`, 'utf-8')); 63 | } 64 | iniBasic.General.Name = projectName; 65 | fs.writeFileSync(`${directory}basic.ini`, ini.stringify(iniBasic)); 66 | } 67 | 68 | function generateINILive(projectName, directory) { 69 | const iniBasic = ini.parse(fs.readFileSync(`${__dirname}/../obs-templates/basic.ini`, 'utf-8')); 70 | iniBasic.General.Name = projectName; 71 | fs.writeFileSync(`${directory}basic.ini`, ini.stringify(iniBasic)); 72 | } 73 | 74 | function generateServiceIVS(directory, projectOutput) { 75 | // TODO: Write advanced setting setup for keyframes for lower latency! 76 | const setup = { 77 | settings: { 78 | key: projectOutput.oVideoInputKey, 79 | server: `rtmps://${projectOutput.oVideoInputURL}`, 80 | }, 81 | type: 'rtmp_custom', 82 | }; 83 | const json = JSON.stringify(setup); 84 | fs.writeFileSync(`${directory}service.json`, json); 85 | } 86 | 87 | function generateServiceLive(directory, primaryURL) { 88 | const primaryKey = primaryURL.split('/'); 89 | const setup = { 90 | settings: { 91 | key: primaryKey[3], 92 | server: `rtmps://${primaryURL}`, 93 | }, 94 | type: 'rtmp_custom', 95 | }; 96 | const json = JSON.stringify(setup); 97 | fs.writeFileSync(`${directory}service.json`, json); 98 | } 99 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/utils/livestream-startstop.js: -------------------------------------------------------------------------------- 1 | const ora = require('ora'); 2 | const { getAWSConfig } = require('./get-aws'); 3 | 4 | module.exports = { 5 | liveStartStop, 6 | }; 7 | 8 | async function liveStartStop(context, options, resourceId, desiredState) { 9 | const spinner = ora(`${(desiredState) ? 'Starting' : 'Stopping'} the resource`); 10 | spinner.start(); 11 | const provider = getAWSConfig(context, options); 12 | const aws = await provider.getConfiguredAWSClient(context); 13 | const mediaLive = new aws.MediaLive(); 14 | 15 | const mediaLiveParameters = { 16 | ChannelId: resourceId, 17 | }; 18 | mediaLive.describeChannel(mediaLiveParameters, (error, channelDetails) => { 19 | if (error) { 20 | spinner.fail(error); 21 | return; 22 | } 23 | if (channelDetails.State === 'RUNNING' && !desiredState) { 24 | mediaLive.stopChannel(mediaLiveParameters, (stopError) => { 25 | if (stopError) spinner.fail(stopError); 26 | else spinner.succeed('Stopped stream successfully'); 27 | }); 28 | } else if (channelDetails.State === 'IDLE' && desiredState) { 29 | mediaLive.startChannel(mediaLiveParameters, (startError) => { 30 | if (startError) spinner.fail(startError); 31 | else spinner.succeed('Started stream successfully'); 32 | }); 33 | } else if (channelDetails.State === 'RUNNING' || channelDetails.State === 'IDLE') { 34 | spinner.fail(`Trying to ${(desiredState) ? ('start') : ('stop')} stream when it already ${(desiredState) ? ('started') : ('stopped')}`); 35 | } else { 36 | spinner.fail(`Stream is in ${channelDetails.State} state. Can't change state right now`); 37 | } 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/utils/video-getinfo.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const fs = require('fs-extra'); 3 | 4 | module.exports = { 5 | getVideoInfo, 6 | getInfoVideoAll, 7 | }; 8 | 9 | async function getInfoVideoAll(context) { 10 | const amplifyMeta = context.amplify.getProjectMeta(); 11 | if ('video' in amplifyMeta && Object.keys(amplifyMeta.video).length !== 0) { 12 | Object.values(amplifyMeta.video).forEach((project) => { 13 | if ('output' in project) { 14 | if (project.serviceType === 'video-on-demand') { 15 | prettifyOutputVod(context, project.output); 16 | } else if (project.serviceType === 'livestream') { 17 | prettifyOutputLive(context, project.output); 18 | } else if (project.serviceType === 'ivs') { 19 | prettifyOutputIVS(context, project.output); 20 | } 21 | if ('oMediaLivePrimaryIngestUrl' in project.output) { 22 | prettifyOutputLive(context, project.output); 23 | } 24 | } 25 | }); 26 | await generateAWSExportsVideo(context); 27 | } 28 | } 29 | 30 | async function generateAWSExportsVideo(context) { 31 | const projectConfig = context.amplify.getProjectConfig(); 32 | const projectMeta = context.amplify.getProjectMeta(); 33 | const targetDir = context.amplify.pathManager.getBackendDirPath(); 34 | const props = {}; 35 | 36 | let filePath = ''; 37 | 38 | if (projectConfig.frontend === 'ios') { 39 | filePath = './aws-video-exports.json'; 40 | } else if (projectConfig.frontend === 'android') { 41 | filePath = `./${projectConfig.android.config.ResDir}/aws-video-exports.json`; 42 | } else if (projectConfig.frontend === 'javascript') { 43 | filePath = `./${projectConfig.javascript.config.SourceDir}/aws-video-exports.js`; 44 | } else { 45 | // Default location in json. Worst case scenario 46 | filePath = './aws-video-exports.json'; 47 | } 48 | 49 | if ('video' in projectMeta && Object.keys(projectMeta.video).length !== 0) { 50 | Object.entries(projectMeta.video).forEach((videoMeta) => { 51 | const [projectName, project] = videoMeta; 52 | const videoConfig = JSON.parse(fs.readFileSync(`${targetDir}/video/${projectName}/props.json`)); 53 | if ('output' in project) { 54 | const { output } = project; 55 | if (project.serviceType === 'video-on-demand') { 56 | props.awsInputVideo = output.oVODInputS3; 57 | props.awsOutputVideo = output.oVodOutputUrl; 58 | props.protectedURLS = videoConfig.signedKey; 59 | } else if (project.serviceType === 'livestream') { 60 | if (output.oPrimaryHlsEgress) { 61 | props.awsOutputLiveHLS = output.oPrimaryHlsEgress; 62 | } 63 | if (output.oPrimaryDashEgress) { 64 | props.awsOutputLiveDash = output.oPrimaryDashEgress; 65 | } 66 | if (output.oPrimaryMssEgress) { 67 | props.awsOutputLiveMss = output.oPrimaryMssEgress; 68 | } 69 | if (output.oPrimaryCmafEgress) { 70 | props.awsOutputLiveCmaf = output.oPrimaryCmafEgress; 71 | } 72 | if (output.oMediaStoreContainerName) { 73 | props.awsOutputLiveLL = output.oPrimaryMediaStoreEgressUrl; 74 | } 75 | } else if (project.serviceType === 'ivs') { 76 | props.awsOutputIVS = output.oVideoOutput; 77 | } 78 | } 79 | }); 80 | 81 | if (projectConfig.frontend === 'javascript') { 82 | const copyJobs = [ 83 | { 84 | dir: __dirname, 85 | template: 'exports-templates/aws-video-exports.js.ejs', 86 | target: filePath, 87 | }, 88 | ]; 89 | await context.amplify.copyBatch(context, copyJobs, props, true); 90 | } else { 91 | fs.writeFileSync(filePath, JSON.stringify(props, null, 4)); 92 | } 93 | } 94 | } 95 | 96 | async function getVideoInfo(context, resourceName) { 97 | const amplifyMeta = context.amplify.getProjectMeta(); 98 | if ('output' in amplifyMeta.video[resourceName]) { 99 | if (amplifyMeta.video[resourceName].serviceType === 'video-on-demand') { 100 | await prettifyOutputVod(context, amplifyMeta.video[resourceName].output); 101 | } else if (amplifyMeta.video[resourceName].serviceType === 'livestream') { 102 | await prettifyOutputLive(context, amplifyMeta.video[resourceName].output); 103 | } else if (amplifyMeta.video[resourceName].serviceType === 'ivs') { 104 | await prettifyOutputIVS(context, amplifyMeta.video[resourceName].output); 105 | } 106 | await generateAWSExportsVideo(context); 107 | } else { 108 | context.print.warning(chalk`{bold You have not pushed ${resourceName} to the cloud yet.}`); 109 | } 110 | } 111 | 112 | async function prettifyOutputIVS(context, output) { 113 | context.print.info(chalk.bold('\nInteractive Video Service:')); 114 | context.print.blue('\nInput url:'); 115 | context.print.blue(chalk`{underline rtmps://${output.oVideoInputURL}}\n`); 116 | context.print.blue('Stream Keys:'); 117 | context.print.blue(`${output.oVideoInputKey}\n`); 118 | context.print.blue('Output url:'); 119 | context.print.blue(chalk`{underline ${output.oVideoOutput}}\n`); 120 | context.print.blue('Channel ARN:'); 121 | context.print.blue(`${output.oVideoChannelArn}\n`); 122 | } 123 | 124 | async function prettifyOutputLive(context, output) { 125 | context.print.info(chalk.bold('\nLivestream Info:')); 126 | context.print.info(chalk.bold('\nMediaLive')); 127 | context.print.blue(chalk`MediaLive Primary Ingest Url: {underline ${output.oMediaLivePrimaryIngestUrl}}`); 128 | const primaryKey = output.oMediaLivePrimaryIngestUrl.split('/'); 129 | context.print.blue(`MediaLive Primary Stream Key: ${primaryKey[3]}\n`); 130 | context.print.blue(chalk`MediaLive Backup Ingest Url: {underline ${output.oMediaLiveBackupIngestUrl}}`); 131 | const backupKey = output.oMediaLiveBackupIngestUrl.split('/'); 132 | context.print.blue(`MediaLive Backup Stream Key: ${backupKey[3]}`); 133 | 134 | if (output.oPrimaryHlsEgress || output.oPrimaryCmafEgress 135 | || output.oPrimaryDashEgress || output.oPrimaryMssEgress) { 136 | context.print.info(chalk.bold('\nMediaPackage')); 137 | } 138 | if (output.oPrimaryHlsEgress) { 139 | context.print.blue(chalk`MediaPackage HLS Egress Url: {underline ${output.oPrimaryHlsEgress}}`); 140 | } 141 | if (output.oPrimaryDashEgress) { 142 | context.print.blue(chalk`MediaPackage Dash Egress Url: {underline ${output.oPrimaryDashEgress}}`); 143 | } 144 | if (output.oPrimaryMssEgress) { 145 | context.print.blue(chalk`MediaPackage MSS Egress Url: {underline ${output.oPrimaryMssEgress}}`); 146 | } 147 | if (output.oPrimaryCmafEgress) { 148 | context.print.blue(chalk`MediaPackage CMAF Egress Url: {underline ${output.oPrimaryCmafEgress}}`); 149 | } 150 | 151 | if (output.oMediaStoreContainerName) { 152 | context.print.info(chalk.bold('\nMediaStore')); 153 | context.print.blue(chalk`MediaStore Output Url: {underline ${output.oPrimaryMediaStoreEgressUrl}}`); 154 | } 155 | } 156 | 157 | async function prettifyOutputVod(context, output) { 158 | context.print.info(chalk.bold('\nVideo on Demand:')); 159 | context.print.blue('\nInput Storage bucket:'); 160 | context.print.blue(`${output.oVODInputS3}\n`); 161 | if (output.oVodOutputUrl) { 162 | context.print.blue('Output URL for content:'); 163 | context.print.blue(chalk`{underline https://${output.oVodOutputUrl}\n}`); 164 | } else { 165 | context.print.blue('Output Storage bucket:'); 166 | context.print.blue(`${output.oVODOutputS3}\n`); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/video-player-templates/android/activity_video_player.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/video-player-templates/android/video-player.ejs: -------------------------------------------------------------------------------- 1 | package <%= packageName %> 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import com.google.android.exoplayer2.MediaItem 6 | import com.google.android.exoplayer2.SimpleExoPlayer 7 | import com.google.android.exoplayer2.ui.PlayerView 8 | 9 | class VideoPlayerActivity : AppCompatActivity() { 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | setContentView(R.layout.activity_video_player) 13 | val playerView: PlayerView = findViewById(R.id.video_view) 14 | val player = SimpleExoPlayer.Builder(this@VideoPlayerActivity).build() 15 | val mediaItem = MediaItem.fromUri("<%= src %>"); 16 | 17 | playerView.player = player; 18 | player.setMediaItem(mediaItem) 19 | } 20 | } -------------------------------------------------------------------------------- /provider-utils/awscloudformation/video-player-templates/ios/bridging-header.h.ejs: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import "MobileVLCKit/MobileVLCKit.h" 6 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/video-player-templates/ios/empty.cpp.ejs: -------------------------------------------------------------------------------- 1 | // 2 | // empty.cpp 3 | // <%= projectName %> 4 | // 5 | // Created by Amazon.com, Inc. or its affiliates. All Rights Reserved. on <%= creationDate %>. 6 | // 7 | 8 | #include "empty.hpp" -------------------------------------------------------------------------------- /provider-utils/awscloudformation/video-player-templates/ios/empty.hpp.ejs: -------------------------------------------------------------------------------- 1 | // 2 | // empty.hpp 3 | // <%= projectName %> 4 | // 5 | // Created by Amazon.com, Inc. or its affiliates. All Rights Reserved. on <%= creationDate %>. 6 | // 7 | 8 | #ifndef empty_hpp 9 | #define empty_hpp 10 | 11 | #include 12 | 13 | #endif /* empty_hpp */ 14 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/video-player-templates/ios/ios-video-component.ejs: -------------------------------------------------------------------------------- 1 | struct VideoPlayer: UIViewRepresentable{ 2 | func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext) { 3 | } 4 | 5 | func makeUIView(context: Context) -> UIView { 6 | return PlayerUIView(frame: .zero) 7 | } 8 | } 9 | 10 | struct ContentView: View { 11 | var body: some View { 12 | VideoPlayer() 13 | } 14 | } -------------------------------------------------------------------------------- /provider-utils/awscloudformation/video-player-templates/ios/video-player.ejs: -------------------------------------------------------------------------------- 1 | // 2 | // VideoPlayer.swift 3 | // <%= projectName %> 4 | // 5 | // Created by Amazon.com, Inc. or its affiliates. All Rights Reserved. on <%= creationDate %>. 6 | // 7 | 8 | import SwiftUI 9 | <% if (serviceType === 'ivs') { -%> 10 | import AmazonIVSPlayer 11 | <% } -%> 12 | 13 | class PlayerUIView: <%= serviceType === 'ivs' ? 'IVSPlayerView' : 'UIView, VLCMediaPlayerDelegate' -%>{ 14 | private let mediaPlayer = <%= serviceType === 'ivs' ? 'IVSPlayer()' : 'VLCMediaPlayer()' -%> 15 | 16 | override init(frame: CGRect) { 17 | super.init(frame: frame) 18 | 19 | let url = URL(string: "<%= src %>")! //replace your resource here 20 | <% if (serviceType === 'ivs') { -%> 21 | let notificationCenter = NotificationCenter.default 22 | notificationCenter.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.willResignActiveNotification, object: nil) 23 | mediaPlayer.delegate = self 24 | self.player = mediaPlayer 25 | mediaPlayer.load(url) 26 | <% } else { -%> 27 | let gesture = UITapGestureRecognizer(target: self, action: #selector(PlayerUIView.movieViewTapped(_:))) 28 | self.addGestureRecognizer(gesture) 29 | 30 | 31 | mediaPlayer.media = VLCMedia(url: url) 32 | mediaPlayer.delegate = self 33 | mediaPlayer.drawable = self 34 | mediaPlayer.play() 35 | <% } -%> 36 | } 37 | 38 | required init?(coder: NSCoder) { 39 | fatalError("init(coder:) has not been implemented") 40 | } 41 | 42 | override func layoutSubviews() { 43 | super.layoutSubviews() 44 | } 45 | 46 | <% if (serviceType === 'ivs') { -%> 47 | @objc func appMovedToBackground() { 48 | print("App moved to background!") 49 | mediaPlayer.pause() 50 | } 51 | <% } else { -%> 52 | @objc func movieViewTapped(_ sender: UITapGestureRecognizer) { 53 | if mediaPlayer.isPlaying { 54 | mediaPlayer.pause() 55 | let remaining = mediaPlayer.remainingTime 56 | let time = mediaPlayer.time 57 | print("Paused at \(time?.stringValue ?? "nil") with \(remaining?.stringValue ?? "nil") time remaining") 58 | } else { 59 | mediaPlayer.play() 60 | print("Playing") 61 | } 62 | } 63 | <% } -%> 64 | } 65 | 66 | <% if (serviceType === 'ivs') { -%> 67 | extension PlayerUIView: IVSPlayer.Delegate { 68 | func player(_ player: IVSPlayer, didChangeState state: IVSPlayer.State) { 69 | if state == .ready { 70 | print("player ready") 71 | player.play() 72 | } 73 | } 74 | } 75 | <% } -%> -------------------------------------------------------------------------------- /provider-utils/awscloudformation/video-player-templates/web/angular-video-component.ejs: -------------------------------------------------------------------------------- 1 | 8 | <% } else { -%> 9 | sources: [{ 10 | src: '<%= src %>', 11 | type: 'application/x-mpegURL' 12 | }]}"> 13 | <% } -%> 14 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/video-player-templates/web/ember-video-component.ejs: -------------------------------------------------------------------------------- 1 | {{video-player 2 | autoplay=true 3 | controls=true 4 | src='<%= src %>' 5 | <% if (channelLatency !== 'LOW') { -%> 6 | type='application/x-mpegURL' 7 | <% } -%> 8 | }} -------------------------------------------------------------------------------- /provider-utils/awscloudformation/video-player-templates/web/none-video-component.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 |
9 | 20 | 21 | <% if (channelLatency === 'LOW') { -%> 22 | 23 | 41 | <% } else { -%> 42 | 55 | <% } -%> 56 | -------------------------------------------------------------------------------- /provider-utils/awscloudformation/video-player-templates/web/react-video-component.ejs: -------------------------------------------------------------------------------- 1 | 5 | src={'<%= src %>'} 6 | techOrder={["AmazonIVS"]} 7 | <% } else { -%> 8 | sources={[{ 9 | src: '<%= src %>', 10 | type: 'application/x-mpegURL' 11 | }]} 12 | <% } -%> 13 | /> -------------------------------------------------------------------------------- /provider-utils/awscloudformation/video-player-templates/web/video-player.component.scss: -------------------------------------------------------------------------------- 1 | @import '~video.js/dist/video-js.css'; -------------------------------------------------------------------------------- /provider-utils/awscloudformation/video-player-templates/web/video-player.ejs: -------------------------------------------------------------------------------- 1 | <% if (framework === 'react') { -%> 2 | import React from 'react'; 3 | import videojs from 'video.js'; 4 | import 'video.js/dist/video-js.css'; 5 | <% if (channelLatency === 'LOW') { -%> 6 | // eslint-disable-next-line no-undef 7 | registerIVSTech(videojs); 8 | <% } -%> 9 | 10 | export default class VideoPlayer extends React.Component { 11 | componentDidMount() { 12 | this.player = videojs(this.videoNode, this.props); 13 | this.player.ready(() => { 14 | console.log('ready'); 15 | <% if (channelLatency === 'LOW') { -%> 16 | this.player.src(this.props.src); 17 | const ivsPlayer = this.player.getIVSPlayer(); 18 | const PlayerState = this.player.getIVSEvents().PlayerState; 19 | ivsPlayer.addEventListener(PlayerState.PLAYING, () => { 20 | console.log("Player State - PLAYING"); 21 | setTimeout(() => { 22 | console.log( 23 | `This stream is ${ 24 | ivsPlayer.isLiveLowLatency() ? "" : "not " 25 | }playing in ultra low latency mode` 26 | ); 27 | console.log(`Stream Latency: ${ivsPlayer.getLiveLatency()}s`); 28 | }, 5000); 29 | }); 30 | <% } -%> 31 | }) 32 | } 33 | 34 | componentWillUnmount() { 35 | if (this.player) { 36 | this.player.dispose(); 37 | } 38 | } 39 | 40 | render() { 41 | return ( 42 |
43 |
44 | 45 |
46 |
47 | ); 48 | } 49 | } 50 | <% } -%> 51 | <% if (framework === 'vue') { -%> 52 | 57 | 58 | 111 | <% } -%> 112 | <% if (framework === 'angular') { -%> 113 | import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; 114 | import videojs from 'video.js'; 115 | <% if (channelLatency === 'LOW') { -%> 116 | // @ts-ignore 117 | registerIVSTech(videojs); 118 | <% } -%> 119 | 120 | @Component({ 121 | selector: 'video-player', 122 | template: ` 123 | 124 | `, 125 | styleUrls: [ 126 | './video-player.component.scss' 127 | ], 128 | encapsulation: ViewEncapsulation.None, 129 | }) 130 | export class VideoPlayerComponent implements OnInit, OnDestroy { 131 | @ViewChild('target', {static: true}) target: ElementRef | undefined; 132 | // see options: https://github.com/videojs/video.js/blob/maintutorial-options.html 133 | @Input() options: any; 134 | player: videojs.Player | undefined = undefined; 135 | 136 | constructor( 137 | private elementRef: ElementRef, 138 | ) { } 139 | 140 | ngOnInit() { 141 | // instantiate Video.js 142 | this.player = videojs(this.target?.nativeElement, this.options, () => { 143 | console.log('onPlayerReady', this); 144 | <% if (channelLatency === 'LOW') { -%> 145 | this.player?.src(this.options.src); 146 | <% } -%> 147 | }); 148 | } 149 | 150 | ngOnDestroy() { 151 | // destroy player 152 | if (this.player) { 153 | this.player.dispose(); 154 | } 155 | } 156 | } 157 | <% } -%> 158 | <% if (framework === 'ember') { -%> 159 | import Ember from 'ember'; 160 | import videojs from 'video.js'; 161 | <% if (channelLatency === 'LOW') { -%> 162 | // eslint-disable-next-line no-undef 163 | registerIVSTech(videojs); 164 | <% } -%> 165 | 166 | export default Ember.Component.extend({ 167 | tagName: 'video', 168 | 169 | classNames: ['video-js'], 170 | 171 | didInsertElement() { 172 | this._super(...arguments); 173 | const { controls, autoplay, src<% if (channelLatency === 'LOW') { -%>, type<% } -%> } = this; 174 | const player = videojs(this.element, { 175 | controls, 176 | autoplay, 177 | preload: 'auto', 178 | <% if (channelLatency === 'LOW') { -%> 179 | techOrder: ['AmazonIVS'], 180 | <% } -%> 181 | }); 182 | 183 | player.ready(() => { 184 | <% if (channelLatency === 'LOW') { -%> 185 | player.src(src); 186 | const ivsPlayer = player.getIVSPlayer(); 187 | const PlayerState = player.getIVSEvents().PlayerState; 188 | ivsPlayer.addEventListener(PlayerState.PLAYING, () => { 189 | console.log("Player State - PLAYING"); 190 | setTimeout(() => { 191 | console.log( 192 | `This stream is ${ 193 | ivsPlayer.isLiveLowLatency() ? "" : "not " 194 | }playing in ultra low latency mode` 195 | ); 196 | console.log(`Stream Latency: ${ivsPlayer.getLiveLatency()}s`); 197 | }, 5000); 198 | }); 199 | <% } else { -%> 200 | player.src({ src, type }); 201 | <% } -%> 202 | this.one('willDestroyElement', function () { 203 | player.dispose(); 204 | }); 205 | }); 206 | }, 207 | }); 208 | <% } -%> -------------------------------------------------------------------------------- /provider-utils/awscloudformation/video-player-templates/web/vue-video-component.ejs: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /provider-utils/ivs-questions.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceName": { 3 | "key": "resourceName", 4 | "question": "Provide a friendly name for your resource to be used as a label for this category in the project:", 5 | "validation": { 6 | "operator": "regex", 7 | "value": "^[a-zA-Z0-9\\-]+$", 8 | "onErrorMsg": "Resource name should be alphanumeric" 9 | }, 10 | "required": true, 11 | "default": "myivs", 12 | "next": "channelQuality" 13 | }, 14 | "channelQuality": { 15 | "key": "channelQuality", 16 | "question": "Choose the Channel type or stream quality:", 17 | "type": "list", 18 | "options": [ 19 | { 20 | "name": "Standard (HD Stream)", 21 | "value": "STANDARD", 22 | "next": "channelLatency" 23 | }, 24 | { 25 | "name": "Basic (SD Stream)", 26 | "value": "BASIC", 27 | "next": "channelLatency" 28 | } 29 | ], 30 | "default": "STANDARD", 31 | "required": true 32 | }, 33 | "channelLatency": { 34 | "key": "channelLatency", 35 | "question": "Choose the Video latency type:", 36 | "type": "list", 37 | "options": [ 38 | { 39 | "name": "Ultra-low latency (~5 seconds)", 40 | "value": "LOW" 41 | }, 42 | { 43 | "name": "Standard latency (~10 seconds) ", 44 | "value": "NORMAL" 45 | } 46 | ], 47 | "default": "LOW", 48 | "required": true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /provider-utils/supported-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "livestream":{ 3 | "alias":"AWS Elemental Live Stream", 4 | "serviceWalkthroughFilename":"livestream-push.js", 5 | "cfnFilename":"livestream-workflow-template.json.ejs", 6 | "stackFolder":"livestream-helpers", 7 | "defaultValuesFilename":"livestream-defaults.json", 8 | "provider":"awscloudformation" 9 | }, 10 | "ivs":{ 11 | "alias":"Amazon Interactive Video Service Live Stream", 12 | "serviceWalkthroughFilename":"ivs-push.js", 13 | "cfnFilename":"ivs-workflow-template.yaml.ejs", 14 | "stackFolder":"ivs-helpers", 15 | "defaultValuesFilename":"ivs-defaults.json", 16 | "provider":"awscloudformation" 17 | }, 18 | "video-on-demand":{ 19 | "alias":"Video-On-Demand", 20 | "serviceWalkthroughFilename":"vod-push.js", 21 | "cfnFilename":"vod-workflow-template.yaml.ejs", 22 | "stackFolder":"vod-helpers", 23 | "defaultValuesFilename":"vod-defaults.json", 24 | "provider":"awscloudformation" 25 | } 26 | } -------------------------------------------------------------------------------- /provider-utils/vod-questions.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceName": { 3 | "key": "resourceName", 4 | "question": "Provide a friendly name for your resource to be used as a label for this category in the project:", 5 | "validation": { 6 | "operator": "regex", 7 | "value": "^[a-zA-Z0-9]+$", 8 | "onErrorMsg": "Resource name should be alphanumeric" 9 | }, 10 | "required": true, 11 | "default": "myvodservice", 12 | "next": "createSnsTopic" 13 | }, 14 | "encodingTemplate": { 15 | "key": "encodingTemplate", 16 | "question": "Select a system-provided encoding template, specify an already-created template name: ", 17 | "type":"list", 18 | "required": true, 19 | "default": "Amplify_Video_HLS.json", 20 | "next": "createSnsTopic" 21 | }, 22 | "encodingTemplateName": { 23 | "key": "encodingTemplateName", 24 | "question": "Provide a specific MediaConvert template name (must be available in project region):", 25 | "validation": { 26 | "operator": "regex", 27 | "value": "^(?!System-)[^$&,:;?<>`\"#%{}\\\/|^~]{1,80}$", 28 | "onErrorMsg": "Resource name should be alphanumeric, should not begin with System-, or contain $&,:;?<>`\"#%{}/|^~" 29 | }, 30 | "required": true, 31 | "next": "createSnsTopic" 32 | }, 33 | "createSnsTopic": { 34 | "key" : "createSnsTopic", 35 | "type": "confirm", 36 | "question" : "Do you want to get notifications on the video processing job?", 37 | "required" : true, 38 | "options" : [ 39 | { 40 | "value" : "true", 41 | "next" : "enableSnsFunction" 42 | }, 43 | { 44 | "value": "false", 45 | "next" : "enableCDN" 46 | } 47 | ] 48 | }, 49 | "enableSnsFunction" : { 50 | "key" : "enableSnsFunction", 51 | "type" : "confirm", 52 | "question" : "Do you want a custom function executed for notifications?", 53 | "required" : true, 54 | "next": "enableCDN" 55 | }, 56 | "enableCDN": { 57 | "key": "enableCDN", 58 | "type": "confirm", 59 | "question": "Is this a production enviroment?", 60 | "required": true, 61 | "options": [ 62 | { 63 | "value": "true", 64 | "next": "modifySignedUrl", 65 | "ignore": true 66 | }, 67 | { 68 | "value": "true", 69 | "next": "signedKey" 70 | }, 71 | { 72 | "value": "false", 73 | "next": "enableCMS" 74 | } 75 | ] 76 | }, 77 | "enableCMS": { 78 | "key": "enableCMS", 79 | "type": "confirm", 80 | "question": "Do you want Amplify to create a new GraphQL API to manage your videos? (Beta)", 81 | "required": true, 82 | "options": [ 83 | { 84 | "value": "true", 85 | "next": "permissionSchema", 86 | "ignore": true 87 | }, 88 | { 89 | "value": "false" 90 | } 91 | ] 92 | }, 93 | "subscribeField": { 94 | "key": "subscribeField", 95 | "type": "confirm", 96 | "question": "Do you want to lock your videos with a subscription?", 97 | "required": true, 98 | "options": [ 99 | { 100 | "value": "true", 101 | "next": "" 102 | }, 103 | { 104 | "value": "false", 105 | "next": "" 106 | } 107 | ] 108 | }, 109 | "editAPI": { 110 | "key": "editAPI", 111 | "type": "confirm", 112 | "question": "Do you want to edit your newly created model?", 113 | "required": true, 114 | "options": [ 115 | { 116 | "value": "true", 117 | "ignore": true 118 | }, 119 | { 120 | "value": "false", 121 | "ignore": true 122 | } 123 | ], 124 | "default": true 125 | }, 126 | "modifySignedUrl": { 127 | "key": "modifySignedUrl", 128 | "type": "list", 129 | "question": "We detected you have signed urls configured. Would you like to:", 130 | "options": [ 131 | { 132 | "name": "Leave as configured", 133 | "value": "leave", 134 | "next": "enableCMS", 135 | "ignore": true 136 | }, 137 | { 138 | "name": "Rotate the keys for the signed urls", 139 | "value": "rotate", 140 | "next": "enableCMS", 141 | "ignore": true 142 | }, 143 | { 144 | "name": "Remove signed urls", 145 | "value": "remove", 146 | "next": "enableCMS", 147 | "ignore": true 148 | } 149 | ], 150 | "default": "leave" 151 | }, 152 | "pemKeyID-DEADDONOTUSE": { 153 | "key": "pemKeyID-DEADDONOTUSE", 154 | "type": "input", 155 | "question": "What is the CloudFront Key Pair Access Key ID associated with the pem key?", 156 | "required": true, 157 | "default": "", 158 | "next": "" 159 | }, 160 | "signedKey": { 161 | "key": "signedKey", 162 | "type": "confirm", 163 | "question": "Do you want to protect your content with signed urls?", 164 | "required": true, 165 | "options": [ 166 | { 167 | "value": "true", 168 | "next": "enableCMS" 169 | }, 170 | { 171 | "value": "false", 172 | "next": "enableCMS", 173 | "ignore": true 174 | } 175 | ], 176 | "default": true 177 | }, 178 | "overrideSchema": { 179 | "key": "overrideSchema", 180 | "type": "confirm", 181 | "question": "Do you want to override your GraphQL schema?", 182 | "required": true, 183 | "options": [ 184 | { 185 | "value": "true", 186 | "next": "editAPI", 187 | "ignore": true 188 | }, 189 | { 190 | "value": "false", 191 | "next": "editAPI", 192 | "ignore": true 193 | } 194 | ], 195 | "default": true 196 | }, 197 | "permissionSchema": { 198 | "key": "permissionSchema", 199 | "question": "Define your permission schema", 200 | "type": "checkbox", 201 | "options": [ 202 | { 203 | "name": "Admins can only upload videos", 204 | "value": "admin", 205 | "checked": true, 206 | "ignore": true 207 | }, 208 | { 209 | "name": "Any authenticated user can upload videos", 210 | "value": "any", 211 | "ignore": true 212 | } 213 | ], 214 | "next": "overrideSchema" 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /scripts/headless/add-ivs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | IFS='|' 4 | 5 | STD_LOW="{\ 6 | \"service\":\"video\",\ 7 | \"serviceType\":\"ivs\",\ 8 | \"providerName\":\"awscloudformation\",\ 9 | \"resourceName\":\"stdLow\",\ 10 | \"channelQuality\":\"STANDARD\",\ 11 | \"channelLatency\":\"LOW\"\ 12 | }" 13 | 14 | STD_NORMAL="{\ 15 | \"service\":\"video\",\ 16 | \"serviceType\":\"ivs\",\ 17 | \"providerName\":\"awscloudformation\",\ 18 | \"resourceName\":\"stdNormal\",\ 19 | \"channelQuality\":\"STANDARD\",\ 20 | \"channelLatency\":\"NORMAL\"\ 21 | }" 22 | 23 | BASIC_LOW="{\ 24 | \"service\":\"video\",\ 25 | \"serviceType\":\"ivs\",\ 26 | \"providerName\":\"awscloudformation\",\ 27 | \"resourceName\":\"basicLow\",\ 28 | \"channelQuality\":\"BASIC\",\ 29 | \"channelLatency\":\"LOW\"\ 30 | }" 31 | 32 | BASIC_NORMAL="{\ 33 | \"service\":\"video\",\ 34 | \"serviceType\":\"ivs\",\ 35 | \"providerName\":\"awscloudformation\",\ 36 | \"resourceName\":\"basicNormal\",\ 37 | \"channelQuality\":\"BASIC\",\ 38 | \"channelLatency\":\"NORMAL\"\ 39 | }" 40 | 41 | amplify video add --payload $STD_LOW 42 | # amplify video add --payload $STD_NORMAL 43 | # amplify video add --payload $BASIC_LOW 44 | # amplify video add --payload $BASIC_NORMAL -------------------------------------------------------------------------------- /scripts/headless/add-vod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | IFS='|' 4 | 5 | VOD="{\ 6 | \"service\":\"video\",\ 7 | \"serviceType\":\"video-on-demand\",\ 8 | \"providerName\":\"awscloudformation\",\ 9 | \"resourceName\":\"vodTest\",\ 10 | \"enableCDN\":true,\ 11 | \"signedKey\":true,\ 12 | \"enableCMS\":false\ 13 | }" 14 | 15 | amplify video add --payload $VOD -------------------------------------------------------------------------------- /scripts/headless/amplify-delete.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | amplify delete --force -------------------------------------------------------------------------------- /scripts/headless/amplify-push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | IFS='|' 4 | 5 | amplify push --yes -------------------------------------------------------------------------------- /scripts/headless/init-new-project.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | IFS='|' 4 | 5 | CONFIG="{\ 6 | \"SourceDir\":\"src\",\ 7 | \"DistributionDir\":\"dist\",\ 8 | \"BuildCommand\":\"npm run-script build\",\ 9 | \"StartCommand\":\"npm run-script start\"\ 10 | }" 11 | 12 | AWSCLOUDFORMATIONCONFIG="{\ 13 | \"configLevel\":\"project\",\ 14 | \"useProfile\":true,\ 15 | \"profileName\":\"default\"\ 16 | }" 17 | 18 | AMPLIFY="{\ 19 | \"projectName\":\"amplifyVideoProject\",\ 20 | \"envName\":\"dev\",\ 21 | \"defaultEditor\":\"code\"\ 22 | }" 23 | 24 | FRONTEND="{\ 25 | \"frontend\":\"javascript\",\ 26 | \"framework\":\"none\",\ 27 | \"config\":$CONFIG\ 28 | }" 29 | 30 | PROVIDERS="{\ 31 | \"awscloudformation\":$AWSCLOUDFORMATIONCONFIG\ 32 | }" 33 | 34 | amplify init \ 35 | --amplify $AMPLIFY \ 36 | --frontend $FRONTEND \ 37 | --providers $PROVIDERS \ 38 | --yes -------------------------------------------------------------------------------- /scripts/post-install.js: -------------------------------------------------------------------------------- 1 | 2 | console.log('------------------------------------'); 3 | console.log('\n'); 4 | console.log('Successfully installed Amplify Video'); 5 | console.log('\n'); 6 | console.log('------------------------------------'); 7 | console.log('\n'); 8 | -------------------------------------------------------------------------------- /scripts/setup.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { exec } = require('../provider-utils/awscloudformation/utils/headless-mode'); 4 | 5 | module.exports = async function setup() { 6 | if (process.env.NODE_ENV !== 'test') { 7 | const directoryPath = path.join(__dirname, `../${process.env.AMP_PATH}/amplify`); 8 | if (!fs.existsSync(directoryPath)) { 9 | throw new Error(`No amplify project found, make sure to set AMP_PATH with correct path.\nActual path: ${directoryPath}`); 10 | } 11 | } else { 12 | await executeScripts(); 13 | } 14 | }; 15 | 16 | async function executeScripts() { 17 | try { 18 | console.log('\namplify init'); 19 | await exec('bash', ['./scripts/headless/init-new-project.sh']); 20 | console.log('\namplify add video'); 21 | await exec('bash', ['./scripts/headless/add-ivs.sh']); 22 | await exec('bash', ['./scripts/headless/add-vod.sh']); 23 | console.log('\namplify push'); 24 | await exec('bash', ['./scripts/headless/amplify-push.sh']); 25 | } catch (error) { 26 | await exec('bash', ['./scripts/headless/amplify-delete.sh']); 27 | throw (new Error(error)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /scripts/teardown.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('../provider-utils/awscloudformation/utils/headless-mode'); 2 | 3 | module.exports = async function teardown() { 4 | console.log('amplify delete --force'); 5 | await exec('bash', ['./scripts/headless/amplify-delete.sh']); 6 | }; 7 | -------------------------------------------------------------------------------- /tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/amplify-video/1f11013a9473f0750940cc05c8b4154f8e6643b7/tests/.gitkeep -------------------------------------------------------------------------------- /tests/integration/cloudformation.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const glob = require('glob'); 3 | const AWS = require('aws-sdk'); 4 | const fs = require('fs'); 5 | 6 | AWS.config.update({region:'us-west-2'}); 7 | 8 | const cloudformation = new AWS.CloudFormation(); 9 | 10 | test('Should validate CloudFormation templates', async () => { 11 | let directoryPath = path.join(__dirname, '../../amplify/backend/video/**/build/**/*.template'); 12 | if (process.env.NODE_ENV !== 'test' && process.env.AMP_PATH) { 13 | directoryPath = path.join(__dirname, `../../${process.env.AMP_PATH}/amplify/backend/video/**/build/**/*.template`); 14 | } 15 | const files = glob.sync(directoryPath); 16 | 17 | if (files.length === 0) { 18 | console.log('No templates found. Passing to next test.'); 19 | return; 20 | } 21 | 22 | await Promise.all(files.map(async (filePath) => { 23 | try { 24 | await cloudformation.validateTemplate({ 25 | TemplateBody: fs.readFileSync(filePath, 26 | { encoding: 'utf8', flag: 'r' }), 27 | }).promise(); 28 | } catch (error) { 29 | throw (new Error(`template path: ${filePath}\n${error}`)); 30 | } 31 | })); 32 | }); 33 | 34 | test('Should validate CloudFormation stack status', async () => { 35 | let directoryPath = path.join(__dirname, '../../amplify/team-provider-info.json'); 36 | if (process.env.NODE_ENV !== 'test' && process.env.AMP_PATH) { 37 | directoryPath = path.join(__dirname, `../../${process.env.AMP_PATH}/amplify/team-provider-info.json`); 38 | } 39 | const teamProvider = JSON.parse(fs.readFileSync(directoryPath, 'utf8')); 40 | const stackName = teamProvider.dev.awscloudformation.StackName; 41 | 42 | const stacksDescription = await cloudformation.describeStacks({ StackName: stackName }).promise(); 43 | const stackStatus = stacksDescription.Stacks[0].StackStatus; 44 | try { 45 | expect(stackStatus).toBe('UPDATE_COMPLETE'); 46 | } catch (e) { 47 | expect(stackStatus).toBe('CREATE_COMPLETE'); 48 | } 49 | }); 50 | -------------------------------------------------------------------------------- /tests/service-questions.test.js: -------------------------------------------------------------------------------- 1 | const ivs = require('../provider-utils/awscloudformation/service-walkthroughs/ivs-push'); 2 | const livestream = require('../provider-utils/awscloudformation/service-walkthroughs/ivs-push'); 3 | 4 | const context = { 5 | amplify: { 6 | getProjectMeta: jest.fn(() => ({ 7 | providers: { 8 | awscloudformation: { 9 | DeploymentBucketName: 'test', 10 | }, 11 | }, 12 | })), 13 | inputValidation: jest.fn(() => true), 14 | }, 15 | parameters: { 16 | options: { 17 | 18 | }, 19 | }, 20 | }; 21 | 22 | test('Should return the default props with correct resource name for IVS', async () => { 23 | const { shared } = await ivs.serviceQuestions(context, '', '', 'ivs'); 24 | expect(shared).toMatchObject({ 25 | resourceName: 'ivs', 26 | bucket: 'test', 27 | }); 28 | }); 29 | 30 | test('Should return the default props with correct resource name for Elemental Livestream service', async () => { 31 | const { shared } = await livestream.serviceQuestions(context, '', '', 'elemental'); 32 | expect(shared).toMatchObject({ 33 | resourceName: 'elemental', 34 | bucket: 'test', 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/video-player.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const videoPlayerUtils = require('../provider-utils/awscloudformation/utils/video-player-utils'); 3 | 4 | const mocks = { 5 | podfile: { 6 | target_definitions: [ 7 | { 8 | name: 'Pods', 9 | abstract: true, 10 | children: [ 11 | { 12 | name: 'iOSVideoPlayer', 13 | uses_frameworks: { linkage: 'dynamic', packaging: 'framework' }, 14 | platform: { ios: '8.4' }, 15 | dependencies: [ 16 | { MobileVLCKit: ['~>3.3.0'] }, 17 | 'AmazonIVS'], 18 | }, 19 | ], 20 | }, 21 | ], 22 | }, 23 | podfile_no_dep: { 24 | target_definitions: [ 25 | { 26 | name: 'Pods', 27 | abstract: true, 28 | children: [ 29 | { 30 | name: 'iOSVideoPlayer', 31 | uses_frameworks: { linkage: 'dynamic', packaging: 'framework' }, 32 | }, 33 | ], 34 | }, 35 | ], 36 | }, 37 | podfile_simple_dep: { 38 | target_definitions: [ 39 | { 40 | name: 'Pods', 41 | abstract: true, 42 | children: [ 43 | { 44 | name: 'iOSVideoPlayer', 45 | uses_frameworks: { linkage: 'dynamic', packaging: 'framework' }, 46 | platform: { ios: '8.4' }, 47 | dependencies: ['AmazonIVS'], 48 | }, 49 | ], 50 | }, 51 | ], 52 | }, 53 | 54 | }; 55 | 56 | describe('isVLCKitInstalled', () => { 57 | test('Should return true if dependency is installed', () => { 58 | expect(videoPlayerUtils.isDependencyInstalled(mocks.podfile, 'iOSVideoPlayer', 'AmazonIVS')).toBe(true); 59 | expect(videoPlayerUtils.isDependencyInstalled(mocks.podfile, 'iOSVideoPlayer', 'MobileVLCKit')).toBe(true); 60 | expect(videoPlayerUtils.isDependencyInstalled(mocks.podfile_simple_dep, 'iOSVideoPlayer', 'AmazonIVS')).toBe(true); 61 | }); 62 | 63 | test('Should return false if project name does not exist', () => { 64 | expect(videoPlayerUtils.isDependencyInstalled(mocks.podfile, 'anotherProjectName', 'MobileVLCKit')).toBe(false); 65 | }); 66 | 67 | test('Should return false dependencies array does not exist', () => { 68 | expect(videoPlayerUtils.isDependencyInstalled(mocks.podfile_no_dep, 'iOSVideoPlayer', 'MobileVLCKit')).toBe(false); 69 | expect(videoPlayerUtils.isDependencyInstalled(mocks.podfile_simple_dep, 'iOSVideoPlayer', 'MobileVLCKit')).toBe(false); 70 | }); 71 | }); 72 | 73 | describe('checkNpmDependencies', () => { 74 | test('Should return true if video.js is installed', () => { 75 | const context = { 76 | amplify: { 77 | pathManager: { 78 | searchProjectRootPath: jest.fn(() => `${__dirname}/../__mocks__`), 79 | }, 80 | readJsonFile: jest.fn((data) => JSON.parse(fs.readFileSync(data))), 81 | }, 82 | }; 83 | expect(videoPlayerUtils.checkNpmDependencies(context, 'video.js')).toBe(true); 84 | }); 85 | 86 | test('Should return false if dependency is not installed', () => { 87 | const context = { 88 | amplify: { 89 | pathManager: { 90 | searchProjectRootPath: jest.fn(() => `${__dirname}/../__mocks__`), 91 | }, 92 | readJsonFile: jest.fn((data) => JSON.parse(fs.readFileSync(data))), 93 | }, 94 | }; 95 | expect(videoPlayerUtils.checkNpmDependencies(context, 'random_lib')).toBe(false); 96 | }); 97 | }); 98 | 99 | describe('getServiceUrl', () => { 100 | test('Should return livestream oPrimaryMediaStoreEgressUrl', () => { 101 | const amplifyVideoMeta = { 102 | serviceType: 'livestream', 103 | output: { 104 | oPrimaryMediaStoreEgressUrl: 'test', 105 | }, 106 | }; 107 | expect(videoPlayerUtils.getServiceUrl(amplifyVideoMeta)).toBe('test'); 108 | }); 109 | 110 | test('Should return ivs oVideoOutput', () => { 111 | const amplifyVideoMeta = { 112 | serviceType: 'ivs', 113 | output: { 114 | oVideoOutput: 'test', 115 | }, 116 | }; 117 | expect(videoPlayerUtils.getServiceUrl(amplifyVideoMeta)).toBe('test'); 118 | }); 119 | 120 | test('Should return vod oVideoOutput', () => { 121 | const amplifyVideoMeta = { 122 | serviceType: 'video-on-demand', 123 | output: { 124 | oVodOutputUrl: 'test', 125 | }, 126 | }; 127 | expect(videoPlayerUtils.getServiceUrl(amplifyVideoMeta)).toBe('https://test/{path}/{path.m3u8}'); 128 | }); 129 | 130 | test('Should return vod oVODOutputS3', () => { 131 | const amplifyVideoMeta = { 132 | serviceType: 'video-on-demand', 133 | output: { 134 | oVODOutputS3: 'test', 135 | }, 136 | }; 137 | expect(videoPlayerUtils.getServiceUrl(amplifyVideoMeta)).toBe('test'); 138 | }); 139 | 140 | test('Should return undefined', () => { 141 | const amplifyVideoMeta = { 142 | serviceType: '', 143 | }; 144 | expect(videoPlayerUtils.getServiceUrl(amplifyVideoMeta)).toBe(undefined); 145 | }); 146 | }); 147 | 148 | describe('fileExtension', () => { 149 | test('Should return jsx for react', () => { 150 | expect(videoPlayerUtils.fileExtension('react')).toBe('jsx'); 151 | }); 152 | 153 | test('Should return vue for vue', () => { 154 | expect(videoPlayerUtils.fileExtension('vue')).toBe('vue'); 155 | }); 156 | 157 | test('Should return ts for angular', () => { 158 | expect(videoPlayerUtils.fileExtension('angular')).toBe('ts'); 159 | }); 160 | 161 | test('Should return js for ember', () => { 162 | expect(videoPlayerUtils.fileExtension('ember')).toBe('js'); 163 | }); 164 | 165 | test('Should return ts for ionic', () => { 166 | expect(videoPlayerUtils.fileExtension('ionic')).toBe('ts'); 167 | }); 168 | 169 | test('Should return swift for ios', () => { 170 | expect(videoPlayerUtils.fileExtension('ios')).toBe('swift'); 171 | }); 172 | }); 173 | 174 | describe('getProjectIndexHTMLPath', () => { 175 | const context = { 176 | amplify: { 177 | pathManager: { 178 | searchProjectRootPath: jest.fn(() => `${__dirname}/../__mocks__`), 179 | getProjectConfigFilePath: jest.fn(() => `${__dirname}/../__mocks__/project-config.json`) 180 | }, 181 | readJsonFile: jest.fn((data) => JSON.parse(fs.readFileSync(data))), 182 | }, 183 | }; 184 | test('Should return correct static path including index.html', () => { 185 | expect(videoPlayerUtils.getProjectIndexHTMLPath(context)) 186 | .toBe(`${__dirname}/../__mocks__/public/index.html`); 187 | }) 188 | }) 189 | --------------------------------------------------------------------------------