├── .eslintrc.json ├── .gitattributes ├── .github ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── auto-approve.yml │ ├── build.yml │ ├── codeql-analysis.yml │ ├── pull-request-lint.yml │ ├── upgrade-site.yml │ └── upgrade.yml ├── .gitignore ├── .mergify.yml ├── .npmignore ├── .projen ├── deps.json ├── files.json └── tasks.json ├── .projenrc.ts ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cdk.json ├── images ├── AmazonChimeSDKMeetingWithTranscribeOverview.png ├── BackendOverview.png ├── MeetingDemo.png ├── PostMeeting.png ├── RecordingDemo.png └── TranscribeExample.png ├── package.json ├── site ├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── README.md ├── package.json ├── public │ ├── index.html │ └── robots.txt ├── src │ ├── App.css │ ├── App.js │ ├── Config.js │ ├── Meeting.js │ ├── Navigation.js │ ├── Recordings.js │ ├── index.css │ └── index.js ├── webpack.config.js └── yarn.lock ├── src ├── amazon-chime-sdk-meeting-with-transcribe.ts ├── cognito.ts ├── index.ts ├── infrastructure.ts ├── media-pipeline.ts ├── resources │ ├── cognitoDomain │ │ └── domainValidator.js │ ├── eventBridge │ │ └── eventBridge.ts │ ├── meetingInfo │ │ └── meetingInfo.ts │ └── postMeeting │ │ └── postMeeting.ts └── site.ts ├── tsconfig.dev.json ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | { 3 | "env": { 4 | "jest": true, 5 | "node": true 6 | }, 7 | "root": true, 8 | "plugins": [ 9 | "@typescript-eslint", 10 | "import" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": 2018, 15 | "sourceType": "module", 16 | "project": "./tsconfig.dev.json" 17 | }, 18 | "extends": [ 19 | "plugin:import/typescript" 20 | ], 21 | "settings": { 22 | "import/parsers": { 23 | "@typescript-eslint/parser": [ 24 | ".ts", 25 | ".tsx" 26 | ] 27 | }, 28 | "import/resolver": { 29 | "node": {}, 30 | "typescript": { 31 | "project": "./tsconfig.dev.json", 32 | "alwaysTryTypes": true 33 | } 34 | } 35 | }, 36 | "ignorePatterns": [ 37 | "*.js", 38 | "*.d.ts", 39 | "node_modules/", 40 | "*.generated.ts", 41 | "coverage", 42 | "!.projenrc.ts", 43 | "!projenrc/**/*.ts" 44 | ], 45 | "rules": { 46 | "indent": [ 47 | "off" 48 | ], 49 | "@typescript-eslint/indent": [ 50 | "error", 51 | 2 52 | ], 53 | "quotes": [ 54 | "error", 55 | "single", 56 | { 57 | "avoidEscape": true 58 | } 59 | ], 60 | "comma-dangle": [ 61 | "error", 62 | "always-multiline" 63 | ], 64 | "comma-spacing": [ 65 | "error", 66 | { 67 | "before": false, 68 | "after": true 69 | } 70 | ], 71 | "no-multi-spaces": [ 72 | "error", 73 | { 74 | "ignoreEOLComments": false 75 | } 76 | ], 77 | "array-bracket-spacing": [ 78 | "error", 79 | "never" 80 | ], 81 | "array-bracket-newline": [ 82 | "error", 83 | "consistent" 84 | ], 85 | "object-curly-spacing": [ 86 | "error", 87 | "always" 88 | ], 89 | "object-curly-newline": [ 90 | "error", 91 | { 92 | "multiline": true, 93 | "consistent": true 94 | } 95 | ], 96 | "object-property-newline": [ 97 | "error", 98 | { 99 | "allowAllPropertiesOnSameLine": true 100 | } 101 | ], 102 | "keyword-spacing": [ 103 | "error" 104 | ], 105 | "brace-style": [ 106 | "error", 107 | "1tbs", 108 | { 109 | "allowSingleLine": true 110 | } 111 | ], 112 | "space-before-blocks": [ 113 | "error" 114 | ], 115 | "curly": [ 116 | "error", 117 | "multi-line", 118 | "consistent" 119 | ], 120 | "@typescript-eslint/member-delimiter-style": [ 121 | "error" 122 | ], 123 | "semi": [ 124 | "error", 125 | "always" 126 | ], 127 | "max-len": [ 128 | "error", 129 | { 130 | "code": 150, 131 | "ignoreUrls": true, 132 | "ignoreStrings": true, 133 | "ignoreTemplateLiterals": true, 134 | "ignoreComments": true, 135 | "ignoreRegExpLiterals": true 136 | } 137 | ], 138 | "quote-props": [ 139 | "error", 140 | "consistent-as-needed" 141 | ], 142 | "@typescript-eslint/no-require-imports": [ 143 | "error" 144 | ], 145 | "import/no-extraneous-dependencies": [ 146 | "error", 147 | { 148 | "devDependencies": [ 149 | "**/test/**", 150 | "**/build-tools/**", 151 | ".projenrc.ts", 152 | "projenrc/**/*.ts" 153 | ], 154 | "optionalDependencies": false, 155 | "peerDependencies": true 156 | } 157 | ], 158 | "import/no-unresolved": [ 159 | "error" 160 | ], 161 | "import/order": [ 162 | "warn", 163 | { 164 | "groups": [ 165 | "builtin", 166 | "external" 167 | ], 168 | "alphabetize": { 169 | "order": "asc", 170 | "caseInsensitive": true 171 | } 172 | } 173 | ], 174 | "import/no-duplicates": [ 175 | "error" 176 | ], 177 | "no-shadow": [ 178 | "off" 179 | ], 180 | "@typescript-eslint/no-shadow": [ 181 | "error" 182 | ], 183 | "key-spacing": [ 184 | "error" 185 | ], 186 | "no-multiple-empty-lines": [ 187 | "error" 188 | ], 189 | "@typescript-eslint/no-floating-promises": [ 190 | "error" 191 | ], 192 | "no-return-await": [ 193 | "off" 194 | ], 195 | "@typescript-eslint/return-await": [ 196 | "error" 197 | ], 198 | "no-trailing-spaces": [ 199 | "error" 200 | ], 201 | "dot-notation": [ 202 | "error" 203 | ], 204 | "no-bitwise": [ 205 | "error" 206 | ], 207 | "@typescript-eslint/member-ordering": [ 208 | "error", 209 | { 210 | "default": [ 211 | "public-static-field", 212 | "public-static-method", 213 | "protected-static-field", 214 | "protected-static-method", 215 | "private-static-field", 216 | "private-static-method", 217 | "field", 218 | "constructor", 219 | "method" 220 | ] 221 | } 222 | ] 223 | }, 224 | "overrides": [ 225 | { 226 | "files": [ 227 | ".projenrc.ts" 228 | ], 229 | "rules": { 230 | "@typescript-eslint/no-require-imports": "off", 231 | "import/no-extraneous-dependencies": "off" 232 | } 233 | } 234 | ] 235 | } 236 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | /.eslintrc.json linguist-generated 4 | /.gitattributes linguist-generated 5 | /.github/pull_request_template.md linguist-generated 6 | /.github/workflows/auto-approve.yml linguist-generated 7 | /.github/workflows/build.yml linguist-generated 8 | /.github/workflows/pull-request-lint.yml linguist-generated 9 | /.github/workflows/upgrade-site.yml linguist-generated 10 | /.github/workflows/upgrade.yml linguist-generated 11 | /.gitignore linguist-generated 12 | /.mergify.yml linguist-generated 13 | /.npmignore linguist-generated 14 | /.projen/** linguist-generated 15 | /.projen/deps.json linguist-generated 16 | /.projen/files.json linguist-generated 17 | /.projen/tasks.json linguist-generated 18 | /cdk.json linguist-generated 19 | /LICENSE linguist-generated 20 | /package.json linguist-generated 21 | /tsconfig.dev.json linguist-generated 22 | /tsconfig.json linguist-generated 23 | /yarn.lock linguist-generated -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: yarn 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Fixes # -------------------------------------------------------------------------------- /.github/workflows/auto-approve.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: auto-approve 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | jobs: 13 | approve: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | pull-requests: write 17 | if: contains(github.event.pull_request.labels.*.name, 'auto-approve') && (github.event.pull_request.user.login == 'schuettc') 18 | steps: 19 | - uses: hmarr/auto-approve-action@v2.2.1 20 | with: 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: build 4 | on: 5 | pull_request: {} 6 | workflow_dispatch: {} 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | outputs: 13 | self_mutation_happened: ${{ steps.self_mutation.outputs.self_mutation_happened }} 14 | env: 15 | CI: "true" 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: ${{ github.event.pull_request.head.ref }} 21 | repository: ${{ github.event.pull_request.head.repo.full_name }} 22 | - name: Install dependencies 23 | run: yarn install --check-files 24 | - name: build 25 | run: npx projen build 26 | - name: Find mutations 27 | id: self_mutation 28 | run: |- 29 | git add . 30 | git diff --staged --patch --exit-code > .repo.patch || echo "self_mutation_happened=true" >> $GITHUB_OUTPUT 31 | working-directory: ./ 32 | - name: Upload patch 33 | if: steps.self_mutation.outputs.self_mutation_happened 34 | uses: actions/upload-artifact@v4 35 | with: 36 | name: .repo.patch 37 | path: .repo.patch 38 | overwrite: true 39 | - name: Fail build on mutation 40 | if: steps.self_mutation.outputs.self_mutation_happened 41 | run: |- 42 | echo "::error::Files were changed during build (see build log). If this was triggered from a fork, you will need to update your branch." 43 | cat .repo.patch 44 | exit 1 45 | self-mutation: 46 | needs: build 47 | runs-on: ubuntu-latest 48 | permissions: 49 | contents: write 50 | if: always() && needs.build.outputs.self_mutation_happened && !(github.event.pull_request.head.repo.full_name != github.repository) 51 | steps: 52 | - name: Checkout 53 | uses: actions/checkout@v4 54 | with: 55 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 56 | ref: ${{ github.event.pull_request.head.ref }} 57 | repository: ${{ github.event.pull_request.head.repo.full_name }} 58 | - name: Download patch 59 | uses: actions/download-artifact@v4 60 | with: 61 | name: .repo.patch 62 | path: ${{ runner.temp }} 63 | - name: Apply patch 64 | run: '[ -s ${{ runner.temp }}/.repo.patch ] && git apply ${{ runner.temp }}/.repo.patch || echo "Empty patch. Skipping."' 65 | - name: Set git identity 66 | run: |- 67 | git config user.name "github-actions" 68 | git config user.email "github-actions@github.com" 69 | - name: Push changes 70 | env: 71 | PULL_REQUEST_REF: ${{ github.event.pull_request.head.ref }} 72 | run: |- 73 | git add . 74 | git commit -s -m "chore: self mutation" 75 | git push origin HEAD:$PULL_REQUEST_REF 76 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '39 21 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-lint.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: pull-request-lint 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | - edited 13 | jobs: 14 | validate: 15 | name: Validate PR title 16 | runs-on: ubuntu-latest 17 | permissions: 18 | pull-requests: write 19 | steps: 20 | - uses: amannn/action-semantic-pull-request@v5.4.0 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | types: |- 25 | feat 26 | fix 27 | chore 28 | requireScope: false 29 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-site.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: upgrade-site 4 | on: 5 | schedule: 6 | - cron: 0 5 * * 1 7 | workflow_dispatch: {} 8 | jobs: 9 | upgradeSite: 10 | name: upgrade-site 11 | runs-on: ubuntu-latest 12 | permissions: 13 | actions: write 14 | contents: read 15 | id-token: write 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Setup Node.js 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: "18" 22 | - run: yarn install --check-files --frozen-lockfile 23 | working-directory: site 24 | - run: yarn upgrade 25 | working-directory: site 26 | - name: Create Pull Request 27 | uses: peter-evans/create-pull-request@v4 28 | with: 29 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 30 | commit-message: "chore: upgrade site" 31 | branch: auto/projen-upgrade 32 | title: "chore: upgrade site" 33 | body: This PR upgrades site 34 | labels: auto-merge, auto-approve 35 | author: github-actions 36 | committer: github-actions 37 | signoff: true 38 | -------------------------------------------------------------------------------- /.github/workflows/upgrade.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: upgrade 4 | on: 5 | workflow_dispatch: {} 6 | schedule: 7 | - cron: 0 0 * * 1 8 | jobs: 9 | upgrade: 10 | name: Upgrade 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | outputs: 15 | patch_created: ${{ steps.create_patch.outputs.patch_created }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | - name: Install dependencies 20 | run: yarn install --check-files --frozen-lockfile 21 | - name: Upgrade dependencies 22 | run: npx projen upgrade 23 | - name: Find mutations 24 | id: create_patch 25 | run: |- 26 | git add . 27 | git diff --staged --patch --exit-code > .repo.patch || echo "patch_created=true" >> $GITHUB_OUTPUT 28 | working-directory: ./ 29 | - name: Upload patch 30 | if: steps.create_patch.outputs.patch_created 31 | uses: actions/upload-artifact@v4 32 | with: 33 | name: .repo.patch 34 | path: .repo.patch 35 | overwrite: true 36 | pr: 37 | name: Create Pull Request 38 | needs: upgrade 39 | runs-on: ubuntu-latest 40 | permissions: 41 | contents: read 42 | if: ${{ needs.upgrade.outputs.patch_created }} 43 | steps: 44 | - name: Checkout 45 | uses: actions/checkout@v4 46 | - name: Download patch 47 | uses: actions/download-artifact@v4 48 | with: 49 | name: .repo.patch 50 | path: ${{ runner.temp }} 51 | - name: Apply patch 52 | run: '[ -s ${{ runner.temp }}/.repo.patch ] && git apply ${{ runner.temp }}/.repo.patch || echo "Empty patch. Skipping."' 53 | - name: Set git identity 54 | run: |- 55 | git config user.name "github-actions" 56 | git config user.email "github-actions@github.com" 57 | - name: Create Pull Request 58 | id: create-pr 59 | uses: peter-evans/create-pull-request@v6 60 | with: 61 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 62 | commit-message: |- 63 | chore(deps): upgrade dependencies 64 | 65 | Upgrades project dependencies. See details in [workflow run]. 66 | 67 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 68 | 69 | ------ 70 | 71 | *Automatically created by projen via the "upgrade" workflow* 72 | branch: github-actions/upgrade 73 | title: "chore(deps): upgrade dependencies" 74 | labels: auto-approve,auto-merge 75 | body: |- 76 | Upgrades project dependencies. See details in [workflow run]. 77 | 78 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 79 | 80 | ------ 81 | 82 | *Automatically created by projen via the "upgrade" workflow* 83 | author: github-actions 84 | committer: github-actions 85 | signoff: true 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | !/.gitattributes 3 | !/.projen/tasks.json 4 | !/.projen/deps.json 5 | !/.projen/files.json 6 | !/.github/workflows/pull-request-lint.yml 7 | !/.github/workflows/auto-approve.yml 8 | !/package.json 9 | !/LICENSE 10 | !/.npmignore 11 | logs 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | lerna-debug.log* 17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | lib-cov 23 | coverage 24 | *.lcov 25 | .nyc_output 26 | build/Release 27 | node_modules/ 28 | jspm_packages/ 29 | *.tsbuildinfo 30 | .eslintcache 31 | *.tgz 32 | .yarn-integrity 33 | .cache 34 | !/.github/workflows/build.yml 35 | !/.mergify.yml 36 | !/.github/workflows/upgrade.yml 37 | !/.github/pull_request_template.md 38 | !/test/ 39 | !/tsconfig.json 40 | !/tsconfig.dev.json 41 | !/src/ 42 | /lib 43 | /dist/ 44 | !/.eslintrc.json 45 | /assets/ 46 | !/cdk.json 47 | /cdk.out/ 48 | .cdk.staging/ 49 | .parcel-cache/ 50 | !/.github/workflows/upgrade-site.yml 51 | cdk.out 52 | cdk.context.json 53 | yarn-error.log 54 | dependabot.yml 55 | *.drawio 56 | .DS_Store 57 | !/.projenrc.ts 58 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | queue_rules: 4 | - name: default 5 | update_method: merge 6 | conditions: 7 | - "#approved-reviews-by>=1" 8 | - -label~=(do-not-merge) 9 | - status-success=build 10 | pull_request_rules: 11 | - name: Automatic merge on approval and successful build 12 | actions: 13 | delete_head_branch: {} 14 | queue: 15 | method: squash 16 | name: default 17 | commit_message_template: |- 18 | {{ title }} (#{{ number }}) 19 | 20 | {{ body }} 21 | conditions: 22 | - "#approved-reviews-by>=1" 23 | - -label~=(do-not-merge) 24 | - status-success=build 25 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | /.projen/ 3 | permissions-backup.acl 4 | /.mergify.yml 5 | /test/ 6 | /tsconfig.dev.json 7 | /src/ 8 | !/lib/ 9 | !/lib/**/*.js 10 | !/lib/**/*.d.ts 11 | dist 12 | /tsconfig.json 13 | /.github/ 14 | /.vscode/ 15 | /.idea/ 16 | /.projenrc.js 17 | tsconfig.tsbuildinfo 18 | /.eslintrc.json 19 | !/assets/ 20 | cdk.out/ 21 | .cdk.staging/ 22 | /.gitattributes 23 | /.projenrc.ts 24 | /projenrc 25 | -------------------------------------------------------------------------------- /.projen/deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "@types/node", 5 | "version": "^18", 6 | "type": "build" 7 | }, 8 | { 9 | "name": "@typescript-eslint/eslint-plugin", 10 | "version": "^7", 11 | "type": "build" 12 | }, 13 | { 14 | "name": "@typescript-eslint/parser", 15 | "version": "^7", 16 | "type": "build" 17 | }, 18 | { 19 | "name": "aws-cdk", 20 | "version": "^2.101.0", 21 | "type": "build" 22 | }, 23 | { 24 | "name": "esbuild", 25 | "type": "build" 26 | }, 27 | { 28 | "name": "eslint-import-resolver-typescript", 29 | "type": "build" 30 | }, 31 | { 32 | "name": "eslint-plugin-import", 33 | "type": "build" 34 | }, 35 | { 36 | "name": "eslint", 37 | "version": "^8", 38 | "type": "build" 39 | }, 40 | { 41 | "name": "projen", 42 | "type": "build" 43 | }, 44 | { 45 | "name": "ts-node", 46 | "type": "build" 47 | }, 48 | { 49 | "name": "typescript", 50 | "type": "build" 51 | }, 52 | { 53 | "name": "@aws-sdk/client-chime-sdk-media-pipelines", 54 | "type": "runtime" 55 | }, 56 | { 57 | "name": "@aws-sdk/client-chime-sdk-meetings", 58 | "type": "runtime" 59 | }, 60 | { 61 | "name": "@aws-sdk/client-dynamodb", 62 | "type": "runtime" 63 | }, 64 | { 65 | "name": "@aws-sdk/lib-dynamodb", 66 | "type": "runtime" 67 | }, 68 | { 69 | "name": "@types/aws-lambda", 70 | "type": "runtime" 71 | }, 72 | { 73 | "name": "@types/fs-extra", 74 | "type": "runtime" 75 | }, 76 | { 77 | "name": "aws-cdk-lib", 78 | "version": "^2.101.0", 79 | "type": "runtime" 80 | }, 81 | { 82 | "name": "aws-lambda", 83 | "type": "runtime" 84 | }, 85 | { 86 | "name": "constructs", 87 | "version": "^10.0.5", 88 | "type": "runtime" 89 | }, 90 | { 91 | "name": "fs-extra", 92 | "type": "runtime" 93 | } 94 | ], 95 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 96 | } 97 | -------------------------------------------------------------------------------- /.projen/files.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | ".eslintrc.json", 4 | ".gitattributes", 5 | ".github/pull_request_template.md", 6 | ".github/workflows/auto-approve.yml", 7 | ".github/workflows/build.yml", 8 | ".github/workflows/pull-request-lint.yml", 9 | ".github/workflows/upgrade-site.yml", 10 | ".github/workflows/upgrade.yml", 11 | ".gitignore", 12 | ".mergify.yml", 13 | ".npmignore", 14 | ".projen/deps.json", 15 | ".projen/files.json", 16 | ".projen/tasks.json", 17 | "cdk.json", 18 | "LICENSE", 19 | "tsconfig.dev.json", 20 | "tsconfig.json" 21 | ], 22 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 23 | } 24 | -------------------------------------------------------------------------------- /.projen/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "build": { 4 | "name": "build", 5 | "description": "Full release build", 6 | "steps": [ 7 | { 8 | "spawn": "default" 9 | }, 10 | { 11 | "spawn": "pre-compile" 12 | }, 13 | { 14 | "spawn": "compile" 15 | }, 16 | { 17 | "spawn": "post-compile" 18 | }, 19 | { 20 | "spawn": "test" 21 | }, 22 | { 23 | "spawn": "package" 24 | } 25 | ] 26 | }, 27 | "bundle": { 28 | "name": "bundle", 29 | "description": "Prepare assets" 30 | }, 31 | "clobber": { 32 | "name": "clobber", 33 | "description": "hard resets to HEAD of origin and cleans the local repo", 34 | "env": { 35 | "BRANCH": "$(git branch --show-current)" 36 | }, 37 | "steps": [ 38 | { 39 | "exec": "git checkout -b scratch", 40 | "name": "save current HEAD in \"scratch\" branch" 41 | }, 42 | { 43 | "exec": "git checkout $BRANCH" 44 | }, 45 | { 46 | "exec": "git fetch origin", 47 | "name": "fetch latest changes from origin" 48 | }, 49 | { 50 | "exec": "git reset --hard origin/$BRANCH", 51 | "name": "hard reset to origin commit" 52 | }, 53 | { 54 | "exec": "git clean -fdx", 55 | "name": "clean all untracked files" 56 | }, 57 | { 58 | "say": "ready to rock! (unpushed commits are under the \"scratch\" branch)" 59 | } 60 | ], 61 | "condition": "git diff --exit-code > /dev/null" 62 | }, 63 | "compile": { 64 | "name": "compile", 65 | "description": "Only compile" 66 | }, 67 | "configLocal": { 68 | "name": "configLocal", 69 | "steps": [ 70 | { 71 | "exec": "aws s3 cp s3://$(yarn run --silent getBucket)/config.json site/public/" 72 | } 73 | ] 74 | }, 75 | "default": { 76 | "name": "default", 77 | "description": "Synthesize project files", 78 | "steps": [ 79 | { 80 | "exec": "ts-node --project tsconfig.dev.json .projenrc.ts" 81 | } 82 | ] 83 | }, 84 | "deploy": { 85 | "name": "deploy", 86 | "description": "Deploys your CDK app to the AWS cloud", 87 | "steps": [ 88 | { 89 | "exec": "cdk deploy", 90 | "receiveArgs": true 91 | } 92 | ] 93 | }, 94 | "destroy": { 95 | "name": "destroy", 96 | "description": "Destroys your cdk app in the AWS cloud", 97 | "steps": [ 98 | { 99 | "exec": "cdk destroy", 100 | "receiveArgs": true 101 | } 102 | ] 103 | }, 104 | "diff": { 105 | "name": "diff", 106 | "description": "Diffs the currently deployed app against your code", 107 | "steps": [ 108 | { 109 | "exec": "cdk diff" 110 | } 111 | ] 112 | }, 113 | "eject": { 114 | "name": "eject", 115 | "description": "Remove projen from the project", 116 | "env": { 117 | "PROJEN_EJECTING": "true" 118 | }, 119 | "steps": [ 120 | { 121 | "spawn": "default" 122 | } 123 | ] 124 | }, 125 | "eslint": { 126 | "name": "eslint", 127 | "description": "Runs eslint against the codebase", 128 | "steps": [ 129 | { 130 | "exec": "eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern $@ src test build-tools projenrc .projenrc.ts", 131 | "receiveArgs": true 132 | } 133 | ] 134 | }, 135 | "getBucket": { 136 | "name": "getBucket", 137 | "steps": [ 138 | { 139 | "exec": "aws cloudformation describe-stacks --stack-name AmazonChimeSDKWithTranscribe --query 'Stacks[0].Outputs[?OutputKey==`siteBucket`].OutputValue' --output text" 140 | } 141 | ] 142 | }, 143 | "install": { 144 | "name": "install", 145 | "description": "Install project dependencies and update lockfile (non-frozen)", 146 | "steps": [ 147 | { 148 | "exec": "yarn install --check-files" 149 | } 150 | ] 151 | }, 152 | "install:ci": { 153 | "name": "install:ci", 154 | "description": "Install project dependencies using frozen lockfile", 155 | "steps": [ 156 | { 157 | "exec": "yarn install --check-files --frozen-lockfile" 158 | } 159 | ] 160 | }, 161 | "launch": { 162 | "name": "launch", 163 | "steps": [ 164 | { 165 | "exec": "yarn && yarn projen && yarn build && yarn cdk bootstrap && yarn cdk deploy && yarn configLocal" 166 | } 167 | ] 168 | }, 169 | "package": { 170 | "name": "package", 171 | "description": "Creates the distribution package" 172 | }, 173 | "post-compile": { 174 | "name": "post-compile", 175 | "description": "Runs after successful compilation", 176 | "steps": [ 177 | { 178 | "spawn": "synth:silent" 179 | } 180 | ] 181 | }, 182 | "post-upgrade": { 183 | "name": "post-upgrade", 184 | "description": "Runs after upgrading dependencies" 185 | }, 186 | "pre-compile": { 187 | "name": "pre-compile", 188 | "description": "Prepare the project for compilation" 189 | }, 190 | "synth": { 191 | "name": "synth", 192 | "description": "Synthesizes your cdk app into cdk.out", 193 | "steps": [ 194 | { 195 | "exec": "cdk synth" 196 | } 197 | ] 198 | }, 199 | "synth:silent": { 200 | "name": "synth:silent", 201 | "description": "Synthesizes your cdk app into cdk.out and suppresses the template in stdout (part of \"yarn build\")", 202 | "steps": [ 203 | { 204 | "exec": "cdk synth -q" 205 | } 206 | ] 207 | }, 208 | "test": { 209 | "name": "test", 210 | "description": "Run tests", 211 | "steps": [ 212 | { 213 | "spawn": "eslint" 214 | } 215 | ] 216 | }, 217 | "upgrade": { 218 | "name": "upgrade", 219 | "description": "upgrade dependencies", 220 | "env": { 221 | "CI": "0" 222 | }, 223 | "steps": [ 224 | { 225 | "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --dep=dev,peer,prod,optional --filter=esbuild,eslint-import-resolver-typescript,eslint-plugin-import,projen,ts-node,typescript,@aws-sdk/client-chime-sdk-media-pipelines,@aws-sdk/client-chime-sdk-meetings,@aws-sdk/client-dynamodb,@aws-sdk/lib-dynamodb,@types/aws-lambda,@types/fs-extra,aws-lambda,fs-extra" 226 | }, 227 | { 228 | "exec": "yarn install --check-files" 229 | }, 230 | { 231 | "exec": "yarn upgrade @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser aws-cdk esbuild eslint-import-resolver-typescript eslint-plugin-import eslint projen ts-node typescript @aws-sdk/client-chime-sdk-media-pipelines @aws-sdk/client-chime-sdk-meetings @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb @types/aws-lambda @types/fs-extra aws-cdk-lib aws-lambda constructs fs-extra" 232 | }, 233 | { 234 | "exec": "npx projen" 235 | }, 236 | { 237 | "spawn": "post-upgrade" 238 | } 239 | ] 240 | }, 241 | "watch": { 242 | "name": "watch", 243 | "description": "Watches changes in your source code and rebuilds and deploys to the current account", 244 | "steps": [ 245 | { 246 | "exec": "cdk deploy --hotswap" 247 | }, 248 | { 249 | "exec": "cdk watch" 250 | } 251 | ] 252 | } 253 | }, 254 | "env": { 255 | "PATH": "$(npx -c \"node --print process.env.PATH\")" 256 | }, 257 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 258 | } 259 | -------------------------------------------------------------------------------- /.projenrc.ts: -------------------------------------------------------------------------------- 1 | const { awscdk } = require('projen'); 2 | const { JobPermission } = require('projen/lib/github/workflows-model'); 3 | const { UpgradeDependenciesSchedule } = require('projen/lib/javascript'); 4 | const AUTOMATION_TOKEN = 'PROJEN_GITHUB_TOKEN'; 5 | 6 | const project = new awscdk.AwsCdkTypeScriptApp({ 7 | cdkVersion: '2.101.0', 8 | defaultReleaseBranch: 'main', 9 | name: 'amazon-chime-sdk-meeting-with-with-transcribe', 10 | appEntrypoint: 'amazon-chime-sdk-meeting-with-transcribe.ts', 11 | devDeps: ['esbuild'], 12 | jest: false, 13 | projenrcTs: true, 14 | deps: [ 15 | 'fs-extra', 16 | '@types/fs-extra', 17 | '@aws-sdk/client-chime-sdk-meetings', 18 | '@aws-sdk/client-chime-sdk-media-pipelines', 19 | '@aws-sdk/lib-dynamodb', 20 | '@aws-sdk/client-dynamodb', 21 | '@types/aws-lambda', 22 | 'aws-lambda', 23 | ], 24 | autoApproveOptions: { 25 | secret: 'GITHUB_TOKEN', 26 | allowedUsernames: ['schuettc'], 27 | }, 28 | depsUpgradeOptions: { 29 | ignoreProjen: false, 30 | workflowOptions: { 31 | labels: ['auto-approve', 'auto-merge'], 32 | schedule: UpgradeDependenciesSchedule.WEEKLY, 33 | }, 34 | }, 35 | scripts: { 36 | launch: 37 | 'yarn && yarn projen && yarn build && yarn cdk bootstrap && yarn cdk deploy && yarn configLocal', 38 | }, 39 | }); 40 | 41 | const upgradeSite = project.github.addWorkflow('upgrade-site'); 42 | upgradeSite.on({ schedule: [{ cron: '0 5 * * 1' }], workflowDispatch: {} }); 43 | upgradeSite.addJobs({ 44 | upgradeSite: { 45 | runsOn: ['ubuntu-latest'], 46 | name: 'upgrade-site', 47 | permissions: { 48 | actions: JobPermission.WRITE, 49 | contents: JobPermission.READ, 50 | idToken: JobPermission.WRITE, 51 | }, 52 | steps: [ 53 | { uses: 'actions/checkout@v3' }, 54 | { 55 | name: 'Setup Node.js', 56 | uses: 'actions/setup-node@v3', 57 | with: { 58 | 'node-version': '18', 59 | }, 60 | }, 61 | { 62 | run: 'yarn install --check-files --frozen-lockfile', 63 | workingDirectory: 'site', 64 | }, 65 | { 66 | run: 'yarn upgrade', 67 | workingDirectory: 'site', 68 | }, 69 | { 70 | name: 'Create Pull Request', 71 | uses: 'peter-evans/create-pull-request@v4', 72 | with: { 73 | 'token': '${{ secrets.' + AUTOMATION_TOKEN + ' }}', 74 | 'commit-message': 'chore: upgrade site', 75 | 'branch': 'auto/projen-upgrade', 76 | 'title': 'chore: upgrade site', 77 | 'body': 'This PR upgrades site', 78 | 'labels': 'auto-merge, auto-approve', 79 | 'author': 'github-actions ', 80 | 'committer': 'github-actions ', 81 | 'signoff': true, 82 | }, 83 | }, 84 | ], 85 | }, 86 | }); 87 | 88 | const common_exclude = [ 89 | 'cdk.out', 90 | 'cdk.context.json', 91 | 'yarn-error.log', 92 | 'dependabot.yml', 93 | '*.drawio', 94 | '.DS_Store', 95 | ]; 96 | 97 | project.addTask('getBucket', { 98 | exec: "aws cloudformation describe-stacks --stack-name AmazonChimeSDKWithTranscribe --query 'Stacks[0].Outputs[?OutputKey==`siteBucket`].OutputValue' --output text", 99 | }); 100 | 101 | project.addTask('configLocal', { 102 | exec: 'aws s3 cp s3://$(yarn run --silent getBucket)/config.json site/public/', 103 | }); 104 | 105 | project.gitignore.exclude(...common_exclude); 106 | project.synth(); 107 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true 3 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon Chime SDK Meetings with Transcribe and Media Capture Pipelines 2 | 3 | Amazon Chime SDK Meetings consist of two different components that work together to add real-time voice and video to an application. These components can be broken down into front end and back end components. The front end uses the Amazon Chime SDK while the back end uses the AWS SDK. This distinction will be important to understand how these two components work together. 4 | 5 | At the most basic level, AWS SDK is used to create and manage Amazon Chime SDK Meetings and the Amazon Chime SDK client libraries are used within the client application to connect to this meeting. In this demo, we will be deploying an AWS Cloud Development Kit (CDK) that uses these components to create a simple Amazon Chime SDK Meeting between two participants. Additionally, this meeting will use [Amazon Transcribe](https://aws.amazon.com/transcribe/) and [Amazon Chime SDK Media Pipelines](https://docs.aws.amazon.com/chime-sdk/latest/dg/media-pipelines.html) to create a transcription and recording of this meeting. Let's see how that works. 6 | 7 | ![Overview](images/AmazonChimeSDKMeetingWithTranscribeOverview.png) 8 | 9 | ## Backend 10 | 11 | We will be using the [AWS SDK for Javascript v3](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-chime-sdk-meetings/index.html) in the backend for five AWS SDK calls: [`CreateMeetingCommand`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-chime-sdk-meetings/classes/createmeetingcommand.html), [`CreateAttendeeCommand`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-chime-sdk-meetings/classes/createattendeecommand.html). [`StartMeetingTranscriptionCommand`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-chime-sdk-meetings/classes/startmeetingtranscriptioncommand.html), [`CreateMediaCapturePipelineCommand`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-chime/classes/createmediacapturepipelinecommand.html), and [`CreateMediaConcatenationPipelineCommand`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-chime-sdk-media-pipelines/classes/createmediaconcatenationpipelinecommand.html) 12 | 13 | `CreateMeetingCommand`, `CreateAttendeeCommand`, `StartMeetingTranscriptionCommand`, and `CreateMediaCapturePipelineCommand` will be used in the [meetingInfo.ts](src/resources/meetingInfo/meetingInfo.ts) AWS Lambda function that is triggered by an API POST from the client. `CreateMediaConcatenationPipelineCommand` will be used in [`eventBridge.ts`](src/resources/eventBridge/eventBridge.ts) that is triggered by [Amazon EventBridge](https://aws.amazon.com/eventbridge/). We will look at these in more depth later. 14 | 15 | ### Create Meeting 16 | 17 | ```typescript 18 | var createMeetingCommandInput: CreateMeetingCommandInput = { 19 | ClientRequestToken: '', 20 | ExternalMeetingId: '', 21 | MediaRegion: 'us-east-1', 22 | }; 23 | 24 | async function createMeeting() { 25 | console.log('Creating Meeting'); 26 | createMeetingCommandInput.ClientRequestToken = randomUUID(); 27 | createMeetingCommandInput.ExternalMeetingId = randomUUID(); 28 | try { 29 | const meetingInfo: CreateMeetingCommandOutput = await chimeSdkMeetings.send( 30 | new CreateMeetingCommand(createMeetingCommandInput), 31 | ); 32 | console.info(`Meeting Info: ${JSON.stringify(meetingInfo)}`); 33 | return meetingInfo; 34 | } catch (err) { 35 | console.info(`Error: ${err}`); 36 | return false; 37 | } 38 | } 39 | ``` 40 | 41 | This function in the [meetingInfo](src/resources/meetingInfo/meetingInfo.ts) Lambda function is called when a new meeting is needed. This particular code will create an Amazon Chime SDK meeting in `us-east-1` with a random `ClientRequestToken` and `ExternalMeetingId`. 42 | 43 | The output of this command will look something like this: 44 | 45 | ```json 46 | { 47 | "Meeting": { 48 | "ExternalMeetingId": "12345", 49 | "MediaPlacement": { 50 | "AudioFallbackUrl": "wss://haxrp.m2.ue1.app.chime.aws:443/calls/4101582d-72d2-451b-8ac0-f0f127992713", 51 | "AudioHostUrl": "d1535c0577976b27a01ae5a034c9ca7c.k.m2.ue1.app.chime.aws:3478", 52 | "EventIngestionUrl": "https://data.svc.ue1.ingest.chime.aws/v1/client-events", 53 | "ScreenDataUrl": "wss://bitpw.m2.ue1.app.chime.aws:443/v2/screen/4101582d-72d2-451b-8ac0-f0f127992713", 54 | "ScreenSharingUrl": "wss://bitpw.m2.ue1.app.chime.aws:443/v2/screen/4101582d-72d2-451b-8ac0-f0f127992713", 55 | "ScreenViewingUrl": "wss://bitpw.m2.ue1.app.chime.aws:443/ws/connect?passcode=null&viewer_uuid=null&X-BitHub-Call-Id=4101582d-72d2-451b-8ac0-f0f127992713", 56 | "SignalingUrl": "wss://signal.m2.ue1.app.chime.aws/control/4101582d-72d2-451b-8ac0-f0f127992713", 57 | "TurnControlUrl": "https://2713.cell.us-east-1.meetings.chime.aws/v2/turn_sessions" 58 | }, 59 | "MediaRegion": "us-east-1", 60 | "MeetingArn": "arn:aws:chime:us-east-1:112233445566:meeting/4101582d-72d2-451b-8ac0-f0f127992713", 61 | "MeetingFeatures": null, 62 | "MeetingHostId": null, 63 | "MeetingId": "4101582d-72d2-451b-8ac0-f0f127992713", 64 | "PrimaryMeetingId": null, 65 | "TenantIds": [] 66 | } 67 | } 68 | ``` 69 | 70 | This information is needed by the client libraries to connect to the Amazon Chime SDK meeting and will be used there. For now, we will package this information and prepare to return it to the client making the request. 71 | 72 | ### Create Attendee 73 | 74 | Using this information, we will create an attendee for this meeting. 75 | 76 | ```typescript 77 | var createAttendeeCommandInput: CreateAttendeeCommandInput = { 78 | MeetingId: '', 79 | ExternalUserId: '', 80 | }; 81 | 82 | async function createAttendee(meetingId: string, attendeeEmail: string) { 83 | console.log(`Creating Attendee for Meeting: ${meetingId}`); 84 | createAttendeeCommandInput.MeetingId = meetingId; 85 | createAttendeeCommandInput.ExternalUserId = attendeeEmail; 86 | const attendeeInfo: CreateAttendeeCommandOutput = await chimeSdkMeetings.send( 87 | new CreateAttendeeCommand(createAttendeeCommandInput), 88 | ); 89 | return attendeeInfo; 90 | } 91 | ``` 92 | 93 | This function will create an attendee using the previously created Amazon Chime SDK meetingId and an ExternalUserId based on the email address that was captured from [Amazon Cognito](https://aws.amazon.com/cognito/) and passed from the UI to the Lambda function through the API Gateway. 94 | 95 | The output of this command will look something like this: 96 | 97 | ```json 98 | { 99 | "Attendee": { 100 | "AttendeeId": "690bd42d-1632-8c92-bb52-54557eabafa0", 101 | "Capabilities": { 102 | "Audio": "SendReceive", 103 | "Content": "SendReceive", 104 | "Video": "SendReceive" 105 | }, 106 | "ExternalUserId": "user@example.com", 107 | "JoinToken": "NjkwYmQ0MmQtMTYzMi04YzkyLWJiNTItNTQ1NTdlYWJhZmEwOjdhYmRkYjljLTkyODUtNDA4NC1hNTJiLWVlMjJhODNhYTU5Mg" 108 | } 109 | } 110 | ``` 111 | 112 | ### Start Transcription 113 | 114 | Once the Amazon Chime SDK meeting has been created, we will use the meetingId to start transcription. 115 | 116 | ```typescript 117 | async function startTranscribe(meetingId: string) { 118 | try { 119 | const transcribeResponse = await chimeSdkMeetings.send( 120 | new StartMeetingTranscriptionCommand({ 121 | MeetingId: meetingId, 122 | TranscriptionConfiguration: { 123 | EngineTranscribeSettings: { 124 | LanguageCode: 'en-US', 125 | }, 126 | }, 127 | }), 128 | ); 129 | console.log(JSON.stringify(transcribeResponse)); 130 | return true; 131 | } catch (error) { 132 | return false; 133 | } 134 | } 135 | ``` 136 | 137 | This will cause attendees in the meeting to have their audio transcribed and returned to all participants in the Amazon Chime SDK meeting through the data plane. These transcriptions will also be captured in the media capture pipeline. 138 | 139 | ![TranscribeExample](images/TranscribeExample.png) 140 | 141 | ### Start Media Capture Pipeline 142 | 143 | To start the media capture pipeline, we will use the meetingId returned from `CreateMeetingCommand`. 144 | 145 | ```typescript 146 | async function startCapture(meetingId: string) { 147 | const createPipelineParams: CreateMediaCapturePipelineCommandInput = { 148 | ChimeSdkMeetingConfiguration: { 149 | ArtifactsConfiguration: { 150 | Audio: { MuxType: 'AudioOnly' }, 151 | CompositedVideo: { 152 | GridViewConfiguration: { 153 | ContentShareLayout: 'PresenterOnly', 154 | }, 155 | Layout: 'GridView', 156 | Resolution: 'FHD', 157 | }, 158 | Content: { State: 'Disabled' }, 159 | Video: { State: 'Disabled' }, 160 | }, 161 | }, 162 | SinkArn: captureBucketArn, 163 | SinkType: 'S3Bucket', 164 | SourceArn: `arn:aws:chime::${awsAccountId}:meeting:${meetingId}`, 165 | SourceType: 'ChimeSdkMeeting', 166 | Tags: [{ Key: 'transcription-for-comprehend', Value: 'true' }], 167 | }; 168 | console.log(JSON.stringify(createPipelineParams)); 169 | try { 170 | const createMediaCapturePipelineResponse: CreateMediaCapturePipelineCommandOutput = 171 | await chimeSdkMediaPipelinesClient.send( 172 | new CreateMediaCapturePipelineCommand(createPipelineParams), 173 | ); 174 | return createMediaCapturePipelineResponse.MediaCapturePipeline 175 | ?.MediaPipelineArn; 176 | } catch (error) { 177 | console.log(error); 178 | return false; 179 | } 180 | } 181 | ``` 182 | 183 | In addition to the `SinkArn` that will direct the capture to our capture bucket, we also configure the [artifacts to be captured](https://docs.aws.amazon.com/chime/latest/APIReference/API_ArtifactsConfiguration.html). The `Tag` included will direct the media capture pipeline to capture a slimmed down version of the transcription that will not require us to parse the transcription output to create a human readable file. 184 | 185 | ### Create Meeting Summary 186 | 187 | ![BackendOverview](images/BackendOverview.png) 188 | 189 | ```typescript 190 | if (attendeeInfo && attendeeInfo.Attendee) { 191 | joinInfo = { 192 | Meeting: meetingInfo.Meeting, 193 | Attendee: [attendeeInfo.Attendee], 194 | }; 195 | const responseInfo = { 196 | Meeting: meetingInfo.Meeting, 197 | Attendee: attendeeInfo.Attendee, 198 | }; 199 | 200 | await startTranscribe(meetingInfo.Meeting.MeetingId); 201 | const mediaCapturePipelineArn = await startCapture( 202 | meetingInfo.Meeting.MeetingId, 203 | ); 204 | if (mediaCapturePipelineArn) { 205 | await putMeetingInfo(joinInfo); 206 | response.statusCode = 200; 207 | response.body = JSON.stringify(responseInfo); 208 | console.info('joinInfo: ' + JSON.stringify(response)); 209 | return response; 210 | } 211 | } 212 | ``` 213 | 214 | To summarize what we have done so far: 215 | 216 | 1. Create Meeting 217 | 2. Create Attendee 218 | 3. Start Transcription (with meetingId from Create Meeting) 219 | 4. Start Media Capture Pipeline (with meetingId from Create Meeting) 220 | 221 | Using the Meeting information and Attendee information, we will package them into a JSON object and return them to the requesting client. 222 | 223 | ## Frontend 224 | 225 | In this demo, the frontend client is a simple React App that will make the API request and render the results. 226 | 227 | ### Join Button 228 | 229 | ```javascript 230 | const JoinButtonProps = { 231 | icon: , 232 | onClick: (event) => handleJoin(event), 233 | label: 'Join', 234 | }; 235 | 236 | const handleJoin = async (event) => { 237 | event.preventDefault(); 238 | const email = (await Auth.currentUserInfo()).attributes.email; 239 | console.log(email); 240 | try { 241 | const joinResponse = await API.post('meetingApi', 'meeting', { 242 | body: { email: email }, 243 | }); 244 | const meetingSessionConfiguration = new MeetingSessionConfiguration( 245 | joinResponse.Meeting, 246 | joinResponse.Attendee, 247 | ); 248 | 249 | const options = { 250 | deviceLabels: DeviceLabels.AudioAndVideo, 251 | }; 252 | 253 | await meetingManager.join(meetingSessionConfiguration, options); 254 | await meetingManager.start(); 255 | meetingManager.invokeDeviceProvider(DeviceLabels.AudioAndVideo); 256 | setMeetingId(joinResponse.Meeting.MeetingId); 257 | setAttendeeId(joinResponse.Attendee.AttendeeId); 258 | } catch (err) { 259 | console.log(err); 260 | } 261 | }; 262 | ``` 263 | 264 | When the Join button is clicked in [Meeting.js](site/src/Meeting.js), the `handleJoin` function will be called and the email address of the Cognito user is passed to the AWS Lambda function. This information will be used to create the meeting and attendee and will return the `joinInfo` response. This response is used with [`meetingManager`](https://aws.github.io/amazon-chime-sdk-component-library-react/?path=/story/sdk-providers-meetingmanager--page) to join and start the meeting session. Once joined and started, this client will be able to view and send audio and video using the configured `DeviceLabels`. 265 | 266 | ### Video Tiles 267 | 268 | To render the meeting session video tiles, we will use [`VideoTileGrid`](https://aws.github.io/amazon-chime-sdk-component-library-react/?path=/docs/sdk-components-videotilegrid--page). This will create a responsive grid layout of the two attendees. 269 | 270 | ### Receive Transcriptions 271 | 272 | In order to receive transcriptions, we will use [`subscribeToTranscriptEvent`](https://aws.github.io/amazon-chime-sdk-js/interfaces/transcriptioncontroller.html#subscribetotranscriptevent) on the [`useAudioVideo`](https://aws.github.io/amazon-chime-sdk-component-library-react/?path=/story/sdk-hooks-useaudiovideo--page) hook. 273 | 274 | ```javascript 275 | useEffect(() => { 276 | if (audioVideo) { 277 | audioVideo.transcriptionController?.subscribeToTranscriptEvent( 278 | (transcriptEvent) => { 279 | setTranscripts(transcriptEvent); 280 | }, 281 | ); 282 | } 283 | }, [audioVideo]); 284 | ``` 285 | 286 | This [Effect Hook](https://reactjs.org/docs/hooks-effect.html) will look for changes on `audioVideo` and store `transcriptEvent` to the `transcripts` array that uses the [State Hook](https://reactjs.org/docs/hooks-state.html) to store the incoming transcriptEvents. 287 | 288 | ```javascript 289 | useEffect(() => { 290 | if (transcripts) { 291 | if (transcripts.results !== undefined) { 292 | if (!transcripts.results[0].isPartial) { 293 | if (transcripts.results[0].alternatives[0].items[0].confidence > 0.5) { 294 | setLine((lines) => [ 295 | ...lines, 296 | `${transcripts.results[0].alternatives[0].items[0].attendee.externalUserId}: ${transcripts.results[0].alternatives[0].transcript}`, 297 | ]); 298 | } 299 | } 300 | } 301 | } 302 | }, [transcripts]); 303 | ``` 304 | 305 | When the `transcripts` array is updated with `setTranscripts`, React will re-render this Hook. This will parse the incoming `transcripts` looking for results that: 306 | 307 | - are not [partial](https://docs.aws.amazon.com/transcribe/latest/dg/streaming.html#streaming-partial-results) 308 | - have a confidence of > 0.5 309 | 310 | When found, this text will be added to the `lines` array for rendering using `setLine`. 311 | 312 | ### Rendering Transcription 313 | 314 | To display the last ten lines of the ongoing transcription, we will use a `slice` and `map` to display each line sequentially. 315 | 316 | ```javascript 317 | Transcription}> 318 | 319 |
320 | {lines.slice(Math.max(lines.length - 10, 0)).map((line, index) => ( 321 |
322 | {line} 323 |
324 |
325 | ))} 326 |
327 |
328 |
329 | ``` 330 | 331 | ### Recordings 332 | 333 | When the Amazon Chime SDK meeting is created in the `meetingInfo` Lambda function, a [media capture pipeline](https://docs.aws.amazon.com/chime-sdk/latest/dg/media-pipelines.html) is also created. This media capture pipeline will capture the audio, video, transcriptions, and meeting events to the designated Amazon Simple Storage Service (Amazon S3) bucket. Additionally, a media concatenation pipeline is created to process the output of the media capture pipeline in a more usable format. In this demo, we will take the artifacts created in the media capture S3 bucket and use a media concatenation pipeline to concatenate each of those artifacts into a single artifact for each type. 334 | 335 | To create the media concatenation pipeline, we will use Amazon EventBridge to trigger a Lambda function when the media capture pipeline starts. 336 | 337 | In this demo, an EventBridge rule is created to send all `aws.chime` events to the [`eventBridge.ts`](src/resources/eventBridge/eventBridge.ts) Lambda function. 338 | 339 | ```typescript 340 | const chimeSdkRule = new Rule(this, 'chimeSdkRule', { 341 | eventPattern: { 342 | source: ['aws.chime'], 343 | }, 344 | }); 345 | chimeSdkRule.addTarget(new LambdaFunction(eventBridgeLambda)); 346 | ``` 347 | 348 | When this Lambda function is triggered, if the `detail-type` is `Chime Media Pipeline State Change` and the `eventType` is `chime:MediaPipelineInProgress`, the `startConcat` function will be called. 349 | 350 | ```typescript 351 | switch (event['detail-type']) { 352 | case 'Chime Media Pipeline State Change': 353 | if (event.detail.eventType == 'chime:MediaPipelineInProgress') { 354 | const mediaCapturePipeline = await getMediaPipeline( 355 | event.detail.mediaPipelineId, 356 | ); 357 | if ( 358 | mediaCapturePipeline && 359 | mediaCapturePipeline.MediaPipeline && 360 | mediaCapturePipeline.MediaPipeline.MediaCapturePipeline && 361 | mediaCapturePipeline.MediaPipeline.MediaCapturePipeline.MediaPipelineArn 362 | ) { 363 | await startConcat( 364 | mediaCapturePipeline.MediaPipeline.MediaCapturePipeline 365 | .MediaPipelineArn, 366 | ); 367 | } 368 | } 369 | } 370 | ``` 371 | 372 | The `startConcat` function will use the `CreateMediaConcatenationPipelineCommand` API to start the media concatenation pipeline process. The media concatenation pipeline will only begin concatenation when the media capture pipeline ends. In this demo, that will occur when the meeting ends. 373 | 374 | ```typescript 375 | async function startConcat(mediaCapturePipelineArn: string) { 376 | const createConcatPipelineParams: CreateMediaConcatenationPipelineCommandInput = 377 | { 378 | Sinks: [ 379 | { 380 | S3BucketSinkConfiguration: { Destination: concatBucketArn }, 381 | Type: 'S3Bucket', 382 | }, 383 | ], 384 | Sources: [ 385 | { 386 | MediaCapturePipelineSourceConfiguration: { 387 | ChimeSdkMeetingConfiguration: { 388 | ArtifactsConfiguration: { 389 | Audio: { State: 'Enabled' }, 390 | CompositedVideo: { State: 'Enabled' }, 391 | Content: { State: 'Disabled' }, 392 | DataChannel: { State: 'Enabled' }, 393 | MeetingEvents: { State: 'Enabled' }, 394 | TranscriptionMessages: { State: 'Enabled' }, 395 | Video: { State: 'Disabled' }, 396 | }, 397 | }, 398 | MediaPipelineArn: mediaCapturePipelineArn, 399 | }, 400 | Type: 'MediaCapturePipeline', 401 | }, 402 | ], 403 | }; 404 | console.log(JSON.stringify(createConcatPipelineParams)); 405 | try { 406 | await chimeSdkMediaPipelinesClient.send( 407 | new CreateMediaConcatenationPipelineCommand(createConcatPipelineParams), 408 | ); 409 | return true; 410 | } catch (error) { 411 | console.log(error); 412 | return false; 413 | } 414 | } 415 | ``` 416 | 417 | Once the media concatenation pipeline completes and the artifacts are stored in the designated `concatBucket`, a Lambda function will be triggered on the S3 event. This Lambda function will load into an Amazon DynamoDB table the associated keys for these artifacts. This information will be used by the [`Recordings.ts`](site/src/Recordings.js) page in the React app to view the recordings and transcriptions. 418 | 419 | ### Recording Summary 420 | 421 | 1. Meeting created and attendees join 422 | 2. Transcription starts 423 | 3. Meeting capture starts - 5 second chunks are stored in the capture bucket 424 | 4. Meeting ends 425 | 5. Meeting concatenation begins - 5 second chunks are concatenated into a single file 426 | 6. Meeting concatenation completes and file is stored in concat bucket 427 | 7. Concat bucket `OBJECT_CREATED` [triggers](https://docs.aws.amazon.com/lambda/latest/dg/with-s3-example.html) [`postMeeting`](src/resources/postMeeting/postMeeting.ts) Lambda function 428 | 8. postMeeting Lambda function adds S3 key information to the Output Table 429 | 430 | ![PostMeeting](images/PostMeeting.png) 431 | 432 | ## Service Linked Roles 433 | 434 | As part of this CDK, two service-linked roles are required. This [service-linked role](https://docs.aws.amazon.com/chime-sdk/latest/ag/using-service-linked-roles-transcription.html) will allow the Amazon Chime SDK complete the necessary actions to start live transcription. This [service-linked role](https://docs.aws.amazon.com/chime-sdk/latest/dg/create-pipeline-role.html) will allow the Amazon Chime SDK to complete the necessary actions to capture media from the Amazon Chime SDK meeting. 435 | 436 | If these do not exist in your account, please add them: 437 | 438 | ```bash 439 | aws iam create-service-linked-role --aws-service-name transcription.chime.amazonaws.com 440 | 441 | aws iam create-service-linked-role --aws-service-name mediapipelines.chime.amazonaws.com 442 | ``` 443 | 444 | ## Deploying and Using Demo 445 | 446 | To deploy this demo, simply run: 447 | 448 | ```bash 449 | yarn launch 450 | ``` 451 | 452 | This will deploy an AWS Cloudformation Stack that includes the backend components and a hosted site that uses an S3 bucket and Amazon CloudFront distribution. This demo can also be used on the hosted site from the Cloudfront distribution or locally. To use locally: 453 | 454 | ```bash 455 | cd site 456 | yarn 457 | yarn run start 458 | ``` 459 | 460 | Then open `localhost:3000` in a browser. 461 | 462 | In both cases, a user must be created in Cognito and used to log in. The email from this login will be used as the `ExternalUserId` when creating the attendee. 463 | 464 | ![MeetingDemo](images/MeetingDemo.png) 465 | 466 | Select `Join` to join in to a new meeting. When done, select `End` to stop the meeting and start the media concatenation pipeline. 467 | 468 | ![RecordingDemo](images/RecordingDemo.png) 469 | 470 | To view recordings, select `Recordings` from the left side navigation menu. 471 | 472 | ## Cleanup 473 | 474 | To remove resources created with this demo: 475 | 476 | ``` 477 | yarn cdk destroy 478 | ``` 479 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node -P tsconfig.json --prefer-ts-exts src/amazon-chime-sdk-meeting-with-transcribe.ts", 3 | "output": "cdk.out", 4 | "build": "npx projen bundle", 5 | "watch": { 6 | "include": [ 7 | "src/**/*.ts", 8 | "test/**/*.ts" 9 | ], 10 | "exclude": [ 11 | "README.md", 12 | "cdk*.json", 13 | "**/*.d.ts", 14 | "**/*.js", 15 | "tsconfig.json", 16 | "package*.json", 17 | "yarn.lock", 18 | "node_modules" 19 | ] 20 | }, 21 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 22 | } 23 | -------------------------------------------------------------------------------- /images/AmazonChimeSDKMeetingWithTranscribeOverview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-media-capture-pipeline-demo/964452c81b3055060622df75d9f85087a37598c5/images/AmazonChimeSDKMeetingWithTranscribeOverview.png -------------------------------------------------------------------------------- /images/BackendOverview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-media-capture-pipeline-demo/964452c81b3055060622df75d9f85087a37598c5/images/BackendOverview.png -------------------------------------------------------------------------------- /images/MeetingDemo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-media-capture-pipeline-demo/964452c81b3055060622df75d9f85087a37598c5/images/MeetingDemo.png -------------------------------------------------------------------------------- /images/PostMeeting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-media-capture-pipeline-demo/964452c81b3055060622df75d9f85087a37598c5/images/PostMeeting.png -------------------------------------------------------------------------------- /images/RecordingDemo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-media-capture-pipeline-demo/964452c81b3055060622df75d9f85087a37598c5/images/RecordingDemo.png -------------------------------------------------------------------------------- /images/TranscribeExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-media-capture-pipeline-demo/964452c81b3055060622df75d9f85087a37598c5/images/TranscribeExample.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amazon-chime-sdk-meeting-with-with-transcribe", 3 | "scripts": { 4 | "build": "npx projen build", 5 | "bundle": "npx projen bundle", 6 | "clobber": "npx projen clobber", 7 | "compile": "npx projen compile", 8 | "configLocal": "npx projen configLocal", 9 | "default": "npx projen default", 10 | "deploy": "npx projen deploy", 11 | "destroy": "npx projen destroy", 12 | "diff": "npx projen diff", 13 | "eject": "npx projen eject", 14 | "eslint": "npx projen eslint", 15 | "getBucket": "npx projen getBucket", 16 | "launch": "npx projen launch", 17 | "package": "npx projen package", 18 | "post-compile": "npx projen post-compile", 19 | "post-upgrade": "npx projen post-upgrade", 20 | "pre-compile": "npx projen pre-compile", 21 | "synth": "npx projen synth", 22 | "synth:silent": "npx projen synth:silent", 23 | "test": "npx projen test", 24 | "upgrade": "npx projen upgrade", 25 | "watch": "npx projen watch", 26 | "projen": "npx projen" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "^18", 30 | "@typescript-eslint/eslint-plugin": "^7", 31 | "@typescript-eslint/parser": "^7", 32 | "aws-cdk": "^2.101.0", 33 | "esbuild": "^0.23.0", 34 | "eslint": "^8", 35 | "eslint-import-resolver-typescript": "^3.6.1", 36 | "eslint-plugin-import": "^2.29.1", 37 | "projen": "^0.84.6", 38 | "ts-node": "^10.9.2", 39 | "typescript": "^4.9.5" 40 | }, 41 | "dependencies": { 42 | "@aws-sdk/client-chime-sdk-media-pipelines": "^3.616.0", 43 | "@aws-sdk/client-chime-sdk-meetings": "^3.616.0", 44 | "@aws-sdk/client-dynamodb": "^3.616.0", 45 | "@aws-sdk/lib-dynamodb": "^3.616.0", 46 | "@types/aws-lambda": "^8.10.141", 47 | "@types/fs-extra": "^9.0.13", 48 | "aws-cdk-lib": "^2.101.0", 49 | "aws-lambda": "^1.0.7", 50 | "constructs": "^10.0.5", 51 | "fs-extra": "^10.1.0" 52 | }, 53 | "license": "Apache-2.0", 54 | "publishConfig": { 55 | "access": "public" 56 | }, 57 | "version": "0.0.0", 58 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 59 | } 60 | -------------------------------------------------------------------------------- /site/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-react", "@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /site/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 3 | extends: [ 4 | 'plugin:react/recommended', // Uses the recommended rules from @eslint-plugin-react 5 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 6 | ], 7 | parserOptions: { 8 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 9 | sourceType: 'module', // Allows for the use of imports 10 | ecmaFeatures: { 11 | jsx: true, // Allows for the parsing of JSX 12 | }, 13 | }, 14 | rules: { 15 | '@typescript-eslint/ban-ts-ignore': 0, 16 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 17 | // e.g. "@typescript-eslint/explicit-function-return-type": "off", 18 | '@typescript-eslint/no-empty-function': 0, 19 | // '@typescript-eslint/no-empty-function': [ 20 | // 'error', 21 | // { 22 | // allow: [ 23 | // 'methods', 24 | // 'functions', 25 | // 'arrowFunctions', 26 | // 'generatorFunctions', 27 | // 'asyncMethods', 28 | // 'generatorMethods', 29 | // 'asyncFunctions', 30 | // 'getters', 31 | // 'setters', 32 | // ], 33 | // }, 34 | // ], 35 | }, 36 | settings: { 37 | react: { 38 | version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use 39 | }, 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /site/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /dist 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | !*.js 26 | 27 | config.json -------------------------------------------------------------------------------- /site/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 4, 7 | }; 8 | -------------------------------------------------------------------------------- /site/README.md: -------------------------------------------------------------------------------- 1 | ## Amazon Chime SDK With Amazon Lex Bot for Existing Contact Centers SIP Client 2 | 3 | ## To Use 4 | 5 | ``` 6 | yarn 7 | yarn run start 8 | ``` 9 | -------------------------------------------------------------------------------- /site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "site", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "webpack serve --config ./webpack.config.js --mode development", 8 | "build": "webpack --config webpack.config.js --mode production" 9 | }, 10 | "dependencies": { 11 | "@aws-amplify/ui-react": "^5.3.0", 12 | "@aws-crypto/sha256-browser": "^2.0.1", 13 | "@aws-sdk/protocol-http": "^3.374.0", 14 | "@aws-sdk/s3-request-presigner": "^3.405.0", 15 | "@aws-sdk/url-parser": "^3.374.0", 16 | "@aws-sdk/util-format-url": "^3.398.0", 17 | "@cloudscape-design/components": "^3.0.44", 18 | "@cloudscape-design/global-styles": "^1.0.1", 19 | "amazon-chime-sdk-component-library-react": "^3.2.0", 20 | "amazon-chime-sdk-js": "^3.11.0", 21 | "aws-amplify": "^5.3.10", 22 | "babel-loader": "^8.2.4", 23 | "react": "^17.0.2", 24 | "react-dom": "^17.0.2", 25 | "react-player": "^2.10.1", 26 | "react-router-dom": "^6.3.0", 27 | "styled-components": "^5.3.5", 28 | "styled-system": "^5.1.5" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.17.5", 32 | "@babel/preset-env": "^7.16.11", 33 | "@babel/preset-react": "^7.16.7", 34 | "copy-webpack-plugin": "^10.2.4", 35 | "css-loader": "^6.6.0", 36 | "file-loader": "^6.2.0", 37 | "html-webpack-plugin": "^5.5.0", 38 | "style-loader": "^3.3.1", 39 | "webpack": "^5.94.0", 40 | "webpack-cli": "^4.9.2", 41 | "webpack-dev-server": "^4.7.4" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /site/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Amazon Chime SDK Meetings with Live Connector 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /site/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /site/src/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-media-capture-pipeline-demo/964452c81b3055060622df75d9f85087a37598c5/site/src/App.css -------------------------------------------------------------------------------- /site/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { BrowserRouter, Route, Routes } from 'react-router-dom'; 3 | import './App.css'; 4 | import { AmplifyConfig as config } from './Config'; 5 | import { Amplify, Auth } from 'aws-amplify'; 6 | import { withAuthenticator } from '@aws-amplify/ui-react'; 7 | import { Meetings } from './Meeting'; 8 | import { Recordings } from './Recordings'; 9 | import { Navigation } from './Navigation'; 10 | import { ThemeProvider } from 'styled-components'; 11 | import { MeetingProvider, lightTheme } from 'amazon-chime-sdk-component-library-react'; 12 | import '@aws-amplify/ui-react/styles.css'; 13 | import '@cloudscape-design/global-styles/index.css'; 14 | import AppLayout from '@cloudscape-design/components/app-layout'; 15 | 16 | Amplify.configure(config); 17 | Amplify.Logger.LOG_LEVEL = 'DEBUG'; 18 | console.log(config.API); 19 | 20 | const App = () => { 21 | const [currentCredentials, setCurrentCredentials] = useState({}); 22 | const [currentSession, setCurrentSession] = useState({}); 23 | 24 | useEffect(() => { 25 | async function getAuth() { 26 | setCurrentSession(await Auth.currentSession()); 27 | setCurrentCredentials(await Auth.currentUserCredentials()); 28 | console.log(`authState: ${currentSession}`); 29 | console.log(`currentCredentials: ${currentCredentials}`); 30 | } 31 | getAuth(); 32 | }, []); 33 | 34 | return ( 35 | } 39 | content={ 40 | 41 | 42 | } 46 | > 47 | 48 | 53 | 54 | 55 | 56 | 57 | } 58 | /> 59 | 60 | 61 | } 62 | > 63 | ); 64 | }; 65 | 66 | export default withAuthenticator(App); 67 | -------------------------------------------------------------------------------- /site/src/Config.js: -------------------------------------------------------------------------------- 1 | const config = await fetch('./config.json').then((response) => response.json()); 2 | 3 | export const ConcatBucket = config.concatBucket; 4 | export const Region = config.userPoolRegion; 5 | 6 | export const AmplifyConfig = { 7 | Auth: { 8 | region: config.userPoolRegion, 9 | identityPoolId: config.identityPoolId, 10 | userPoolId: config.userPoolId, 11 | userPoolWebClientId: config.userPoolClientId, 12 | mandatorySignIn: true, 13 | cookieStorage: { 14 | domain: `${window.location.hostname}`, 15 | path: '/', 16 | expires: 365, 17 | secure: true, 18 | }, 19 | }, 20 | Analytics: { 21 | disabled: true, 22 | }, 23 | API: { 24 | endpoints: [ 25 | { 26 | name: 'meetingApi', 27 | endpoint: config.apiUrl, 28 | }, 29 | ], 30 | }, 31 | Storage: { 32 | AWSS3: { 33 | bucket: config.concatBucket, 34 | region: config.userPoolRegion, 35 | }, 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /site/src/Meeting.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { 3 | useMeetingManager, 4 | useLocalVideo, 5 | useAudioVideo, 6 | ControlBar, 7 | ControlBarButton, 8 | Meeting, 9 | LeaveMeeting, 10 | AudioInputControl, 11 | DeviceLabels, 12 | VideoTileGrid, 13 | Remove, 14 | VideoInputControl, 15 | AudioOutputControl, 16 | MeetingStatus, 17 | useMeetingStatus, 18 | } from 'amazon-chime-sdk-component-library-react'; 19 | import Container from '@cloudscape-design/components/container'; 20 | import Header from '@cloudscape-design/components/header'; 21 | import SpaceBetween from '@cloudscape-design/components/space-between'; 22 | import { MeetingSessionConfiguration } from 'amazon-chime-sdk-js'; 23 | import './App.css'; 24 | import { AmplifyConfig as config } from './Config'; 25 | import { Amplify, API, Auth } from 'aws-amplify'; 26 | import '@aws-amplify/ui-react/styles.css'; 27 | Amplify.configure(config); 28 | Amplify.Logger.LOG_LEVEL = 'DEBUG'; 29 | console.log(config.API); 30 | export const Meetings = () => { 31 | const meetingManager = useMeetingManager(); 32 | const meetingStatus = useMeetingStatus(); 33 | const [meetingId, setMeetingId] = useState(''); 34 | const [attendeeId, setAttendeeId] = useState(''); 35 | const [transcripts, setTranscripts] = useState([]); 36 | const [lines, setLine] = useState([]); 37 | const [currentLine, setCurrentLine] = useState(''); 38 | const audioVideo = useAudioVideo(); 39 | 40 | const { toggleVideo } = useLocalVideo(); 41 | 42 | useEffect(() => { 43 | async function tog() { 44 | if (meetingStatus === MeetingStatus.Succeeded) { 45 | await toggleVideo(); 46 | } 47 | } 48 | tog(); 49 | }, [meetingStatus]); 50 | 51 | useEffect(() => { 52 | if (transcripts) { 53 | if (transcripts.results !== undefined) { 54 | console.log(`Transcript Results: ${JSON.stringify(transcripts.results)}`); 55 | if (!transcripts.results[0].isPartial) { 56 | setLine((lines) => [ 57 | ...lines, 58 | `${transcripts.results[0].alternatives[0].items[0].attendee.externalUserId}: ${transcripts.results[0].alternatives[0].transcript}`, 59 | ]); 60 | setCurrentLine(''); 61 | } else { 62 | setCurrentLine( 63 | `${transcripts.results[0].alternatives[0].items[0].attendee.externalUserId}: ${transcripts.results[0].alternatives[0].transcript}`, 64 | ); 65 | } 66 | } 67 | } 68 | }, [transcripts]); 69 | 70 | useEffect(() => { 71 | if (audioVideo) { 72 | audioVideo.transcriptionController?.subscribeToTranscriptEvent((transcriptEvent) => { 73 | setTranscripts(transcriptEvent); 74 | }); 75 | } 76 | }, [audioVideo]); 77 | 78 | const JoinButtonProps = { 79 | icon: , 80 | onClick: (event) => handleJoin(event), 81 | label: 'Join', 82 | }; 83 | 84 | const LeaveButtonProps = { 85 | icon: , 86 | onClick: (event) => handleLeave(event), 87 | label: 'Leave', 88 | }; 89 | 90 | const EndButtonProps = { 91 | icon: , 92 | onClick: (event) => handleEnd(event), 93 | label: 'End', 94 | }; 95 | 96 | const handleLeave = async (event) => { 97 | await meetingManager.leave(); 98 | }; 99 | 100 | const handleEnd = async (event) => { 101 | console.log(`Auth ${JSON.stringify(await Auth.currentUserInfo())}`); 102 | event.preventDefault(); 103 | try { 104 | await API.post('meetingApi', 'end', { body: { meetingId: meetingId } }); 105 | setLine([]); 106 | } catch (err) { 107 | console.log(err); 108 | } 109 | }; 110 | 111 | const handleJoin = async (event) => { 112 | event.preventDefault(); 113 | const email = (await Auth.currentUserInfo()).attributes.email; 114 | console.log(email); 115 | try { 116 | const joinResponse = await API.post('meetingApi', 'meeting', { body: { email: email } }); 117 | const meetingSessionConfiguration = new MeetingSessionConfiguration( 118 | joinResponse.Meeting, 119 | joinResponse.Attendee, 120 | ); 121 | 122 | const options = { 123 | deviceLabels: DeviceLabels.AudioAndVideo, 124 | }; 125 | 126 | await meetingManager.join(meetingSessionConfiguration, options); 127 | await meetingManager.start(); 128 | meetingManager.invokeDeviceProvider(DeviceLabels.AudioAndVideo); 129 | setMeetingId(joinResponse.Meeting.MeetingId); 130 | setAttendeeId(joinResponse.Attendee.AttendeeId); 131 | } catch (err) { 132 | console.log(err); 133 | } 134 | }; 135 | 136 | return ( 137 | 138 | 139 | Amazon Chime SDK Meeting}> 140 |
141 | 142 |
143 |
144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 |
153 | 154 | Transcription}> 155 | 156 |
157 | {lines.slice(Math.max(lines.length - 10, 0)).map((line, index) => ( 158 |
159 | {line} 160 |
161 |
162 | ))} 163 | {currentLine.length > 0 && currentLine} 164 |
165 |
166 |
167 |
168 | ); 169 | }; 170 | -------------------------------------------------------------------------------- /site/src/Navigation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SideNavigation from '@cloudscape-design/components/side-navigation'; 3 | export const Navigation = () => { 4 | const navigationItems = [ 5 | { type: 'link', text: 'Meetings', href: '/' }, 6 | { type: 'link', text: 'Recordings', href: '/recordings' }, 7 | ]; 8 | return ( 9 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /site/src/Recordings.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import ReactPlayer from 'react-player'; 3 | import { HttpRequest } from '@aws-sdk/protocol-http'; 4 | import { S3RequestPresigner } from '@aws-sdk/s3-request-presigner'; 5 | import Container from '@cloudscape-design/components/container'; 6 | import Header from '@cloudscape-design/components/header'; 7 | import SpaceBetween from '@cloudscape-design/components/space-between'; 8 | import Button from '@cloudscape-design/components/button'; 9 | import { parseUrl } from '@aws-sdk/url-parser'; 10 | import { Sha256 } from '@aws-crypto/sha256-browser'; 11 | import { formatUrl } from '@aws-sdk/util-format-url'; 12 | import { ConcatBucket, Region } from './Config'; 13 | import './App.css'; 14 | import { AmplifyConfig as config } from './Config'; 15 | import { Amplify, API, Auth } from 'aws-amplify'; 16 | import '@aws-amplify/ui-react/styles.css'; 17 | 18 | Amplify.configure(config); 19 | Amplify.Logger.LOG_LEVEL = 'DEBUG'; 20 | console.log(config.API); 21 | export const Recordings = ({ currentCredentials }) => { 22 | const [recordings, setRecordings] = useState([]); 23 | const [mediaPipelineIds, setMediaPipelineIds] = useState([]); 24 | 25 | useEffect(() => { 26 | console.log(recordings); 27 | async function generateS3PresignedUrl(key) { 28 | console.log(`currentCredentials: ${JSON.stringify(currentCredentials)}`); 29 | const s3ObjectUrl = parseUrl(`https://${ConcatBucket}.s3.${Region}.amazonaws.com/${key}`); 30 | const presigner = new S3RequestPresigner({ 31 | credentials: Auth.currentUserCredentials(), 32 | region: 'us-east-1', 33 | sha256: Sha256, 34 | }); 35 | 36 | const presignedResponse = await presigner.presign(new HttpRequest(s3ObjectUrl)); 37 | const presignedUrl = formatUrl(presignedResponse); 38 | console.log(`presignedUrl: ${presignedUrl}`); 39 | return presignedUrl; 40 | } 41 | 42 | async function getMediaPipelineIds() { 43 | for (let recording of recordings) { 44 | let mediaPipelineId = {}; 45 | mediaPipelineId.video = await generateS3PresignedUrl(recording['composited-video']); 46 | mediaPipelineId.timestamp = parseFloat(recording.timestamp) * 1000; 47 | mediaPipelineId.id = recording.mediaPipelineId; 48 | mediaPipelineId.transcript = await generateS3PresignedUrl(recording['transcription-messages']); 49 | console.log(`mediaPipelineId ${JSON.stringify(mediaPipelineId)}`); 50 | setMediaPipelineIds((mediaPipelineIds) => [...mediaPipelineIds, mediaPipelineId]); 51 | } 52 | } 53 | getMediaPipelineIds(); 54 | console.log(mediaPipelineIds); 55 | }, [recordings]); 56 | 57 | useEffect(() => { 58 | const getRecordings = async () => { 59 | try { 60 | const recordingsResponse = await API.post('meetingApi', 'recordings', {}); 61 | console.log(`Recording Response: ${JSON.stringify(recordingsResponse)}`); 62 | setRecordings(recordingsResponse); 63 | } catch (err) { 64 | console.log(err); 65 | } 66 | }; 67 | 68 | getRecordings(); 69 | }, []); 70 | 71 | return ( 72 |
73 | 74 | {mediaPipelineIds 75 | .slice(Math.max(mediaPipelineIds.length - 10, 0)) 76 | .sort((a, b) => b.timestamp - a.timestamp) 77 | .map((mediaPipelineId, index) => ( 78 |
79 | 90 | Transcription 91 | 92 | } 93 | > 94 | MediaPipeline ID: {mediaPipelineId.id} 95 | 96 | 97 | } 98 | > 99 | 100 |
101 |
102 |
103 | ))} 104 |
105 |
106 | ); 107 | }; 108 | -------------------------------------------------------------------------------- /site/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 4 | 'Droid Sans', 'Helvetica Neue', sans-serif; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | } 8 | 9 | code { 10 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; 11 | } 12 | -------------------------------------------------------------------------------- /site/src/index.js: -------------------------------------------------------------------------------- 1 | import './index.css'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import App from './App'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root'), 11 | ); 12 | -------------------------------------------------------------------------------- /site/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: ['regenerator-runtime/runtime.js', './src/index.js'], 6 | output: { 7 | path: path.resolve(__dirname, 'dist'), 8 | filename: 'index_bundle.js', 9 | publicPath: '/', 10 | }, 11 | performance: { 12 | hints: false, 13 | maxEntrypointSize: 512000, 14 | maxAssetSize: 512000, 15 | }, 16 | devServer: { 17 | historyApiFallback: true, 18 | static: { 19 | directory: path.join(__dirname, 'public'), 20 | }, 21 | compress: true, 22 | port: 3000, 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.(js)$/, 28 | use: 'babel-loader', 29 | }, 30 | { 31 | test: /\.css$/, 32 | use: ['style-loader', 'css-loader'], 33 | }, 34 | { 35 | test: /\.(png|svg|jpg|jpeg|gif)$/i, 36 | 37 | type: 'asset/resource', 38 | }, 39 | // { 40 | // test: /\.svg$/, 41 | // use: [ 42 | // { 43 | // loader: 'svg-url-loader', 44 | // options: { 45 | // limit: 10000, 46 | // }, 47 | // }, 48 | // ], 49 | // }, 50 | { 51 | test: /\.m?js/, 52 | resolve: { 53 | fullySpecified: false, 54 | }, 55 | }, 56 | { 57 | test: /\.mp3$/, 58 | loader: 'file-loader', 59 | }, 60 | ], 61 | }, 62 | mode: 'development', 63 | experiments: { 64 | topLevelAwait: true, 65 | }, 66 | plugins: [ 67 | new HtmlWebpackPlugin({ 68 | template: 'public/index.html', 69 | // favicon: './src/favicon.ico', 70 | }), 71 | ], 72 | }; 73 | -------------------------------------------------------------------------------- /src/amazon-chime-sdk-meeting-with-transcribe.ts: -------------------------------------------------------------------------------- 1 | import { App, Stack, StackProps, CfnOutput } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { Site, Infrastructure, MediaPipeline, Cognito } from './index'; 4 | 5 | export class AmazonChimeSDKWithTranscribe extends Stack { 6 | constructor(scope: Construct, id: string, props: StackProps = {}) { 7 | super(scope, id, props); 8 | 9 | const mediapipeline = new MediaPipeline(this, 'mediapipeline'); 10 | 11 | const allowedDomain = this.node.tryGetContext('AllowedDomain'); 12 | const cognito = new Cognito(this, 'Cognito', { 13 | allowedDomain: allowedDomain, 14 | concatBucket: mediapipeline.concatBucket, 15 | }); 16 | 17 | const infrastructure = new Infrastructure(this, 'infrastructure', { 18 | captureBucket: mediapipeline.captureBucket, 19 | concatBucket: mediapipeline.concatBucket, 20 | outputTable: mediapipeline.outputTable, 21 | }); 22 | 23 | const site = new Site(this, 'Site', { 24 | apiUrl: infrastructure.apiUrl, 25 | concatBucket: mediapipeline.concatBucket, 26 | userPool: cognito.userPool, 27 | userPoolClient: cognito.userPoolClient, 28 | identityPool: cognito.identityPool, 29 | }); 30 | 31 | new CfnOutput(this, 'distribution', { 32 | value: site.distribution.domainName, 33 | }); 34 | 35 | new CfnOutput(this, 'siteBucket', { value: site.siteBucket.bucketName }); 36 | } 37 | } 38 | const devEnv = { 39 | account: process.env.CDK_DEFAULT_ACCOUNT, 40 | region: process.env.CDK_DEFAULT_REGION, 41 | }; 42 | 43 | const app = new App(); 44 | 45 | new AmazonChimeSDKWithTranscribe(app, 'AmazonChimeSDKWithTranscribe', { 46 | env: devEnv, 47 | }); 48 | 49 | app.synth(); 50 | -------------------------------------------------------------------------------- /src/cognito.ts: -------------------------------------------------------------------------------- 1 | import { RemovalPolicy, Duration, Stack } from 'aws-cdk-lib'; 2 | 3 | import { 4 | AccountRecovery, 5 | UserPool, 6 | Mfa, 7 | IUserPool, 8 | UserPoolClient, 9 | CfnIdentityPool, 10 | CfnIdentityPoolRoleAttachment, 11 | IUserPoolClient, 12 | UserPoolClientIdentityProvider, 13 | } from 'aws-cdk-lib/aws-cognito'; 14 | import { 15 | PolicyStatement, 16 | IRole, 17 | Effect, 18 | Role, 19 | FederatedPrincipal, 20 | } from 'aws-cdk-lib/aws-iam'; 21 | import { Runtime, Architecture } from 'aws-cdk-lib/aws-lambda'; 22 | import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; 23 | import { Bucket } from 'aws-cdk-lib/aws-s3'; 24 | import { Construct } from 'constructs'; 25 | 26 | export interface CognitoStackProps { 27 | readonly allowedDomain: string; 28 | concatBucket: Bucket; 29 | } 30 | 31 | export class Cognito extends Construct { 32 | public readonly authenticatedRole: IRole; 33 | public readonly identityPool: CfnIdentityPool; 34 | public readonly userPool: IUserPool; 35 | public readonly userPoolClient: IUserPoolClient; 36 | public readonly userPoolRegion: string; 37 | 38 | constructor(scope: Construct, id: string, props: CognitoStackProps) { 39 | super(scope, id); 40 | 41 | const domainValidator = new NodejsFunction(this, 'domainValidator', { 42 | entry: 'src/resources/cognitoDomain/domainValidator.js', 43 | runtime: Runtime.NODEJS_16_X, 44 | architecture: Architecture.ARM_64, 45 | timeout: Duration.seconds(60), 46 | environment: { 47 | ALLOWED_DOMAIN: props.allowedDomain, 48 | }, 49 | }); 50 | 51 | const userPool = new UserPool(this, 'UserPool', { 52 | removalPolicy: RemovalPolicy.DESTROY, 53 | selfSignUpEnabled: true, 54 | lambdaTriggers: { 55 | preSignUp: domainValidator, 56 | }, 57 | signInAliases: { 58 | username: false, 59 | phone: false, 60 | email: true, 61 | }, 62 | accountRecovery: AccountRecovery.EMAIL_ONLY, 63 | standardAttributes: { 64 | email: { 65 | required: true, 66 | mutable: true, 67 | }, 68 | }, 69 | mfa: Mfa.OPTIONAL, 70 | mfaSecondFactor: { 71 | sms: true, 72 | otp: true, 73 | }, 74 | userInvitation: { 75 | emailSubject: 76 | 'Your Amazon Chime SDK Media Capture Pipeline Demo web app temporary password', 77 | emailBody: 78 | 'Your Amazon Chime SDK Media Capture Pipeline Demo web app username is {username} and temporary password is {####}', 79 | }, 80 | userVerification: { 81 | emailSubject: 82 | 'Verify your new Amazon Chime SDK Media Capture Pipeline Demo web app account', 83 | emailBody: 84 | 'The verification code to your new Amazon Chime SDK Media Capture Pipeline Demo web app account is {####}', 85 | }, 86 | }); 87 | 88 | const userPoolClient = new UserPoolClient(this, 'UserPoolClient', { 89 | userPool: userPool, 90 | generateSecret: false, 91 | supportedIdentityProviders: [UserPoolClientIdentityProvider.COGNITO], 92 | authFlows: { 93 | userSrp: true, 94 | custom: true, 95 | }, 96 | refreshTokenValidity: Duration.hours(1), 97 | }); 98 | 99 | const identityPool = new CfnIdentityPool(this, 'cognitoIdentityPool', { 100 | identityPoolName: 'cognitoIdentityPool', 101 | allowUnauthenticatedIdentities: false, 102 | cognitoIdentityProviders: [ 103 | { 104 | clientId: userPoolClient.userPoolClientId, 105 | providerName: userPool.userPoolProviderName, 106 | }, 107 | ], 108 | }); 109 | 110 | const unauthenticatedRole = new Role( 111 | this, 112 | 'CognitoDefaultUnauthenticatedRole', 113 | { 114 | assumedBy: new FederatedPrincipal( 115 | 'cognito-identity.amazonaws.com', 116 | { 117 | // eslint-disable-next-line quote-props 118 | StringEquals: { 119 | 'cognito-identity.amazonaws.com:aud': identityPool.ref, 120 | }, 121 | 'ForAnyValue:StringLike': { 122 | 'cognito-identity.amazonaws.com:amr': 'unauthenticated', 123 | }, 124 | }, 125 | 'sts:AssumeRoleWithWebIdentity', 126 | ), 127 | }, 128 | ); 129 | 130 | unauthenticatedRole.addToPolicy( 131 | new PolicyStatement({ 132 | effect: Effect.ALLOW, 133 | actions: ['mobileanalytics:PutEvents', 'cognito-sync:*'], 134 | resources: ['*'], 135 | }), 136 | ); 137 | 138 | const authenticatedRole = new Role( 139 | this, 140 | 'CognitoDefaultAuthenticatedRole', 141 | { 142 | assumedBy: new FederatedPrincipal( 143 | 'cognito-identity.amazonaws.com', 144 | { 145 | // eslint-disable-next-line quote-props 146 | StringEquals: { 147 | 'cognito-identity.amazonaws.com:aud': identityPool.ref, 148 | }, 149 | 'ForAnyValue:StringLike': { 150 | 'cognito-identity.amazonaws.com:amr': 'authenticated', 151 | }, 152 | }, 153 | 'sts:AssumeRoleWithWebIdentity', 154 | ), 155 | }, 156 | ); 157 | 158 | authenticatedRole.addToPolicy( 159 | new PolicyStatement({ 160 | effect: Effect.ALLOW, 161 | actions: [ 162 | 'mobileanalytics:PutEvents', 163 | 'cognito-sync:*', 164 | 'cognito-identity:*', 165 | ], 166 | resources: ['*'], 167 | }), 168 | ); 169 | 170 | authenticatedRole.addToPolicy( 171 | new PolicyStatement({ 172 | effect: Effect.ALLOW, 173 | actions: ['s3:*'], 174 | resources: [ 175 | props.concatBucket.bucketArn, 176 | `${props.concatBucket.bucketArn}/*`, 177 | ], 178 | }), 179 | ); 180 | 181 | new CfnIdentityPoolRoleAttachment(this, 'DefaultValid', { 182 | identityPoolId: identityPool.ref, 183 | roles: { 184 | unauthenticated: unauthenticatedRole.roleArn, 185 | authenticated: authenticatedRole.roleArn, 186 | }, 187 | }); 188 | 189 | this.authenticatedRole = authenticatedRole; 190 | this.identityPool = identityPool; 191 | this.userPool = userPool; 192 | this.userPoolClient = userPoolClient; 193 | this.userPoolRegion = Stack.of(this).region; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './infrastructure'; 2 | export * from './site'; 3 | export * from './cognito'; 4 | export * from './media-pipeline'; 5 | -------------------------------------------------------------------------------- /src/infrastructure.ts: -------------------------------------------------------------------------------- 1 | import { Duration, RemovalPolicy, Stack } from 'aws-cdk-lib'; 2 | import { 3 | RestApi, 4 | LambdaIntegration, 5 | EndpointType, 6 | MethodLoggingLevel, 7 | } from 'aws-cdk-lib/aws-apigateway'; 8 | import { Table, AttributeType, BillingMode } from 'aws-cdk-lib/aws-dynamodb'; 9 | import { 10 | Role, 11 | ServicePrincipal, 12 | PolicyDocument, 13 | PolicyStatement, 14 | ManagedPolicy, 15 | } from 'aws-cdk-lib/aws-iam'; 16 | import { Architecture, Runtime } from 'aws-cdk-lib/aws-lambda'; 17 | import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; 18 | import { Bucket } from 'aws-cdk-lib/aws-s3'; 19 | import { Construct } from 'constructs'; 20 | 21 | export interface InfrastructureProps { 22 | captureBucket: Bucket; 23 | concatBucket: Bucket; 24 | outputTable: Table; 25 | } 26 | export class Infrastructure extends Construct { 27 | public readonly apiUrl: string; 28 | constructor(scope: Construct, id: string, props: InfrastructureProps) { 29 | super(scope, id); 30 | 31 | const meetingsTable = new Table(this, 'meetings', { 32 | partitionKey: { 33 | name: 'meetingId', 34 | type: AttributeType.STRING, 35 | }, 36 | removalPolicy: RemovalPolicy.DESTROY, 37 | timeToLiveAttribute: 'timeToLive', 38 | billingMode: BillingMode.PAY_PER_REQUEST, 39 | }); 40 | 41 | const infrastructureRole = new Role(this, 'infrastructureRole', { 42 | assumedBy: new ServicePrincipal('lambda.amazonaws.com'), 43 | inlinePolicies: { 44 | ['chimePolicy']: new PolicyDocument({ 45 | statements: [ 46 | new PolicyStatement({ 47 | resources: ['*'], 48 | actions: ['chime:*'], 49 | }), 50 | ], 51 | }), 52 | ['transcribePolicy']: new PolicyDocument({ 53 | statements: [ 54 | new PolicyStatement({ 55 | resources: ['*'], 56 | actions: ['transcribe:*'], 57 | }), 58 | ], 59 | }), 60 | }, 61 | managedPolicies: [ 62 | ManagedPolicy.fromAwsManagedPolicyName( 63 | 'service-role/AWSLambdaBasicExecutionRole', 64 | ), 65 | ], 66 | }); 67 | 68 | const meetingLambda = new NodejsFunction(this, 'meetingLambda', { 69 | entry: 'src/resources/meetingInfo/meetingInfo.ts', 70 | runtime: Runtime.NODEJS_16_X, 71 | handler: 'lambdaHandler', 72 | architecture: Architecture.ARM_64, 73 | role: infrastructureRole, 74 | timeout: Duration.seconds(60), 75 | environment: { 76 | MEETINGS_TABLE: meetingsTable.tableName, 77 | OUTPUT_TABLE: props.outputTable.tableName, 78 | CAPTURE_BUCKET_ARN: props.captureBucket.bucketArn, 79 | CONCAT_BUCKET_ARN: props.concatBucket.bucketArn, 80 | AWS_ACCOUNT_ID: Stack.of(this).account, 81 | }, 82 | }); 83 | 84 | meetingsTable.grantReadWriteData(meetingLambda); 85 | props.outputTable.grantReadWriteData(meetingLambda); 86 | props.concatBucket.grantReadWrite(meetingLambda); 87 | props.captureBucket.grantReadWrite(meetingLambda); 88 | 89 | const api = new RestApi(this, 'ChimeSDKMeetingWithTranscribeAPI', { 90 | defaultCorsPreflightOptions: { 91 | allowHeaders: [ 92 | 'Content-Type', 93 | 'X-Amz-Date', 94 | 'X-Amz-Security-Token', 95 | 'Authorization', 96 | 'X-Api-Key', 97 | ], 98 | allowMethods: ['OPTIONS', 'POST'], 99 | allowCredentials: true, 100 | allowOrigins: ['*'], 101 | }, 102 | deployOptions: { 103 | loggingLevel: MethodLoggingLevel.INFO, 104 | dataTraceEnabled: true, 105 | }, 106 | endpointConfiguration: { 107 | types: [EndpointType.REGIONAL], 108 | }, 109 | }); 110 | 111 | const meeting = api.root.addResource('meeting'); 112 | const end = api.root.addResource('end'); 113 | const recordings = api.root.addResource('recordings'); 114 | 115 | const meetingIntegration = new LambdaIntegration(meetingLambda); 116 | 117 | meeting.addMethod('POST', meetingIntegration, {}); 118 | end.addMethod('POST', meetingIntegration, {}); 119 | recordings.addMethod('POST', meetingIntegration, {}); 120 | 121 | this.apiUrl = api.url; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/media-pipeline.ts: -------------------------------------------------------------------------------- 1 | import { Duration, RemovalPolicy, Stack } from 'aws-cdk-lib'; 2 | import { Table, AttributeType, BillingMode } from 'aws-cdk-lib/aws-dynamodb'; 3 | import { Rule } from 'aws-cdk-lib/aws-events'; 4 | import { LambdaFunction } from 'aws-cdk-lib/aws-events-targets'; 5 | import { 6 | Role, 7 | ServicePrincipal, 8 | PolicyDocument, 9 | Effect, 10 | PolicyStatement, 11 | ManagedPolicy, 12 | } from 'aws-cdk-lib/aws-iam'; 13 | import { Architecture, Runtime } from 'aws-cdk-lib/aws-lambda'; 14 | import { S3EventSource } from 'aws-cdk-lib/aws-lambda-event-sources'; 15 | import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; 16 | import { 17 | BlockPublicAccess, 18 | Bucket, 19 | EventType, 20 | HttpMethods, 21 | } from 'aws-cdk-lib/aws-s3'; 22 | import { Construct } from 'constructs'; 23 | 24 | export class MediaPipeline extends Construct { 25 | public concatBucket: Bucket; 26 | public captureBucket: Bucket; 27 | public outputTable: Table; 28 | constructor(scope: Construct, id: string) { 29 | super(scope, id); 30 | 31 | this.captureBucket = new Bucket(this, 'captureBucket', { 32 | publicReadAccess: false, 33 | removalPolicy: RemovalPolicy.DESTROY, 34 | blockPublicAccess: BlockPublicAccess.BLOCK_ALL, 35 | autoDeleteObjects: true, 36 | }); 37 | 38 | this.concatBucket = new Bucket(this, 'concatBucket', { 39 | publicReadAccess: false, 40 | removalPolicy: RemovalPolicy.DESTROY, 41 | blockPublicAccess: BlockPublicAccess.BLOCK_ALL, 42 | cors: [ 43 | { 44 | allowedHeaders: ['*'], 45 | allowedMethods: [HttpMethods.GET], 46 | allowedOrigins: ['*'], 47 | exposedHeaders: [ 48 | 'x-amz-server-side-encryption', 49 | 'x-amz-security-token', 50 | 'x-amz-request-id', 51 | 'x-amz-id-2', 52 | 'ETag', 53 | ], 54 | maxAge: 3000, 55 | }, 56 | ], 57 | autoDeleteObjects: true, 58 | }); 59 | 60 | const captureBucketPolicy = new PolicyStatement({ 61 | principals: [new ServicePrincipal('mediapipelines.chime.amazonaws.com')], 62 | sid: 'AWSChimeMediaCaptureBucketPolicy', 63 | actions: [ 64 | 's3:PutObject', 65 | 's3:PutObjectAcl', 66 | 's3:GetObject', 67 | 's3:ListBucket', 68 | ], 69 | effect: Effect.ALLOW, 70 | resources: [ 71 | `${this.captureBucket.bucketArn}/*`, 72 | `${this.captureBucket.bucketArn}`, 73 | ], 74 | conditions: { 75 | StringEquals: { 76 | 'aws:SourceAccount': Stack.of(this).account, 77 | }, 78 | ArnLike: { 79 | 'aws:SourceArn': `arn:aws:chime:*:${Stack.of(this).account}:*`, 80 | }, 81 | }, 82 | }); 83 | 84 | const concatBucketPolicy = new PolicyStatement({ 85 | principals: [new ServicePrincipal('mediapipelines.chime.amazonaws.com')], 86 | sid: 'AWSChimeMediaConcatBucketPolicy', 87 | actions: ['s3:PutObject', 's3:PutObjectAcl'], 88 | effect: Effect.ALLOW, 89 | resources: [ 90 | `${this.concatBucket.bucketArn}/*`, 91 | `${this.concatBucket.bucketArn}`, 92 | ], 93 | conditions: { 94 | StringEquals: { 95 | 'aws:SourceAccount': Stack.of(this).account, 96 | }, 97 | ArnLike: { 98 | 'aws:SourceArn': `arn:aws:chime:*:${Stack.of(this).account}:*`, 99 | }, 100 | }, 101 | }); 102 | 103 | this.captureBucket.addToResourcePolicy(captureBucketPolicy); 104 | this.concatBucket.addToResourcePolicy(concatBucketPolicy); 105 | 106 | const eventBridgeLambdaRole = new Role(this, 'eventBridgeLambdaRole', { 107 | assumedBy: new ServicePrincipal('lambda.amazonaws.com'), 108 | inlinePolicies: { 109 | ['chimePolicy']: new PolicyDocument({ 110 | statements: [ 111 | new PolicyStatement({ 112 | resources: ['*'], 113 | actions: ['chime:*'], 114 | }), 115 | ], 116 | }), 117 | }, 118 | managedPolicies: [ 119 | ManagedPolicy.fromAwsManagedPolicyName( 120 | 'service-role/AWSLambdaBasicExecutionRole', 121 | ), 122 | ], 123 | }); 124 | 125 | const eventBridgeLambda = new NodejsFunction(this, 'eventBridgeLambda', { 126 | entry: 'src/resources/eventBridge/eventBridge.ts', 127 | runtime: Runtime.NODEJS_16_X, 128 | handler: 'lambdaHandler', 129 | architecture: Architecture.ARM_64, 130 | role: eventBridgeLambdaRole, 131 | timeout: Duration.seconds(60), 132 | environment: { 133 | CAPTURE_BUCKET_ARN: this.captureBucket.bucketArn, 134 | CONCAT_BUCKET_ARN: this.concatBucket.bucketArn, 135 | }, 136 | }); 137 | 138 | this.captureBucket.grantReadWrite(eventBridgeLambda); 139 | this.concatBucket.grantReadWrite(eventBridgeLambda); 140 | const chimeSdkRule = new Rule(this, 'chimeSdkRule', { 141 | eventPattern: { 142 | source: ['aws.chime'], 143 | }, 144 | }); 145 | chimeSdkRule.addTarget(new LambdaFunction(eventBridgeLambda)); 146 | 147 | this.outputTable = new Table(this, 'meetings', { 148 | partitionKey: { 149 | name: 'mediaPipelineId', 150 | type: AttributeType.STRING, 151 | }, 152 | removalPolicy: RemovalPolicy.DESTROY, 153 | billingMode: BillingMode.PAY_PER_REQUEST, 154 | }); 155 | 156 | const postMeetingLambdaRole = new Role(this, 'postMeetingLambdaRole', { 157 | assumedBy: new ServicePrincipal('lambda.amazonaws.com'), 158 | inlinePolicies: { 159 | ['chimePolicy']: new PolicyDocument({ 160 | statements: [ 161 | new PolicyStatement({ 162 | resources: ['*'], 163 | actions: ['chime:*'], 164 | }), 165 | ], 166 | }), 167 | }, 168 | managedPolicies: [ 169 | ManagedPolicy.fromAwsManagedPolicyName( 170 | 'service-role/AWSLambdaBasicExecutionRole', 171 | ), 172 | ], 173 | }); 174 | 175 | const postMeetingLambda = new NodejsFunction(this, 'postMeetingLambda', { 176 | entry: 'src/resources/postMeeting/postMeeting.ts', 177 | runtime: Runtime.NODEJS_16_X, 178 | handler: 'lambdaHandler', 179 | architecture: Architecture.ARM_64, 180 | role: postMeetingLambdaRole, 181 | timeout: Duration.seconds(60), 182 | environment: { 183 | CAPTURE_BUCKET_ARN: this.captureBucket.bucketArn, 184 | CONCAT_BUCKET_ARN: this.concatBucket.bucketArn, 185 | OUTPUT_TABLE: this.outputTable.tableName, 186 | }, 187 | }); 188 | 189 | this.outputTable.grantReadWriteData(postMeetingLambda); 190 | 191 | postMeetingLambda.addEventSource( 192 | new S3EventSource(this.concatBucket, { 193 | events: [EventType.OBJECT_CREATED], 194 | }), 195 | ); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/resources/cognitoDomain/domainValidator.js: -------------------------------------------------------------------------------- 1 | const allowedDomain = process.env['ALLOWED_DOMAIN']; 2 | 3 | exports.handler = async (event, context, callback) => { 4 | const userEmailDomain = event.request.userAttributes.email.split('@')[1]; 5 | if ( 6 | userEmailDomain === allowedDomain || 7 | !allowedDomain || 8 | allowedDomain.length === 0 9 | ) { 10 | callback(null, event); 11 | } else { 12 | const error = new Error( 13 | 'Cannot authenticate users from domains different from ' + allowedDomain, 14 | ); 15 | callback(error, event); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/resources/eventBridge/eventBridge.ts: -------------------------------------------------------------------------------- 1 | /*eslint import/no-unresolved: 0 */ 2 | import { 3 | ChimeSDKMediaPipelinesClient, 4 | CreateMediaConcatenationPipelineCommand, 5 | CreateMediaConcatenationPipelineCommandInput, 6 | GetMediaPipelineCommand, 7 | GetMediaPipelineCommandOutput, 8 | } from '@aws-sdk/client-chime-sdk-media-pipelines'; 9 | import { 10 | ChimeSDKMeetingsClient, 11 | ListAttendeesCommand, 12 | DeleteMeetingCommand, 13 | DeleteAttendeeCommand, 14 | } from '@aws-sdk/client-chime-sdk-meetings'; 15 | import { Handler } from 'aws-cdk-lib/aws-lambda'; 16 | 17 | const chimeSdkMediaPipelinesClient = new ChimeSDKMediaPipelinesClient({ 18 | region: 'us-east-1', 19 | }); 20 | 21 | const chimeSdkMeetingsClient = new ChimeSDKMeetingsClient({ 22 | region: 'us-east-1', 23 | }); 24 | var concatBucketArn = process.env.CONCAT_BUCKET_ARN; 25 | 26 | interface Detail { 27 | version: '0'; 28 | eventType: string; 29 | timestamp: number; 30 | meetingId: string; 31 | attendeeId: string; 32 | externalUserId: string; 33 | externalMeetingId: string; 34 | mediaPipelineId: string; 35 | mediaRegion: string; 36 | } 37 | interface EventBridge { 38 | 'version': '0'; 39 | 'id': string; 40 | 'detail-type': string; 41 | 'source': 'aws.chime'; 42 | 'account': string; 43 | 'time': string; 44 | 'region': string; 45 | 'resources': []; 46 | 'detail': Detail; 47 | } 48 | 49 | export const lambdaHandler: Handler = async ( 50 | event: EventBridge, 51 | ): Promise => { 52 | console.info(event); 53 | 54 | switch (event['detail-type']) { 55 | case 'Chime Meeting State Change': 56 | if ( 57 | event.detail.eventType == 'chime:AttendeeLeft' || 58 | event.detail.eventType == 'chime:AttendeeDropped' 59 | ) { 60 | await deleteAttendee(event.detail.meetingId, event.detail.attendeeId); 61 | } 62 | 63 | if (event.detail.eventType == 'chime:AttendeeDeleted') { 64 | const meetingAttendees = await listAttendees(event.detail.meetingId); 65 | 66 | if (meetingAttendees && meetingAttendees.Attendees) { 67 | if ( 68 | meetingAttendees.Attendees.length == 1 && 69 | meetingAttendees.Attendees[0].ExternalUserId?.slice(0, 17) == 70 | 'aws:MediaPipeline' 71 | ) { 72 | await deleteMeeting(event.detail.meetingId); 73 | } 74 | } 75 | } 76 | 77 | break; 78 | case 'Chime Media Pipeline State Change': 79 | if (event.detail.eventType == 'chime:MediaPipelineInProgress') { 80 | const mediaCapturePipeline = await getMediaPipeline( 81 | event.detail.mediaPipelineId, 82 | ); 83 | if ( 84 | mediaCapturePipeline && 85 | mediaCapturePipeline.MediaPipeline && 86 | mediaCapturePipeline.MediaPipeline.MediaCapturePipeline && 87 | mediaCapturePipeline.MediaPipeline.MediaCapturePipeline 88 | .MediaPipelineArn 89 | ) { 90 | await startConcat( 91 | mediaCapturePipeline.MediaPipeline.MediaCapturePipeline 92 | .MediaPipelineArn, 93 | ); 94 | } 95 | } 96 | break; 97 | } 98 | return null; 99 | }; 100 | 101 | async function getMediaPipeline(mediaPipelineId: string) { 102 | try { 103 | const getMediaPipelineResponse: GetMediaPipelineCommandOutput = 104 | await chimeSdkMediaPipelinesClient.send( 105 | new GetMediaPipelineCommand({ MediaPipelineId: mediaPipelineId }), 106 | ); 107 | return getMediaPipelineResponse; 108 | } catch (error) { 109 | console.log(error); 110 | return false; 111 | } 112 | } 113 | 114 | async function listAttendees(meetingId: string) { 115 | try { 116 | const listAttendeesResponse = await chimeSdkMeetingsClient.send( 117 | new ListAttendeesCommand({ MeetingId: meetingId }), 118 | ); 119 | return listAttendeesResponse; 120 | } catch (error) { 121 | console.log(error); 122 | return false; 123 | } 124 | } 125 | 126 | async function deleteAttendee(meetingId: string, attendeeId: string) { 127 | try { 128 | await chimeSdkMeetingsClient.send( 129 | new DeleteAttendeeCommand({ 130 | MeetingId: meetingId, 131 | AttendeeId: attendeeId, 132 | }), 133 | ); 134 | } catch (error) { 135 | console.log(error); 136 | } 137 | } 138 | 139 | async function deleteMeeting(meetingId: string) { 140 | try { 141 | await chimeSdkMeetingsClient.send( 142 | new DeleteMeetingCommand({ MeetingId: meetingId }), 143 | ); 144 | } catch (error) { 145 | console.log(error); 146 | } 147 | } 148 | 149 | async function startConcat(mediaCapturePipelineArn: string) { 150 | const createConcatPipelineParams: CreateMediaConcatenationPipelineCommandInput = 151 | { 152 | Sinks: [ 153 | { 154 | S3BucketSinkConfiguration: { Destination: concatBucketArn }, 155 | Type: 'S3Bucket', 156 | }, 157 | ], 158 | Sources: [ 159 | { 160 | MediaCapturePipelineSourceConfiguration: { 161 | ChimeSdkMeetingConfiguration: { 162 | ArtifactsConfiguration: { 163 | Audio: { State: 'Enabled' }, 164 | CompositedVideo: { State: 'Enabled' }, 165 | Content: { State: 'Disabled' }, 166 | DataChannel: { State: 'Enabled' }, 167 | MeetingEvents: { State: 'Enabled' }, 168 | TranscriptionMessages: { State: 'Enabled' }, 169 | Video: { State: 'Disabled' }, 170 | }, 171 | }, 172 | MediaPipelineArn: mediaCapturePipelineArn, 173 | }, 174 | Type: 'MediaCapturePipeline', 175 | }, 176 | ], 177 | }; 178 | console.log(JSON.stringify(createConcatPipelineParams)); 179 | try { 180 | await chimeSdkMediaPipelinesClient.send( 181 | new CreateMediaConcatenationPipelineCommand(createConcatPipelineParams), 182 | ); 183 | return true; 184 | } catch (error) { 185 | console.log(error); 186 | return false; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/resources/meetingInfo/meetingInfo.ts: -------------------------------------------------------------------------------- 1 | /*eslint import/no-unresolved: 0 */ 2 | import { randomUUID } from 'crypto'; 3 | import { 4 | ChimeSDKMediaPipelinesClient, 5 | CreateMediaCapturePipelineCommand, 6 | CreateMediaCapturePipelineCommandInput, 7 | CreateMediaCapturePipelineCommandOutput, 8 | } from '@aws-sdk/client-chime-sdk-media-pipelines'; 9 | import { 10 | ChimeSDKMeetingsClient, 11 | DeleteMeetingCommand, 12 | CreateAttendeeCommand, 13 | CreateMeetingCommand, 14 | CreateMeetingCommandOutput, 15 | CreateMeetingCommandInput, 16 | CreateAttendeeCommandInput, 17 | CreateAttendeeCommandOutput, 18 | GetMeetingCommand, 19 | StartMeetingTranscriptionCommand, 20 | Attendee, 21 | Meeting, 22 | DeleteMeetingCommandOutput, 23 | } from '@aws-sdk/client-chime-sdk-meetings'; 24 | import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; 25 | import { 26 | DynamoDBDocumentClient, 27 | ScanCommand, 28 | PutCommand, 29 | PutCommandOutput, 30 | ScanCommandInput, 31 | ScanCommandOutput, 32 | DeleteCommand, 33 | DeleteCommandInput, 34 | DeleteCommandOutput, 35 | } from '@aws-sdk/lib-dynamodb'; 36 | import { APIGatewayProxyResult, APIGatewayEvent } from 'aws-lambda'; 37 | const ddbClient = new DynamoDBClient({ region: 'us-east-1' }); 38 | const marshallOptions = { 39 | convertEmptyValues: false, 40 | removeUndefinedValues: true, 41 | convertClassInstanceToMap: false, 42 | }; 43 | const unmarshallOptions = { 44 | wrapNumbers: false, 45 | }; 46 | const translateConfig = { marshallOptions, unmarshallOptions }; 47 | const ddbDocClient = DynamoDBDocumentClient.from(ddbClient, translateConfig); 48 | 49 | const chimeSdkMeetings = new ChimeSDKMeetingsClient({ 50 | region: 'us-east-1', 51 | }); 52 | 53 | const chimeSdkMediaPipelinesClient = new ChimeSDKMediaPipelinesClient({ 54 | region: 'us-east-1', 55 | }); 56 | 57 | var meetingInfoTable = process.env.MEETINGS_TABLE; 58 | var outputTable = process.env.OUTPUT_TABLE; 59 | var captureBucketArn = process.env.CAPTURE_BUCKET_ARN; 60 | var awsAccountId = process.env.AWS_ACCOUNT_ID; 61 | 62 | interface JoinInfo { 63 | Meeting: Meeting; 64 | Attendee: Array; 65 | } 66 | 67 | var response: APIGatewayProxyResult = { 68 | statusCode: 200, 69 | body: '', 70 | headers: { 71 | 'Access-Control-Allow-Origin': '*', 72 | 'Access-Control-Allow-Headers': '*', 73 | 'Access-Control-Allow-Methods': 'POST, OPTIONS', 74 | 'Content-Type': 'application/json', 75 | }, 76 | }; 77 | 78 | var createMeetingCommandInput: CreateMeetingCommandInput = { 79 | ClientRequestToken: '', 80 | ExternalMeetingId: '', 81 | MediaRegion: 'us-east-1', 82 | }; 83 | 84 | var createAttendeeCommandInput: CreateAttendeeCommandInput = { 85 | MeetingId: '', 86 | ExternalUserId: '', 87 | }; 88 | 89 | export const lambdaHandler = async ( 90 | event: APIGatewayEvent, 91 | ): Promise => { 92 | console.info(event); 93 | 94 | switch (event.path) { 95 | case '/meeting': 96 | return meetingRequest(event); 97 | case '/end': 98 | return endRequest(event); 99 | case '/recordings': 100 | return recordingsRequest(); 101 | default: 102 | return response; 103 | } 104 | }; 105 | 106 | async function recordingsRequest() { 107 | const scanOutputTable: ScanCommandInput = { 108 | TableName: outputTable, 109 | }; 110 | try { 111 | const data: ScanCommandOutput = await ddbDocClient.send( 112 | new ScanCommand(scanOutputTable), 113 | ); 114 | console.log(data); 115 | response.body = JSON.stringify(data.Items); 116 | response.statusCode = 200; 117 | return response; 118 | } catch (err) { 119 | console.log(err); 120 | response.body = JSON.stringify('No recordings found'); 121 | response.statusCode = 404; 122 | return response; 123 | } 124 | } 125 | 126 | async function endRequest(event: APIGatewayEvent) { 127 | if (event.body) { 128 | const body = JSON.parse(event.body); 129 | const deleteMeetingResponse: DeleteMeetingCommandOutput = 130 | await chimeSdkMeetings.send( 131 | new DeleteMeetingCommand({ MeetingId: body.meetingId }), 132 | ); 133 | console.log(JSON.stringify(deleteMeetingResponse)); 134 | const deleteCommandParams: DeleteCommandInput = { 135 | TableName: meetingInfoTable, 136 | Key: { meetingId: body.meetingId }, 137 | }; 138 | console.log(deleteCommandParams); 139 | const deleteItemResponse: DeleteCommandOutput = await ddbDocClient.send( 140 | new DeleteCommand(deleteCommandParams), 141 | ); 142 | console.log(JSON.stringify(deleteItemResponse)); 143 | 144 | response.body = JSON.stringify('Meeting Deleted'); 145 | response.statusCode = 200; 146 | return response; 147 | } else { 148 | response.body = JSON.stringify('Meeting not found'); 149 | response.statusCode = 404; 150 | return response; 151 | } 152 | } 153 | 154 | async function meetingRequest(event: APIGatewayEvent) { 155 | const currentMeetings = await checkForMeetings(); 156 | 157 | if (event.body) { 158 | const body = JSON.parse(event.body); 159 | const attendeeEmail = body.email; 160 | if (currentMeetings) { 161 | for (let meeting of currentMeetings) { 162 | try { 163 | await chimeSdkMeetings.send( 164 | new GetMeetingCommand({ MeetingId: meeting.meetingId }), 165 | ); 166 | console.log('Adding an attendee to an existing meeting'); 167 | console.log(JSON.stringify(meeting.joinInfo)); 168 | const attendeeInfo = await createAttendee( 169 | meeting.meetingId, 170 | attendeeEmail, 171 | ); 172 | console.log(`attendeeInfo: ${JSON.stringify(attendeeInfo)}`); 173 | meeting.joinInfo.Attendee.push(attendeeInfo.Attendee); 174 | console.log(JSON.stringify(meeting.joinInfo)); 175 | await putMeetingInfo(meeting.joinInfo); 176 | 177 | const responseInfo = { 178 | Meeting: meeting.joinInfo.Meeting, 179 | Attendee: attendeeInfo.Attendee, 180 | }; 181 | 182 | response.statusCode = 200; 183 | response.body = JSON.stringify(responseInfo); 184 | console.info('joinInfo: ' + JSON.stringify(response)); 185 | return response; 186 | } catch (err) { 187 | console.log(`Error: ${err}`); 188 | continue; 189 | } 190 | } 191 | } 192 | 193 | const meetingInfo = await createMeeting(); 194 | if (meetingInfo && meetingInfo.Meeting && meetingInfo.Meeting.MeetingId) { 195 | const attendeeInfo: CreateAttendeeCommandOutput = await createAttendee( 196 | meetingInfo.Meeting.MeetingId, 197 | attendeeEmail, 198 | ); 199 | let joinInfo: JoinInfo; 200 | if (attendeeInfo && attendeeInfo.Attendee) { 201 | joinInfo = { 202 | Meeting: meetingInfo.Meeting, 203 | Attendee: [attendeeInfo.Attendee], 204 | }; 205 | const responseInfo = { 206 | Meeting: meetingInfo.Meeting, 207 | Attendee: attendeeInfo.Attendee, 208 | }; 209 | 210 | await startTranscribe(meetingInfo.Meeting.MeetingId); 211 | const mediaCapturePipelineArn = await startCapture( 212 | meetingInfo.Meeting.MeetingId, 213 | ); 214 | if (mediaCapturePipelineArn) { 215 | await putMeetingInfo(joinInfo); 216 | response.statusCode = 200; 217 | response.body = JSON.stringify(responseInfo); 218 | console.info('joinInfo: ' + JSON.stringify(response)); 219 | return response; 220 | } 221 | } 222 | } 223 | } 224 | return response; 225 | } 226 | async function createMeeting() { 227 | console.log('Creating Meeting'); 228 | createMeetingCommandInput.ClientRequestToken = randomUUID(); 229 | createMeetingCommandInput.ExternalMeetingId = randomUUID(); 230 | try { 231 | const meetingInfo: CreateMeetingCommandOutput = await chimeSdkMeetings.send( 232 | new CreateMeetingCommand(createMeetingCommandInput), 233 | ); 234 | console.info(`Meeting Info: ${JSON.stringify(meetingInfo)}`); 235 | return meetingInfo; 236 | } catch (err) { 237 | console.info(`Error: ${err}`); 238 | return false; 239 | } 240 | } 241 | 242 | async function createAttendee(meetingId: string, attendeeEmail: string) { 243 | console.log(`Creating Attendee for Meeting: ${meetingId}`); 244 | createAttendeeCommandInput.MeetingId = meetingId; 245 | createAttendeeCommandInput.ExternalUserId = attendeeEmail; 246 | const attendeeInfo: CreateAttendeeCommandOutput = await chimeSdkMeetings.send( 247 | new CreateAttendeeCommand(createAttendeeCommandInput), 248 | ); 249 | return attendeeInfo; 250 | } 251 | 252 | async function putMeetingInfo(joinInfo: JoinInfo) { 253 | var timeToLive = new Date(); 254 | timeToLive.setMinutes(timeToLive.getMinutes() + 5); 255 | const putMeetingInfoInput = { 256 | TableName: meetingInfoTable, 257 | Item: { 258 | meetingId: joinInfo.Meeting.MeetingId, 259 | joinInfo, 260 | timeToLive: timeToLive.getTime() / 1e3, 261 | }, 262 | }; 263 | console.log(`info to put: ${JSON.stringify(putMeetingInfoInput)}`); 264 | try { 265 | const data: PutCommandOutput = await ddbDocClient.send( 266 | new PutCommand(putMeetingInfoInput), 267 | ); 268 | console.log('Success - item added or updated', data); 269 | return data; 270 | } catch (err) { 271 | console.log('Error', err); 272 | return false; 273 | } 274 | } 275 | async function checkForMeetings() { 276 | const scanMeetingInfo: ScanCommandInput = { 277 | TableName: meetingInfoTable, 278 | FilterExpression: 'timeToLive >= :currentEpoch', 279 | ExpressionAttributeValues: { 280 | ':currentEpoch': Date.now() / 1e3, 281 | }, 282 | }; 283 | try { 284 | const data: ScanCommandOutput = await ddbDocClient.send( 285 | new ScanCommand(scanMeetingInfo), 286 | ); 287 | console.log(data); 288 | return data.Items; 289 | } catch (err) { 290 | console.log('Error', err); 291 | return false; 292 | } 293 | } 294 | 295 | async function startTranscribe(meetingId: string) { 296 | try { 297 | const transcribeResponse = await chimeSdkMeetings.send( 298 | new StartMeetingTranscriptionCommand({ 299 | MeetingId: meetingId, 300 | TranscriptionConfiguration: { 301 | EngineTranscribeSettings: { 302 | LanguageCode: 'en-US', 303 | }, 304 | }, 305 | }), 306 | ); 307 | console.log(JSON.stringify(transcribeResponse)); 308 | return true; 309 | } catch (error) { 310 | return false; 311 | } 312 | } 313 | 314 | async function startCapture(meetingId: string) { 315 | const createPipelineParams: CreateMediaCapturePipelineCommandInput = { 316 | ChimeSdkMeetingConfiguration: { 317 | ArtifactsConfiguration: { 318 | Audio: { MuxType: 'AudioOnly' }, 319 | CompositedVideo: { 320 | GridViewConfiguration: { 321 | ContentShareLayout: 'PresenterOnly', 322 | }, 323 | Layout: 'GridView', 324 | Resolution: 'FHD', 325 | }, 326 | Content: { State: 'Disabled' }, 327 | Video: { State: 'Disabled', MuxType: 'VideoOnly' }, 328 | }, 329 | }, 330 | SinkArn: captureBucketArn, 331 | SinkType: 'S3Bucket', 332 | SourceArn: `arn:aws:chime::${awsAccountId}:meeting:${meetingId}`, 333 | SourceType: 'ChimeSdkMeeting', 334 | Tags: [{ Key: 'transcription-for-comprehend', Value: 'true' }], 335 | }; 336 | console.log(JSON.stringify(createPipelineParams)); 337 | try { 338 | const createMediaCapturePipelineResponse: CreateMediaCapturePipelineCommandOutput = 339 | await chimeSdkMediaPipelinesClient.send( 340 | new CreateMediaCapturePipelineCommand(createPipelineParams), 341 | ); 342 | return createMediaCapturePipelineResponse.MediaCapturePipeline 343 | ?.MediaPipelineArn; 344 | } catch (error) { 345 | console.log(error); 346 | return false; 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /src/resources/postMeeting/postMeeting.ts: -------------------------------------------------------------------------------- 1 | /*eslint import/no-unresolved: 0 */ 2 | import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; 3 | import { 4 | DynamoDBDocumentClient, 5 | UpdateCommand, 6 | UpdateCommandInput, 7 | UpdateCommandOutput, 8 | } from '@aws-sdk/lib-dynamodb'; 9 | const ddbClient = new DynamoDBClient({ region: 'us-east-1' }); 10 | const marshallOptions = { 11 | convertEmptyValues: false, 12 | removeUndefinedValues: true, 13 | convertClassInstanceToMap: false, 14 | }; 15 | const unmarshallOptions = { 16 | wrapNumbers: false, 17 | }; 18 | const translateConfig = { marshallOptions, unmarshallOptions }; 19 | const ddbDocClient = DynamoDBDocumentClient.from(ddbClient, translateConfig); 20 | 21 | var outputTable = process.env.OUTPUT_TABLE; 22 | 23 | import { Handler } from 'aws-cdk-lib/aws-lambda'; 24 | import { S3Event } from 'aws-lambda'; 25 | 26 | export const lambdaHandler: Handler = async (event: S3Event): Promise => { 27 | console.info(JSON.stringify(event)); 28 | 29 | const keyInfo: Array = event.Records[0].s3.object.key.split('/'); 30 | const mediaPipelineId: string = keyInfo[keyInfo.length - 3]; 31 | const keyType: string = keyInfo[keyInfo.length - 2]; 32 | const key: string = event.Records[0].s3.object.key; 33 | await updateOutputTable(mediaPipelineId, keyType, key); 34 | return null; 35 | }; 36 | 37 | async function updateOutputTable( 38 | mediaPipelineId: string, 39 | keyType: string, 40 | key: string, 41 | ) { 42 | const updateOutputTableInput: UpdateCommandInput = { 43 | TableName: outputTable, 44 | Key: { mediaPipelineId: mediaPipelineId }, 45 | UpdateExpression: 'SET #kt = :k, #ts = :ts', 46 | ExpressionAttributeNames: { 47 | '#kt': keyType, 48 | '#ts': 'timestamp', 49 | }, 50 | ExpressionAttributeValues: { 51 | ':k': key, 52 | ':ts': Date.now() / 1e3, 53 | }, 54 | }; 55 | console.log(`info to put: ${JSON.stringify(updateOutputTableInput)}`); 56 | try { 57 | const data: UpdateCommandOutput = await ddbDocClient.send( 58 | new UpdateCommand(updateOutputTableInput), 59 | ); 60 | console.log('Success - item added or updated', data); 61 | return data; 62 | } catch (err) { 63 | console.log('Error', err); 64 | return false; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/site.ts: -------------------------------------------------------------------------------- 1 | import { execSync, ExecSyncOptions } from 'child_process'; 2 | import { RemovalPolicy, DockerImage, Stack } from 'aws-cdk-lib'; 3 | import { 4 | Distribution, 5 | SecurityPolicyProtocol, 6 | CachePolicy, 7 | ViewerProtocolPolicy, 8 | } from 'aws-cdk-lib/aws-cloudfront'; 9 | import { S3Origin } from 'aws-cdk-lib/aws-cloudfront-origins'; 10 | import { 11 | IUserPool, 12 | IUserPoolClient, 13 | CfnIdentityPool, 14 | } from 'aws-cdk-lib/aws-cognito'; 15 | import { Bucket } from 'aws-cdk-lib/aws-s3'; 16 | import { Source, BucketDeployment } from 'aws-cdk-lib/aws-s3-deployment'; 17 | import { Construct } from 'constructs'; 18 | import { copySync } from 'fs-extra'; 19 | 20 | interface SiteProps { 21 | apiUrl: string; 22 | concatBucket: Bucket; 23 | userPool: IUserPool; 24 | userPoolClient: IUserPoolClient; 25 | identityPool: CfnIdentityPool; 26 | } 27 | 28 | export class Site extends Construct { 29 | public readonly siteBucket: Bucket; 30 | public readonly distribution: Distribution; 31 | 32 | constructor(scope: Construct, id: string, props: SiteProps) { 33 | super(scope, id); 34 | 35 | this.siteBucket = new Bucket(this, 'websiteBucket', { 36 | publicReadAccess: false, 37 | removalPolicy: RemovalPolicy.DESTROY, 38 | autoDeleteObjects: true, 39 | }); 40 | 41 | this.distribution = new Distribution(this, 'CloudfrontDistribution', { 42 | enableLogging: true, 43 | minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2021, 44 | errorResponses: [ 45 | { 46 | httpStatus: 404, 47 | responsePagePath: '/index.html', 48 | responseHttpStatus: 200, 49 | }, 50 | { 51 | httpStatus: 403, 52 | responsePagePath: '/index.html', 53 | responseHttpStatus: 200, 54 | }, 55 | ], 56 | defaultBehavior: { 57 | origin: new S3Origin(this.siteBucket), 58 | viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 59 | cachePolicy: CachePolicy.CACHING_DISABLED, 60 | }, 61 | defaultRootObject: 'index.html', 62 | }); 63 | const execOptions: ExecSyncOptions = { stdio: 'inherit' }; 64 | 65 | const bundle = Source.asset('./site', { 66 | bundling: { 67 | command: [ 68 | 'sh', 69 | '-c', 70 | 'echo "Docker build not supported. Please install esbuild."', 71 | ], 72 | image: DockerImage.fromRegistry('alpine'), 73 | local: { 74 | tryBundle(outputDir: string) { 75 | try { 76 | execSync('esbuild --version', execOptions); 77 | } catch { 78 | /* istanbul ignore next */ 79 | return false; 80 | } 81 | execSync( 82 | 'cd site && yarn install --frozen-lockfile && yarn build', 83 | execOptions, 84 | ); 85 | copySync('./site/dist', outputDir, { 86 | ...execOptions, 87 | recursive: true, 88 | }); 89 | return true; 90 | }, 91 | }, 92 | }, 93 | }); 94 | 95 | const config = { 96 | apiUrl: props.apiUrl, 97 | concatBucket: props.concatBucket.bucketName, 98 | userPoolRegion: Stack.of(this).region, 99 | userPoolId: props.userPool.userPoolId, 100 | userPoolClientId: props.userPoolClient.userPoolClientId, 101 | identityPoolId: props.identityPool.ref, 102 | }; 103 | 104 | new BucketDeployment(this, 'DeployBucket', { 105 | sources: [bundle, Source.jsonData('config.json', config)], 106 | destinationBucket: this.siteBucket, 107 | distribution: this.distribution, 108 | distributionPaths: ['/*'], 109 | }); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | { 3 | "compilerOptions": { 4 | "alwaysStrict": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "inlineSourceMap": true, 9 | "inlineSources": true, 10 | "lib": [ 11 | "es2019" 12 | ], 13 | "module": "CommonJS", 14 | "noEmitOnError": false, 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitAny": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "resolveJsonModule": true, 22 | "strict": true, 23 | "strictNullChecks": true, 24 | "strictPropertyInitialization": true, 25 | "stripInternal": true, 26 | "target": "ES2019" 27 | }, 28 | "include": [ 29 | "src/**/*.ts", 30 | "test/**/*.ts", 31 | ".projenrc.ts", 32 | "projenrc/**/*.ts" 33 | ], 34 | "exclude": [ 35 | "node_modules" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | { 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib", 6 | "alwaysStrict": true, 7 | "declaration": true, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true, 10 | "inlineSourceMap": true, 11 | "inlineSources": true, 12 | "lib": [ 13 | "es2019" 14 | ], 15 | "module": "CommonJS", 16 | "noEmitOnError": false, 17 | "noFallthroughCasesInSwitch": true, 18 | "noImplicitAny": true, 19 | "noImplicitReturns": true, 20 | "noImplicitThis": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "resolveJsonModule": true, 24 | "strict": true, 25 | "strictNullChecks": true, 26 | "strictPropertyInitialization": true, 27 | "stripInternal": true, 28 | "target": "ES2019" 29 | }, 30 | "include": [ 31 | "src/**/*.ts" 32 | ], 33 | "exclude": [] 34 | } 35 | --------------------------------------------------------------------------------