├── .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 |
6 | src: '<%= src %>',
7 | techOrder:['AmazonIVS']}">
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 |
53 |
54 |
55 |
56 |
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 |
2 |
3 |
4 |
5 |
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 |
--------------------------------------------------------------------------------