├── NOTICE ├── .gitignore ├── virtual-participant-orchestrator-for-zoom-meeting ├── raw-templates │ ├── lambda │ │ ├── job-api │ │ │ ├── requirements.txt │ │ │ └── lambda.py │ │ ├── poller │ │ │ ├── requirements.txt │ │ │ └── lambda.py │ │ ├── instance-api │ │ │ ├── requirements.txt │ │ │ └── lambda.py │ │ └── job-completion-handler │ │ │ ├── requirements.txt │ │ │ └── lambda.py │ ├── windows-container-pipeline-template.yaml │ └── codepipeline-custom-action-template.yaml ├── .npmignore ├── .gitignore ├── etc │ ├── join-event.json │ ├── leave-event.json │ └── run-task.json ├── jest.config.js ├── docs │ └── images │ │ ├── Zoom-Virtual-Participant.png │ │ └── Zoom-Virtual-Participant.xml ├── src │ ├── img │ │ └── join_meeting_dialog_screenshot.png │ ├── build_zoom_app.bat │ ├── build_kvs_windows.bat │ ├── kvs_msvc.patch │ ├── export_credentials.py │ ├── vcpkg_template.json │ ├── transcribe_tester_app │ │ ├── package.json │ │ ├── .gitignore │ │ ├── README.md │ │ ├── zoom-kvsWorker.js │ │ └── zoom-consumer.js │ ├── build-and-publish-docker-image.ps1 │ ├── community-patches │ │ └── 5.14.10-tos-acceptance.patch │ ├── Dockerfile │ └── README.md ├── bin │ └── zoom-meeting-bot-cdk.ts ├── test │ └── zoom-meeting-bot-cdk.test.ts ├── package.json ├── tsconfig.json ├── lib │ ├── codepipeline-custom-action-stack.ts │ ├── lambda-py37 │ │ └── fargate-client │ │ │ └── index.py │ ├── windows-container-pipeline-stack.ts │ ├── zoom-meeting-bot-cdk-pipeline-stack.ts │ ├── zoom-meeting-bot-cdk-app-stage.ts │ └── zoom-meeting-bot-cdk-app-stack.ts ├── cdk.json └── README.md ├── amazon-transcribe-lca-virtual-participant-connector-for-zoom-meeting └── README.md ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── CODE_OF_CONDUCT.md ├── README.md ├── CONTRIBUTING.md └── LICENSE /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # scan reports 2 | .scan_results 3 | 4 | # system files 5 | .DS_Store 6 | Thumbs.db -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/raw-templates/lambda/job-api/requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/raw-templates/lambda/poller/requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/raw-templates/lambda/instance-api/requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/raw-templates/lambda/job-completion-handler/requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /amazon-transcribe-lca-virtual-participant-connector-for-zoom-meeting/README.md: -------------------------------------------------------------------------------- 1 | # Amazon Transcribe Live Call Analytics Virtual Participant Connector for Zoom Meeting 2 | 3 | To be released in the future... -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/.gitignore: -------------------------------------------------------------------------------- 1 | !jest.config.js 2 | *.d.ts 3 | node_modules 4 | 5 | ### CDK-specific ignores ### 6 | *.swp 7 | yarn.lock 8 | .cdk.staging 9 | !cdk.out 10 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/etc/join-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "ip_address": "10.0.0.10", 3 | "path": "join", 4 | "meeting_id": "1111111111", 5 | "meeting_passcode": "aaaaaa", 6 | "display_name": "fargate" 7 | } -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/etc/leave-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "ip_address": "10.0.0.10", 3 | "path": "leave", 4 | "meeting_id": "1111111111", 5 | "meeting_passcode": "aaaaaa", 6 | "display_name": "fargate" 7 | } -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Expected behavior and actual behavior 2 | 3 | ANSWER: 4 | 5 | 6 | ### Steps to reproduce the problem 7 | 8 | ANSWER: 9 | 10 | ### Specifications like the version of the project, operating system, or hardware 11 | 12 | ANSWER: -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/docs/images/Zoom-Virtual-Participant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-virtual-participant-framework-for-rtc/HEAD/virtual-participant-orchestrator-for-zoom-meeting/docs/images/Zoom-Virtual-Participant.png -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/src/img/join_meeting_dialog_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-virtual-participant-framework-for-rtc/HEAD/virtual-participant-orchestrator-for-zoom-meeting/src/img/join_meeting_dialog_screenshot.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### A reference to a related issue in your repository. 2 | 3 | 4 | ANSWER: 5 | 6 | 7 | ### A description of the changes proposed in the pull request. 8 | 9 | 10 | ANSWER: 11 | 12 | ### @mentions of the person or team responsible for reviewing proposed changes. 13 | 14 | ANSWER: 15 | 16 | 17 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/src/build_zoom_app.bat: -------------------------------------------------------------------------------- 1 | if defined DOCKER_BUILD ( 2 | call "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\VC\Auxiliary\Build\vcvars64.bat" 3 | ) else ( 4 | call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat" 5 | ) 6 | cd zoom-sdk-windows-5.13.10.13355\x64\demo\sdk_demo_v2\ 7 | msbuild sdk_demo_v2.vcxproj /p:configuration=release /p:platform=x64 -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/src/build_kvs_windows.bat: -------------------------------------------------------------------------------- 1 | if defined DOCKER_BUILD ( 2 | call "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\VC\Auxiliary\Build\vcvars64.bat" 3 | ) else ( 4 | call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat" 5 | ) 6 | mkdir build 7 | cd build 8 | cmd.exe /c cmake -G "NMake Makefiles" .. 9 | cmake -G "NMake Makefiles" -DBUILD_TEST=TRUE -DBUILD_GSTREAMER_PLUGIN=TRUE .. 10 | nmake -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/src/kvs_msvc.patch: -------------------------------------------------------------------------------- 1 | diff --git a/CMakeLists.txt b/CMakeLists.txt 2 | index 60ecca1..bc93aaf 100644 3 | --- a/CMakeLists.txt 4 | +++ b/CMakeLists.txt 5 | @@ -126,7 +126,7 @@ else() 6 | endif() 7 | 8 | if (WIN32) 9 | - set(PKG_CONFIG_EXECUTABLE "C:\\gstreamer\\1.0\\x86_64\\bin\\pkg-config.exe") 10 | + set(PKG_CONFIG_EXECUTABLE "C:\\gstreamer\\1.0\\msvc_x86_64\\bin\\pkg-config.exe") 11 | endif() 12 | 13 | ############# Enable Sanitizers ############ 14 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/src/export_credentials.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import boto3 5 | from datetime import datetime, timedelta 6 | 7 | session = boto3.Session() 8 | cred = session.get_credentials() 9 | print( 10 | 'CREDENTIALS', 11 | cred.access_key, 12 | (datetime.now().replace(microsecond=0) + timedelta(hours=1)).isoformat(), 13 | cred.secret_key, 14 | cred.token 15 | ) -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/raw-templates/lambda/job-completion-handler/lambda.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import json 5 | 6 | def lambda_handler(event, context): 7 | # Log the received event 8 | print("Received event: " + json.dumps(event, indent=2)) 9 | 10 | error_details = event.get('errorDetails', {}) 11 | if error_details: 12 | raise Exception(f'Error processing a job: {error_details}') -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/bin/zoom-meeting-bot-cdk.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | // SPDX-License-Identifier: Apache-2.0 5 | 6 | import 'source-map-support/register'; 7 | import * as cdk from 'aws-cdk-lib'; 8 | import { ZoomMeetingBotCdkPipelineStack } from '../lib/zoom-meeting-bot-cdk-pipeline-stack'; 9 | 10 | const app = new cdk.App(); 11 | new ZoomMeetingBotCdkPipelineStack(app, 'ZoomMeetingBotCdkStack', { 12 | env: { 13 | account: app.node.tryGetContext("account"), 14 | region: app.node.tryGetContext("region"), 15 | } 16 | }); -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/etc/run-task.json: -------------------------------------------------------------------------------- 1 | { 2 | "cluster": "PipelineAppStage-PipelineAppStack-FargateCluster7CCD5F93-OBXn7CntOj1P", 3 | "count": 1, 4 | "enableExecuteCommand": true, 5 | "launchType": "FARGATE", 6 | "networkConfiguration": { 7 | "awsvpcConfiguration": { 8 | "subnets": [ 9 | "subnet-0565aceb20e50d718" 10 | ], 11 | "securityGroups": [ 12 | "sg-0bd963a09f2127d96" 13 | ], 14 | "assignPublicIp": "DISABLED" 15 | } 16 | }, 17 | "platformVersion": "LATEST", 18 | "taskDefinition": "ZoomMeetingBotCdkStackPipelineAppStagePipelineAppStackZoomTaskDefinition7C9DFC1C" 19 | } -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/test/zoom-meeting-bot-cdk.test.ts: -------------------------------------------------------------------------------- 1 | // import * as cdk from 'aws-cdk-lib'; 2 | // import { Template } from 'aws-cdk-lib/assertions'; 3 | // import * as ZoomMeetingBotCdk from '../lib/zoom-meeting-bot-cdk-stack'; 4 | 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/zoom-meeting-bot-cdk-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new ZoomMeetingBotCdk.ZoomMeetingBotCdkStack(app, 'MyTestStack'); 11 | // // THEN 12 | // const template = Template.fromStack(stack); 13 | 14 | // template.hasResourceProperties('AWS::SQS::Queue', { 15 | // VisibilityTimeout: 300 16 | // }); 17 | }); 18 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zoom-meeting-bot-cdk", 3 | "version": "0.1.0", 4 | "bin": { 5 | "zoom-meeting-bot-cdk": "bin/zoom-meeting-bot-cdk.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^27.5.2", 15 | "@types/node": "^20.8.0", 16 | "@types/prettier": "2.6.0", 17 | "jest": "^27.5.1", 18 | "ts-jest": "^27.1.4", 19 | "ts-node": "^10.9.1", 20 | "typescript": "~4.6.3" 21 | }, 22 | "dependencies": { 23 | "aws-cdk": "^2.99.1", 24 | "aws-cdk-lib": "^2.0.0", 25 | "constructs": "^10.0.0", 26 | "source-map-support": "^0.5.21" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/src/vcpkg_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", 3 | "name": "sdk-demo-v2", 4 | "version": "0.0.1", 5 | "builtin-baseline": "8563627bf17c6b76460a35c0e668557be2f8bbdc", 6 | "dependencies": [ 7 | { 8 | "name": "aws-sdk-cpp", 9 | "features": ["sns"] 10 | }, 11 | "crow", 12 | "cpp-jwt" 13 | ], 14 | "overrides": [ 15 | { 16 | "name": "aws-sdk-cpp", 17 | "version": "1.9.220", 18 | "port-version": 3 19 | }, 20 | { 21 | "name": "crow", 22 | "version": "1.0-5" 23 | }, 24 | { 25 | "name": "cpp-jwt", 26 | "version": "2022-08-27", 27 | "port-version": 1 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2018" 7 | ], 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "experimentalDecorators": true, 21 | "strictPropertyInitialization": false, 22 | "typeRoots": [ 23 | "./node_modules/@types" 24 | ] 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "cdk.out" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/lib/codepipeline-custom-action-stack.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | 5 | import * as cdk from 'aws-cdk-lib'; 6 | import * as cfn_inc from "aws-cdk-lib/cloudformation-include"; 7 | import { Construct } from 'constructs'; 8 | 9 | 10 | export class CodePipelineCustomActionStack extends cdk.Stack { 11 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 12 | super(scope, id, props); 13 | 14 | const cfnTemplate = new cfn_inc.CfnInclude(this, 'Template', { 15 | templateFile: 'raw-templates/codepipeline-custom-action-deployment.yaml', 16 | parameters: { 17 | "CustomActionProviderVersion": "100" 18 | } 19 | }); 20 | 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/lib/lambda-py37/fargate-client/index.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | 5 | import json 6 | from botocore.vendored import requests 7 | 8 | def lambda_handler(event, context): 9 | 10 | ip_address = event["ip_address"] 11 | path = event["path"] 12 | meeting_id = event["meeting_id"] 13 | meeting_passcode = event["meeting_passcode"] 14 | display_name = event["display_name"] 15 | 16 | url = f"http://{ip_address}:3000/{path}" 17 | 18 | payload = json.dumps( 19 | { 20 | "meeting_id": meeting_id, 21 | "meeting_passcode": meeting_passcode, 22 | "bot_display_name": display_name, 23 | } 24 | ) 25 | headers = {"Content-Type": "application/json"} 26 | 27 | response = requests.request("POST", url, headers=headers, data=payload) 28 | 29 | print(response.text) 30 | 31 | return {"statusCode": 200, "body": response.text} 32 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/src/transcribe_tester_app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-kvs-consumer-example", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "zoom-consumer.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@aws-sdk/client-kinesis": "^3.421.0", 13 | "@aws-sdk/client-kinesis-video": "^3.421.0", 14 | "@aws-sdk/client-kinesis-video-media": "^3.421.0", 15 | "@aws-sdk/client-lambda": "^3.421.0", 16 | "@aws-sdk/client-s3": "^3.421.0", 17 | "@aws-sdk/client-transcribe-streaming": "^3.421.0", 18 | "@aws-sdk/types": "^3.418.0", 19 | "alawmulaw": "^6.0.0", 20 | "aws-sdk": "^2.1467.0", 21 | "aws4": "^1.12.0", 22 | "block-stream2": "^2.1.0", 23 | "child_process": "^1.0.2", 24 | "ebml-stream": "^1.0.3", 25 | "interleave-stream": "^1.0.2", 26 | "node-fetch": "^3.3.2", 27 | "queue-fifo": "^0.2.6", 28 | "terminal-kit": "^3.0.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/lib/windows-container-pipeline-stack.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import * as cdk from 'aws-cdk-lib'; 5 | import * as cfn_inc from "aws-cdk-lib/cloudformation-include"; 6 | import { Construct } from 'constructs'; 7 | 8 | 9 | export class WindowsContainerPipelineStack extends cdk.Stack { 10 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 11 | super(scope, id, props); 12 | 13 | const cfnTemplate = new cfn_inc.CfnInclude(this, 'WindowsContainerPipelineTemplate', { 14 | templateFile: 'raw-templates/windows-container-pipeline-template.yaml', 15 | parameters: { 16 | "ECRPrivateRepoName": "zoom-virtual-participant-windows", 17 | "CustomActionProviderCategory": "Build", 18 | "CustomActionProviderName": "EC2-CodePipeline-Builder", 19 | "CustomActionProviderVersion": "100", 20 | "RepositoryArn": this.node.tryGetContext('codecommit_arn') 21 | } 22 | }); 23 | 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/src/build-and-publish-docker-image.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [string]$accountId, 3 | [string]$region, 4 | [string]$repositoryName 5 | ) 6 | 7 | trap 8 | { 9 | Write-Output $_ 10 | exit 1 11 | } 12 | 13 | # Authenticate with ECR 14 | Write-Output "Authenticate with ECR..." 15 | Invoke-Expression -Command (Get-ECRLoginCommand).Command 16 | 17 | New-Variable -Name builderRepoName -Value "$repositoryName" 18 | $builderRepoName += "-builder-stage" 19 | 20 | # Pull the images 21 | Write-Output "Pulling $builderRepoName..." 22 | (docker pull $builderRepoName) -or (1) 23 | 24 | Write-Output "Pulling $repositoryName..." 25 | (docker pull $repositoryName) -or (1) 26 | 27 | # Build images 28 | Write-Output "Building $builderRepoName..." 29 | docker build --target builder --cache-from $builderRepoName -t $builderRepoName . 30 | 31 | Write-Output "Building $repositoryName..." 32 | docker build --target runner --cache-from $builderRepoName --cache-from $repositoryName -t $repositoryName . 33 | 34 | # Build and push the 35 | Write-Output "Push $builderRepoName to ECR..." 36 | docker push $builderRepoName 37 | 38 | Write-Output "Push $repositoryName to ECR..." 39 | docker push $repositoryName 40 | 41 | return $LASTEXITCODE -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/lib/zoom-meeting-bot-cdk-pipeline-stack.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import * as cdk from 'aws-cdk-lib'; 5 | import { Construct } from 'constructs'; 6 | import { Repository } from 'aws-cdk-lib/aws-codecommit'; 7 | import { CodePipeline, CodePipelineSource, ShellStep } from 'aws-cdk-lib/pipelines'; 8 | import { ZoomMeetingBotCdkAppStage } from './zoom-meeting-bot-cdk-app-stage'; 9 | 10 | export class ZoomMeetingBotCdkPipelineStack extends cdk.Stack { 11 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 12 | super(scope, id, props); 13 | 14 | const repository = Repository.fromRepositoryArn(this, 'PipelineRepository', this.node.tryGetContext('codecommit_arn')); 15 | 16 | const pipeline = new CodePipeline(this, 'Pipeline', { 17 | pipelineName: 'CDKPipeline', 18 | dockerEnabledForSynth: true, 19 | synth: new ShellStep('Synth', { 20 | input: CodePipelineSource.codeCommit(repository, 'main'), 21 | commands: ['npm install -g npm@^9','npm ci', 'npm run build', 'npx cdk synth'] 22 | }) 23 | }); 24 | 25 | pipeline.addStage(new ZoomMeetingBotCdkAppStage(this, "PipelineAppStage", { 26 | env: { account: this.node.tryGetContext("account"), region: this.node.tryGetContext("region") } 27 | })); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/lib/zoom-meeting-bot-cdk-app-stage.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import * as cdk from 'aws-cdk-lib'; 5 | import { Construct } from "constructs"; 6 | import { ZoomMeetingBotCdkAppStack } from './zoom-meeting-bot-cdk-app-stack' 7 | import { CodePipelineCustomActionStack } from './codepipeline-custom-action-stack'; 8 | import { WindowsContainerPipelineStack } from './windows-container-pipeline-stack'; 9 | 10 | export class ZoomMeetingBotCdkAppStage extends cdk.Stage { 11 | 12 | constructor(scope: Construct, id: string, props?: cdk.StageProps) { 13 | super(scope, id, props); 14 | 15 | // from https://github.com/aws-samples/aws-codepipeline-custom-action 16 | // featured in https://aws.amazon.com/blogs/devops/building-windows-containers-with-aws-codepipeline-and-custom-actions/ 17 | const codePipelineCustomActionStack = new CodePipelineCustomActionStack(this, 'CodePipelineCustomActionStack'); 18 | const windowsContainerPipelineStack = new WindowsContainerPipelineStack(this, 'WindowsContainerPipelineStack'); 19 | 20 | windowsContainerPipelineStack.addDependency(codePipelineCustomActionStack); 21 | 22 | const appStack = new ZoomMeetingBotCdkAppStack(this, 'PipelineAppStack'); 23 | 24 | appStack.addDependency(windowsContainerPipelineStack); 25 | } 26 | } -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/zoom-meeting-bot-cdk.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 21 | "@aws-cdk/core:stackRelativeExports": true, 22 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 23 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 24 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 25 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/core:checkSecretUsage": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 31 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 32 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 33 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 34 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 35 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 36 | "@aws-cdk/core:enablePartitionLiterals": true, 37 | "@aws-cdk/core:target-partitions": [ 38 | "aws", 39 | "aws-cn" 40 | ], 41 | "account": "111111111111", 42 | "region": "us-west-2", 43 | "codecommit_arn": "arn:aws:codecommit:us-west-2:111111111111:zoom-meeting-bot-cdk", 44 | "zoomsecret_arn": "arn:aws:secretsmanager:us-west-2:111111111111:secret:zoomsecret-Ou67cL", 45 | "usersecret_arn": "arn:aws:secretsmanager:us-west-2:111111111111:secret:usersecret-xzX7U9" 46 | } 47 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Virtual Participant Framework for RTC on AWS 2 | 3 | This is a sample solutions umbrella project for building AI powered virtual participants for Real-Time Communication (RTC) services like Zoom. This repository is under development. 4 | 5 | > :warning: The Zoom Meeting sample **IS NO LONGER Functional**. As of Feb 3, 2024 the version of the Zoom Meeting Windows SDK used as a dependency is no longer compatible with Zoom Service. This means the sample app (i.e. the bot application that is created by Virtual Patricipant Framework) can no longer join a Zoom meeting as a participant. Unfortunately newer versions of the Zoom Meeting Windows SDK wrappend in a Docker container can no longer stream media to Amazon Kinesis Video Stream rendering them incompatible with the sample. After a lengthy investigation we have not been able to resolve the compatiblity issue, highlighting the fragility of this sample solution. This bring our Virtual Participant Framework sample and the experiment to End of Life. For any community support or questions please connect with the Zoom developer ecosystem team via the [Zoom Developer Forum](https://devforum.zoom.us/). 6 | 7 | ## Project Directory 8 | * [Virtual Participant Orcestrator for Zoom Meeting](virtual-participant-orchestrator-for-zoom-meeting/README.md) [Ready for dev/test] 9 | 10 | > This project builds the containerized virtual participants that connect to Zoom Meeting(s) and streams it's multimedia to Amazon Kinesis Video Stream. 11 | 12 | [virtual participant framework producer and consumder zoomtopia demo](https://user-images.githubusercontent.com/590609/215575041-1036dedb-e512-43fc-9af4-e44c680a8f8b.mov) 13 | 14 | 15 | * [Amazon Transcribe Virtual Participant Connector for Zoom Meeting](amazon-transcribe-lca-virtual-participant-connector-for-zoom-meeting/README.md) [under development] 16 | 17 | >This project is under development and allows Zoom Meeting audio data streamed to KVS to be transcribed using Amazon Transcribe. 18 | 19 | [amazon transcribe virtual participant connector for zoom meeting zoomtopia demo](https://user-images.githubusercontent.com/590609/215573136-92636fc1-2db2-449c-bd94-0a677f188d13.mov) 20 | 21 | 22 | 23 | Please reach out to contributors or your AWS account team regarding questions. 24 | 25 | ## Security 26 | 27 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 28 | 29 | ## License 30 | 31 | This project is licensed under the Apache-2.0 License. 32 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/src/transcribe_tester_app/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # Extras 133 | *.wav 134 | *.raw 135 | *.pcm 136 | *.ulaw 137 | *.mkv 138 | *.zip 139 | .DS_Store 140 | *.tmp 141 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/src/transcribe_tester_app/README.md: -------------------------------------------------------------------------------- 1 | # Amazon Transcribe sample tester 2 | 3 | ## About 4 | 5 | This small sample run as a standalone NodeJS app tests the Zoom Meeting virtual participant application functionality by streaming audio data from Amazon Kinesis Video Stream to Amazon Transcribe and logs transcription results to the console. 6 | 7 | This project is based on [call_transcriber lambda function](https://github.com/aws-samples/amazon-transcribe-live-call-analytics/tree/develop/lca-chimevc-stack/lambda_functions/call_transcriber) in AWS LCA architecture built by Chris Lott and other contributors. 8 | 9 | ## Important 10 | 11 | - As an AWS best practice, grant this code least privilege, or only the 12 | permissions required to perform a task. For more information, see 13 | [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege) 14 | in the *AWS Identity and Access Management User Guide*. 15 | - This code has not been tested in all AWS Regions. Some AWS services are 16 | available only in specific AWS Regions. For more information, see the 17 | [AWS Regional Services List](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/) 18 | on the AWS website. 19 | - Running this code might result in charges to your AWS account. 20 | 21 | ## Setup 22 | 23 | ### Prerequisites 24 | Before running the code you will need 25 | 26 | * An AWS account. To create an account, see [How do I create and activate a new AWS account](https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/) on the AWS Premium Support website. 27 | * AWS credentials. For details, see [Setting credentials in Node.js](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html) in the *AWS SDK for Javascript (v3) Developer Guide*. 28 | * IAM access to read from Kinesis Video Stream (see [Controlling Access to Kinesis Video Streams Resources Using IAM](https://docs.aws.amazon.com/kinesisvideostreams/latest/dg/how-iam.html)) and permissions to transcribe with Amazon Transcribe (see [Identity and Access Management for Amazon Transcribe](https://docs.aws.amazon.com/transcribe/latest/dg/security-iam.html)) 29 | 30 | ### Install dependencies 31 | In this directory run: 32 | 33 | ``` 34 | npm install node -g 35 | npm install 36 | ``` 37 | 38 | ### Running the code 39 | To run execute: `node zoom-consumer.js ` 40 | 41 | e.g. 42 | 43 | ``` 44 | node zoom-consumer.js 1111222233 zoom-meeting-1111222233-example-stream 45 | ``` 46 | > Note you can look up the Amazon Kinesis Stream *stream-name* above from AWS Console or using the AWS CLI. The stream-name prefix is hardcoded as "zoom-meeting-". This is followed by the 10 digit and ending with a suffix that was passed as an environment variable to the Zoom Meeting Windows SDK application `KVS_STREAM_SUFFIX` (see [README](../README.md#set-environment-variables)). 47 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/raw-templates/lambda/job-api/lambda.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import os 5 | import json 6 | import boto3 7 | 8 | # env variables 9 | SSM_DOCUMENT_NAME = os.environ['SSM_DOCUMENT_NAME'] 10 | 11 | print('Loading function') 12 | ssm = boto3.client('ssm') 13 | 14 | COMMAND_RUN = 'run' 15 | COMMAND_STATUS = 'status' 16 | 17 | STATUS_FAILED = 'FAILED' 18 | STATUS_SUCCESS = 'SUCCESS' 19 | STATUS_IN_PROGRESS = 'IN PROGRESS' 20 | 21 | 22 | def lambda_handler(event, context): 23 | # Log the received event 24 | print("Received event: " + json.dumps(event, indent=2)) 25 | 26 | try: 27 | # Get parameters from the event 28 | command = event['command'] 29 | 30 | if command == COMMAND_RUN: 31 | return run_command(event) 32 | elif command == COMMAND_STATUS: 33 | return check_command_status(event) 34 | else: 35 | raise Exception('Unknown command') 36 | 37 | except Exception as e: 38 | print(e) 39 | raise Exception('Error processing a job') 40 | 41 | 42 | def run_command(event): 43 | # Get parameters from the event 44 | instance_id = event['instanceId'] 45 | command_text = event['commandText'] 46 | command_timeout = event['timeout'] 47 | command_working_directory = event['workingDirectory'] 48 | input_bucket_name = event['inputBucketName'] 49 | input_object_key = event['inputObjectKey'] 50 | 51 | output_artifact_path = event['outputArtifactPath'] 52 | output_bucket_name = event['outputBucketName'] 53 | output_object_key = event['outputObjectKey'] 54 | 55 | pipeline_execution_id = event['executionId'] 56 | pipeline_arn = event['pipelineArn'] 57 | pipeline_name = event['pipelineName'] 58 | 59 | # Send command to the builder instance 60 | response = ssm.send_command( 61 | InstanceIds=[instance_id], 62 | DocumentName=SSM_DOCUMENT_NAME, 63 | Parameters={ 64 | 'inputBucketName': [input_bucket_name], 65 | 'inputObjectKey': [input_object_key], 66 | 'commands': [command_text], 67 | 'executionTimeout': [str(command_timeout)], 68 | 'workingDirectory': [command_working_directory], 69 | 'outputArtifactPath': [output_artifact_path], 70 | 'outputBucketName': [output_bucket_name], 71 | 'outputObjectKey': [output_object_key], 72 | 'executionId': [pipeline_execution_id], 73 | 'pipelineArn': [pipeline_arn], 74 | 'pipelineName': [pipeline_name] 75 | }, 76 | CloudWatchOutputConfig={ 77 | 'CloudWatchOutputEnabled': True 78 | } 79 | ) 80 | 81 | # extract command ID 82 | command_id = response.get('Command', {}).get('CommandId', '') 83 | 84 | return { 85 | 'commandId': command_id, 86 | 'status': STATUS_IN_PROGRESS 87 | } 88 | 89 | 90 | def check_command_status(event): 91 | # Get parameters from the event 92 | command_id = event['commandId'] 93 | instance_id = event['instanceId'] 94 | 95 | response = ssm.list_commands( 96 | CommandId=command_id, 97 | InstanceId=instance_id 98 | ) 99 | 100 | commands = response.get('Commands', {}) 101 | if commands: 102 | command = commands[0] 103 | aws_status = command['Status'] 104 | 105 | status = STATUS_FAILED 106 | if aws_status in ['Pending', 'InProgress']: 107 | status = STATUS_IN_PROGRESS 108 | elif aws_status in ['Success']: 109 | status = STATUS_SUCCESS 110 | 111 | return { 112 | 'commandId': command_id, 113 | 'status': status 114 | } 115 | 116 | raise Exception('Command is not found.') 117 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/src/community-patches/5.14.10-tos-acceptance.patch: -------------------------------------------------------------------------------- 1 | diff --git a/x64/demo/sdk_demo_v2/LOGIN_join_meeting_only_workflow.cpp b/x64/demo/sdk_demo_v2/LOGIN_join_meeting_only_workflow.cpp 2 | index 06a46c1..aaeed53 100644 3 | --- a/x64/demo/sdk_demo_v2/LOGIN_join_meeting_only_workflow.cpp 4 | +++ b/x64/demo/sdk_demo_v2/LOGIN_join_meeting_only_workflow.cpp 5 | @@ -6,6 +6,8 @@ CSDKWithoutLoginStartJoinMeetingFlow::CSDKWithoutLoginStartJoinMeetingFlow() 6 | m_pCSDKJoinMeetingOnlyFlowUIEvent = NULL; 7 | m_pAuthService = NULL; 8 | m_pMeetingService = NULL; 9 | + 10 | + m_reminder_event = new ReminderEvent(); 11 | } 12 | 13 | CSDKWithoutLoginStartJoinMeetingFlow::~CSDKWithoutLoginStartJoinMeetingFlow() 14 | @@ -13,6 +15,8 @@ CSDKWithoutLoginStartJoinMeetingFlow::~CSDKWithoutLoginStartJoinMeetingFlow() 15 | m_pAuthService = NULL; 16 | m_pMeetingService = NULL; 17 | SDKInterfaceWrap::GetInst().UnListenMeetingServiceEvent(this); 18 | + 19 | + delete m_reminder_event; 20 | } 21 | 22 | void CSDKWithoutLoginStartJoinMeetingFlow::Cleanup() 23 | @@ -63,7 +67,12 @@ ZOOM_SDK_NAMESPACE::SDKError CSDKWithoutLoginStartJoinMeetingFlow::JoinMeeting(Z 24 | pAudioContext->EnableAutoJoinAudio(true); 25 | } 26 | } 27 | - ZOOM_SDK_NAMESPACE::SDKError err = ZOOM_SDK_NAMESPACE::SDKERR_SUCCESS; 28 | + 29 | + const auto controller = m_pMeetingService->GetMeetingReminderController(); 30 | + ZOOM_SDK_NAMESPACE::SDKError err = controller->SetEvent(dynamic_cast(m_reminder_event)); 31 | + 32 | + if (err != SDKERR_SUCCESS) return err; 33 | + 34 | err = m_pMeetingService->Join(paramJoinMeeting); 35 | return err; 36 | } 37 | diff --git a/x64/demo/sdk_demo_v2/LOGIN_join_meeting_only_workflow.h b/x64/demo/sdk_demo_v2/LOGIN_join_meeting_only_workflow.h 38 | index 5365519..f41ce9c 100644 39 | --- a/x64/demo/sdk_demo_v2/LOGIN_join_meeting_only_workflow.h 40 | +++ b/x64/demo/sdk_demo_v2/LOGIN_join_meeting_only_workflow.h 41 | @@ -4,6 +4,9 @@ 42 | #include "resource.h" 43 | #include "sdk_demo_app_common.h" 44 | //#include "LOGIN_login_with_email_workflow.h" 45 | +#include 46 | +#include "ReminderEvent.h" 47 | + 48 | class CSDKJoinMeetingOnlyFlowUIEvent 49 | { 50 | public: 51 | @@ -39,4 +42,6 @@ private: 52 | CSDKJoinMeetingOnlyFlowUIEvent *m_pCSDKJoinMeetingOnlyFlowUIEvent; 53 | ZOOM_SDK_NAMESPACE::IAuthService *m_pAuthService; 54 | ZOOM_SDK_NAMESPACE::IMeetingService *m_pMeetingService; 55 | + 56 | + ReminderEvent *m_reminder_event; 57 | }; 58 | \ No newline at end of file 59 | diff --git a/x64/demo/sdk_demo_v2/ReminderEvent.cpp b/x64/demo/sdk_demo_v2/ReminderEvent.cpp 60 | new file mode 100644 61 | index 0000000..54d1380 62 | --- /dev/null 63 | +++ b/x64/demo/sdk_demo_v2/ReminderEvent.cpp 64 | @@ -0,0 +1,9 @@ 65 | +#include "stdafx.h" 66 | +#include "ReminderEvent.h" 67 | +#include "sdk_util.h" 68 | + 69 | +void ReminderEvent::onReminderNotify(IMeetingReminderContent* content, IMeetingReminderHandler* handle) 70 | +{ 71 | + const bool is_tos = content->GetType() == TYPE_TERMS_OF_SERVICE; 72 | + if (is_tos) handle->Accept(); 73 | +} 74 | diff --git a/x64/demo/sdk_demo_v2/ReminderEvent.h b/x64/demo/sdk_demo_v2/ReminderEvent.h 75 | new file mode 100644 76 | index 0000000..903b700 77 | --- /dev/null 78 | +++ b/x64/demo/sdk_demo_v2/ReminderEvent.h 79 | @@ -0,0 +1,11 @@ 80 | +#pragma once 81 | +#include 82 | + 83 | +using namespace ZOOMSDK; 84 | + 85 | +class ReminderEvent : public IMeetingReminderEvent 86 | +{ 87 | +public: 88 | + void onReminderNotify(IMeetingReminderContent * content, IMeetingReminderHandler * handle) override; 89 | +}; 90 | + 91 | diff --git a/x64/demo/sdk_demo_v2/sdk_demo_v2.vcxproj b/x64/demo/sdk_demo_v2/sdk_demo_v2.vcxproj 92 | index 9e00fae..8ff4d11 100644 93 | --- a/x64/demo/sdk_demo_v2/sdk_demo_v2.vcxproj 94 | +++ b/x64/demo/sdk_demo_v2/sdk_demo_v2.vcxproj 95 | @@ -230,6 +230,7 @@ 96 | 97 | 98 | 99 | + 100 | 101 | 102 | 103 | @@ -299,6 +300,7 @@ 104 | 105 | 106 | 107 | + 108 | 109 | 110 | 111 | diff --git a/x64/demo/sdk_demo_v2/sdk_demo_v2.vcxproj.filters b/x64/demo/sdk_demo_v2/sdk_demo_v2.vcxproj.filters 112 | index 82df2db..86d5945 100644 113 | --- a/x64/demo/sdk_demo_v2/sdk_demo_v2.vcxproj.filters 114 | +++ b/x64/demo/sdk_demo_v2/sdk_demo_v2.vcxproj.filters 115 | @@ -324,6 +324,9 @@ 116 | 117 | Source Files 118 | 119 | + 120 | + Source Files 121 | + 122 | 123 | 124 | 125 | @@ -521,6 +524,9 @@ 126 | 127 | Source Files 128 | 129 | + 130 | + Source Files 131 | + 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/src/transcribe_tester_app/zoom-kvsWorker.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const { workerData, parentPort } = require('worker_threads') 5 | const process = require('process'); 6 | const { EbmlStreamDecoder, EbmlTagId, EbmlTag, EbmlTagPosition } = require('ebml-stream'); 7 | const { KinesisVideoClient, GetDataEndpointCommand, GetDataEndpointInput } = require("@aws-sdk/client-kinesis-video"); 8 | const { KinesisVideoMediaClient, KinesisVideoMedia, GetMediaCommand, GetMediaInput } = require("@aws-sdk/client-kinesis-video-media"); 9 | const alawmulaw = require('alawmulaw'); 10 | const util = require('util'); 11 | 12 | 13 | console.log('inside',workerData.streamName,'worker'); 14 | 15 | let timeToStop = false; 16 | 17 | parentPort.on('message', (message) => { 18 | console.log('received message from parent', message); 19 | timeToStop = true; 20 | }); 21 | 22 | // Start the stream for this particular stream 23 | const readKVS = async function(region, streamName, lastFragment) { 24 | let actuallyStop = false; 25 | let firstDecodeEbml = true; 26 | console.log('inside readKVS worker', region, streamName); 27 | const decoder = new EbmlStreamDecoder({ 28 | bufferTagIds: [ 29 | EbmlTagId.SimpleTag, 30 | EbmlTagId.SimpleBlock 31 | ] 32 | }); 33 | decoder.on('error', error => { 34 | console.log('Decoder Error:', JSON.stringify(error)); 35 | }) 36 | decoder.on('data', chunk => { 37 | 38 | if(chunk.id === EbmlTagId.Segment && chunk.position === EbmlTagPosition.End) { 39 | //this is the end of a segment. Lets forcefully stop if needed. 40 | if(timeToStop) actuallyStop = true; 41 | } 42 | if(!timeToStop) { 43 | if(chunk.id === EbmlTagId.SimpleTag) 44 | { 45 | if (chunk.Children[0].data === 'AWS_KINESISVIDEO_FRAGMENT_NUMBER') { 46 | lastFragment = chunk.Children[1].data; 47 | //console.log("read fragment", lastFragment); 48 | parentPort.postMessage({ type: 'new_fragment', fragment: lastFragment }); 49 | //console.log("fragment: " + new Date(Date.now()).toString()); 50 | } 51 | if (chunk.Children[0].data === 'speaker') { 52 | 53 | let speaker = chunk.Children[1].data; 54 | // console.log("speaker: " + speaker); 55 | parentPort.postMessage({ type: 'speaker_change', speaker: speaker }); 56 | } 57 | } 58 | if(chunk.id === EbmlTagId.SimpleBlock) 59 | { 60 | if(firstDecodeEbml) { 61 | firstDecodeEbml = false; 62 | console.log('decoded ebml, simpleblock size:' + chunk.size + ' stream: ' + streamName); 63 | } 64 | try { 65 | //TODO: Watch out when video is included need to check chunk.track to grab audio only 66 | // console.log("chunk: "); 67 | // console.log(util.inspect(chunk)); 68 | //Todo: track 1 should not be auido but that needs to be fixed on producer side 69 | if(chunk.track == 1){ 70 | let pcmSamples = Buffer.from(alawmulaw.mulaw.decode(chunk.payload).buffer); 71 | parentPort.postMessage({ type: 'chunk', chunk: pcmSamples }) 72 | } 73 | } catch(error) { 74 | console.error("Error piping to parentPort: " + JSON.stringify(error)); 75 | } 76 | } 77 | } 78 | }); //use this to find last fragment tag we received 79 | decoder.on('end', () => { 80 | //close stdio 81 | console.log(streamName, 'Finished'); 82 | 83 | console.log('Last fragment for ' + streamName + ' ' + lastFragment + ' total size: ' + totalSize); 84 | parentPort.postMessage({type:'lastFragment', streamName:streamName, lastFragment: lastFragment}); 85 | process.exit(); 86 | }); 87 | console.log('Starting stream ' + streamName); 88 | const kvClient = new KinesisVideoClient({ region: region}); 89 | const getDataCmd = new GetDataEndpointCommand({ APIName: 'GET_MEDIA', StreamName: streamName}); 90 | const response = await kvClient.send(getDataCmd); 91 | console.log("received endpoint for stream:", response.DataEndpoint); 92 | const mediaClient = new KinesisVideoMedia({region:region, endpoint:response.DataEndpoint}); 93 | //var getMediaCmd = new GetMediaCommand({ StreamName:streamName, StartSelector: { StartSelectorType: 'NOW'}}); 94 | let fragmentSelector = { StreamName: streamName, StartSelector: { StartSelectorType: 'NOW' } }; 95 | console.log("Selector:" + util.inspect(fragmentSelector)); 96 | if (lastFragment && lastFragment.length > 0) { 97 | console.log('resumging after last fragment', lastFragment); 98 | fragmentSelector = { 99 | StreamName: streamName, 100 | StartSelector: { 101 | StartSelectorType: 'FRAGMENT_NUMBER', 102 | AfterFragmentNumber: lastFragment 103 | } 104 | } 105 | } 106 | console.log("Fragment selector: " + JSON.stringify(fragmentSelector)); 107 | const result = await mediaClient.getMedia(fragmentSelector) 108 | const streamReader = result.Payload; 109 | let totalSize = 0; 110 | let firstKvsChunk = true; 111 | try { 112 | for await (const chunk of streamReader) { 113 | if(firstKvsChunk) { 114 | firstKvsChunk = false; 115 | console.log(streamName + " received chunk size: " + chunk.length); 116 | } 117 | totalSize = totalSize + chunk.length; 118 | decoder.write(chunk); 119 | if (actuallyStop) { break; } 120 | } 121 | console.log("done reading"); 122 | } 123 | catch(error) { 124 | console.error("error writing to decoder", error); 125 | } finally { 126 | console.log('Closing buffers ' + streamName); 127 | decoder.end(); 128 | } 129 | } 130 | 131 | console.log("Stream Name: " + workerData.streamName); 132 | 133 | readKVS(workerData.region, workerData.streamName, workerData.lastFragment); -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/raw-templates/lambda/instance-api/lambda.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import json 5 | import boto3 6 | import os 7 | 8 | # env variables 9 | BUILDER_INSTANCE_PROFILE_ARN = os.environ["BUILDER_INSTANCE_PROFILE_ARN"] 10 | 11 | print("Loading function") 12 | ec2 = boto3.client("ec2") 13 | ssm = boto3.client("ssm") 14 | 15 | COMMAND_START = "start" 16 | COMMAND_STOP = "stop" 17 | COMMAND_STATUS_EC2 = "status_ec2" 18 | COMMAND_STATUS_SSM = "status_ssm" 19 | 20 | STATUS_STOPPED = "STOPPED" 21 | STATUS_STARTED = "STARTED" 22 | STATUS_IN_PROGRESS = "IN PROGRESS" 23 | 24 | 25 | def lambda_handler(event, context): 26 | # Log the received event 27 | print("Received event: " + json.dumps(event, indent=2)) 28 | 29 | # Get parameters from the event 30 | command = event["command"] 31 | 32 | try: 33 | if command == COMMAND_START: 34 | return start_instance(event) 35 | elif command == COMMAND_STOP: 36 | return stop_instance(event) 37 | elif command == COMMAND_STATUS_EC2: 38 | return check_instance_status_ec2(event) 39 | elif command == COMMAND_STATUS_SSM: 40 | return check_instance_status_ssm(event) 41 | else: 42 | raise Exception("Unknown command") 43 | 44 | except Exception as e: 45 | print(e) 46 | message = "Error submitting Batch Job" 47 | print(message) 48 | raise Exception(message) 49 | 50 | 51 | def get_instance_status(state_name): 52 | if state_name in ["running"]: 53 | return STATUS_STARTED 54 | elif state_name in ["terminated", "stopped"]: 55 | return STATUS_STOPPED 56 | else: 57 | return STATUS_IN_PROGRESS 58 | 59 | 60 | def get_detailed_instance_status(status): 61 | if status in ["passed"]: 62 | return STATUS_STARTED 63 | elif status in ["failed"]: 64 | return STATUS_STOPPED 65 | elif status in ["insufficient-data", "initializing"]: 66 | return STATUS_IN_PROGRESS 67 | else: 68 | raise Exception("Unsupported detailed instance status: " + status) 69 | 70 | 71 | def start_instance(event): 72 | # Get parameters from event 73 | image_id = event.get("imageId", "") 74 | instance_type = event.get("instanceType", "") 75 | key_name = event.get("keyName", "") 76 | 77 | # TODO: validate parameters 78 | 79 | # Send command to the builder instance 80 | response = ec2.run_instances( 81 | ImageId=image_id, 82 | MinCount=1, 83 | MaxCount=1, 84 | BlockDeviceMappings=[ 85 | { 86 | "DeviceName": "/dev/sda1", 87 | "VirtualName": "ephemeral0", 88 | "Ebs": { 89 | "DeleteOnTermination": True, 90 | "VolumeSize": 100, 91 | "VolumeType": "gp3", 92 | "Encrypted": True, 93 | }, 94 | } 95 | ], 96 | InstanceType=instance_type, 97 | # KeyName = key_name, 98 | IamInstanceProfile={"Arn": BUILDER_INSTANCE_PROFILE_ARN}, 99 | TagSpecifications=[ 100 | { 101 | "ResourceType": "instance", 102 | "Tags": [{"Key": "AmiBuilder", "Value": "True"}], 103 | } 104 | ], 105 | ) 106 | 107 | # Log response 108 | # print("Response: " + json.dumps(instances, indent=2)) 109 | 110 | instances = response.get("Instances", []) 111 | if not instances: 112 | raise Exception("Instance creation error.") 113 | 114 | instance = instances[0] 115 | instance_id = instance["InstanceId"] 116 | 117 | return {"instanceId": instance_id, "status": STATUS_IN_PROGRESS} 118 | 119 | 120 | def stop_instance(event): 121 | # Get parameters from event 122 | instance_id = event["instanceId"] 123 | 124 | # Send command to the builder instance 125 | response = ec2.terminate_instances(InstanceIds=[instance_id]) 126 | 127 | # Log response 128 | # print("Response: " + json.dumps(response, indent=2)) 129 | 130 | instances = response.get("TerminatingInstances", []) 131 | if not instances: 132 | raise Exception("Instance termination error.") 133 | 134 | state_name = instances[0].get("CurrentState", {}).get("Name", "") 135 | return {"instanceId": instance_id, "status": get_instance_status(state_name)} 136 | 137 | 138 | def check_instance_status_ssm(event): 139 | # Get parameters from event 140 | instance_id = event["instanceId"] 141 | 142 | # Send command to the builder instance 143 | response = ssm.describe_instance_information( 144 | Filters=[{"Key": "InstanceIds", "Values": [instance_id]}] 145 | ) 146 | 147 | status = STATUS_IN_PROGRESS 148 | instance_list = response.get("InstanceInformationList", []) 149 | if instance_list: 150 | status = STATUS_STARTED 151 | 152 | return {"instanceId": instance_id, "status": status} 153 | 154 | 155 | def check_instance_status_ec2(event): 156 | # Get parameters from event 157 | instance_id = event["instanceId"] 158 | 159 | # Send command to the builder instance 160 | response = ec2.describe_instances(InstanceIds=[instance_id]) 161 | 162 | # Log response 163 | # print("Response: " + json.dumps(response, indent=2)) 164 | 165 | reservations = response.get("Reservations", []) 166 | if not reservations: 167 | raise Exception("Describe instances error.") 168 | 169 | reservation = reservations[0] 170 | instances = reservation.get("Instances", []) 171 | if not instances: 172 | raise Exception("No instance found with provided id") 173 | 174 | state_name = instances[0].get("State", {}).get("Name", "") 175 | status = get_instance_status(state_name) 176 | 177 | if status == STATUS_STARTED: 178 | 179 | # we have to wait until all status checks are passed 180 | response = ec2.describe_instance_status(InstanceIds=[instance_id]) 181 | 182 | instance_statuses = response.get("InstanceStatuses", []) 183 | if not instance_statuses: 184 | raise Exception("Describe instance status error.") 185 | 186 | details = instance_statuses[0].get("InstanceStatus", {}).get("Details", []) 187 | if not details: 188 | raise Exception("Describe instance status error (missing details).") 189 | 190 | detailed_status = details[0].get("Status", "") 191 | status = get_detailed_instance_status(detailed_status) 192 | 193 | return {"instanceId": instance_id, "status": status} 194 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/src/Dockerfile: -------------------------------------------------------------------------------- 1 | # escape=` 2 | 3 | # Use the latest Windows Server Core 2019 image. 4 | FROM mcr.microsoft.com/windows/servercore:ltsc2019 AS base 5 | 6 | ENV DOCKER_BUILD=true 7 | 8 | # Restore the default Windows shell for correct batch processing. 9 | SHELL ["cmd", "/S", "/C"] 10 | 11 | # ================= Install Chocolately ==================================================================== 12 | ENV chocolateyVersion=1.4.0 13 | RUN @powershell iex ((new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1')) 14 | 15 | # ================= Install Git ============================================================================ 16 | RUN choco install git.install -y 17 | RUN git config --system core.longpaths true 18 | 19 | # ================= Install Strawberry Perl ================================================================ 20 | RUN choco install nasm strawberryperl python -y 21 | 22 | # ================= Install Gstreamer ====================================================================== 23 | RUN curl -SL --output gstreamer-1.0-msvc-x86_64-1.20.3.msi https://gstreamer.freedesktop.org/data/pkg/windows/1.20.3/msvc/gstreamer-1.0-msvc-x86_64-1.20.3.msi 24 | RUN curl -SL --output gstreamer-1.0-devel-msvc-x86_64-1.20.3.msi https://gstreamer.freedesktop.org/data/pkg/windows/1.20.3/msvc/gstreamer-1.0-devel-msvc-x86_64-1.20.3.msi 25 | RUN msiexec /i gstreamer-1.0-msvc-x86_64-1.20.3.msi ADDLOCAL=ALL /qb 26 | RUN msiexec /i gstreamer-1.0-devel-msvc-x86_64-1.20.3.msi ADDLOCAL=ALL /qb 27 | RUN setx /M GSTREAMER_ROOT_MSVC_X86_64 "C:\gstreamer\1.0\msvc_x86_64" 28 | RUN setx GST_PLUGIN_SYSTEM_PATH "C:\gstreamer\1.0\msvc_x86_64\lib\gstreamer-1.0" 29 | RUN setx /M PATH "%PATH%;%GSTREAMER_1_0_ROOT_MSVC_X86_64%\bin" 30 | 31 | # ================= Install Python dependencies ============================================================ 32 | RUN pip install boto3 33 | 34 | # ================= Install AWS CLI V2 ===================================================================== 35 | RUN start /wait msiexec.exe /i https://awscli.amazonaws.com/AWSCLIV2.msi /quiet /qn /norestart 36 | RUN setx /M PATH "%PATH%;C:\Program Files\Amazon\AWSCLIV2" 37 | 38 | # Use the latest Windows Server Core 2019 image. 39 | FROM base AS builder 40 | 41 | RUN ` 42 | # Download the Visual Studio 2019 Build Tools bootstrapper. 43 | curl -SL --output vs_buildtools.exe https://aka.ms/vs/16/release/vs_buildtools.exe ` 44 | ` 45 | # Install Build Tools 46 | && (start /w vs_buildtools.exe --quiet --wait --norestart --nocache ` 47 | --installPath "%ProgramFiles(x86)%\Microsoft Visual Studio\2019\BuildTools" ` 48 | --add Microsoft.VisualStudio.Workload.NativeDesktop ` 49 | --add Microsoft.VisualStudio.Component.TestTools.BuildTools ` 50 | --add Microsoft.VisualStudio.Component.VC.CMake.Project ` 51 | --add Microsoft.VisualStudio.Component.VC.Tools.x86.x64 ` 52 | --add Microsoft.VisualStudio.Component.VC.ASAN ` 53 | --add Microsoft.VisualStudio.Component.Windows10SDK.18362 ` 54 | --add Microsoft.VisualStudio.Component.VC.ATLMFC ` 55 | || IF "%ERRORLEVEL%"=="3010" EXIT 0) ` 56 | ` 57 | # Cleanup 58 | && del /q vs_buildtools.exe 59 | 60 | # ===== Git Checkout latest Kinesis Video Streams Producer SDK (CPP) ======================================= 61 | WORKDIR /opt/ 62 | COPY kvs_msvc.patch . 63 | RUN git clone --depth 1 --branch v3.3.1 https://github.com/awslabs/amazon-kinesis-video-streams-producer-sdk-cpp.git 64 | WORKDIR /opt/amazon-kinesis-video-streams-producer-sdk-cpp/ 65 | RUN git apply ../kvs_msvc.patch 66 | 67 | # ===== Build Kinesis Video Streams C++ Producer and GStreamer element ===================================== 68 | COPY build_kvs_windows.bat . 69 | RUN call build_kvs_windows.bat 70 | 71 | # ===== Set required environment variables ================================================================= 72 | RUN setx GST_PLUGIN_SYSTEM_PATH "%GST_PLUGIN_SYSTEM_PATH%;C:\opt\amazon-kinesis-video-streams-producer-sdk-cpp\build" 73 | RUN setx /M PATH "%PATH%;C:\opt\amazon-kinesis-video-streams-producer-sdk-cpp\open-source\local\bin;C:\opt\amazon-kinesis-video-streams-producer-sdk-cpp\open-source\local\lib" 74 | 75 | # ===== Install vcpkg ====================================================================================== 76 | WORKDIR /opt/ 77 | RUN git clone --depth 1 --branch 2023.02.24 https://github.com/Microsoft/vcpkg.git 78 | RUN call .\vcpkg\bootstrap-vcpkg.bat 79 | 80 | # ===== Install vcpkg packages ============================================================================= 81 | RUN vcpkg\vcpkg install "crow" --triplet x64-windows 82 | RUN vcpkg\vcpkg install "cpp-jwt" --triplet x64-windows 83 | RUN vcpkg\vcpkg install "aws-sdk-cpp[sns]" --recurse --triplet x64-windows 84 | RUN vcpkg\vcpkg integrate install 85 | 86 | # ===== Copy Windows Zoom SDK code ========================================================================= 87 | COPY zoom-sdk-windows-5.13.10.13355 /opt/zoom-sdk-windows-5.13.10.13355 88 | 89 | # ===== Build Windows Zoom SDK code ======================================================================== 90 | WORKDIR /opt/ 91 | COPY build_zoom_app.bat . 92 | RUN call build_zoom_app.bat 93 | 94 | # Use the latest Windows Server Core 2019 image. 95 | FROM base AS runner 96 | 97 | RUN ` 98 | # Download the Visual Studio 2019 Build Tools bootstrapper. 99 | curl -SL --output vs_buildtools.exe https://aka.ms/vs/16/release/vs_buildtools.exe ` 100 | ` 101 | # Install Build Tools 102 | && (start /w vs_buildtools.exe --quiet --wait --norestart --nocache ` 103 | --installPath "%ProgramFiles(x86)%\Microsoft Visual Studio\2019\BuildTools" ` 104 | --add Microsoft.VisualStudio.Component.VC.Tools.x86.x64 ` 105 | --add Microsoft.VisualStudio.Component.Windows10SDK.18362 ` 106 | || IF "%ERRORLEVEL%"=="3010" EXIT 0) ` 107 | ` 108 | # Cleanup 109 | && del /q vs_buildtools.exe 110 | 111 | COPY --from=builder C:\opt\zoom-sdk-windows-5.13.10.13355\x64\bin /opt/bin 112 | COPY --from=builder C:\opt\amazon-kinesis-video-streams-producer-sdk-cpp /opt/amazon-kinesis-video-streams-producer-sdk-cpp 113 | 114 | # ===== Set required environment variables ================================================================= 115 | RUN setx GST_PLUGIN_SYSTEM_PATH "%GST_PLUGIN_SYSTEM_PATH%;C:\opt\amazon-kinesis-video-streams-producer-sdk-cpp\build" 116 | RUN setx /M PATH "%PATH%;C:\opt\amazon-kinesis-video-streams-producer-sdk-cpp\open-source\local\bin;C:\opt\amazon-kinesis-video-streams-producer-sdk-cpp\open-source\local\lib" 117 | 118 | HEALTHCHECK --interval=5s --timeout=60s CMD powershell -command ` 119 | try { ` 120 | $response = Invoke-WebRequest http://localhost:3000/health -UseBasicParsing;` 121 | if ($response.StatusCode -le 500) {return 0} ` 122 | else {return 1}; ` 123 | } catch {return 1} 124 | 125 | WORKDIR /opt/bin -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/src/transcribe_tester_app/zoom-consumer.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const { TranscribeStreamingClient, StartStreamTranscriptionCommand, StartStreamTranscriptionCommandInput } = require("@aws-sdk/client-transcribe-streaming"); 5 | 6 | const { Worker } = require('worker_threads') 7 | const fs = require('fs'); 8 | const stream = require('stream'); 9 | const util = require('util'); 10 | const magic_term = require('terminal-kit').terminal; 11 | 12 | const REGION = process.env.AWS_DEFAULT_REGION || 'us-east-1'; 13 | const TEMP_FILE_PATH = process.env.TEMP_FILE_PATH || './raw_recordings/'; 14 | const IS_CONTENT_REDACTION_ENABLED = (process.env.IS_CONTENT_REDACTION_ENABLED || "true") === "true"; 15 | const TRANSCRIBE_LANGUAGE_CODE = process.env.TRANSCRIBE_LANGUAGE_CODE || 'en-US'; 16 | const CONTENT_REDACTION_TYPE = process.env.CONTENT_REDACTION_TYPE || 'PII'; 17 | const PII_ENTITY_TYPES = process.env.PII_ENTITY_TYPES || 'ALL'; 18 | const CUSTOM_VOCABULARY_NAME = process.env.CUSTOM_VOCABULARY_NAME || ''; 19 | const TIMEOUT = 1000000; 20 | 21 | let currentSpeaker = ''; 22 | let currentTranscript = ''; 23 | let currentFragment = ''; 24 | 25 | const updateUI = function updateUI(writeAsNewLine) { 26 | message = currentFragment + " " + currentSpeaker + ": " + currentTranscript; 27 | if(writeAsNewLine){ 28 | magic_term('\n' + message ); 29 | }else{ 30 | magic_term(message); 31 | } 32 | } 33 | 34 | //returns a promise, so we can await it 35 | const runKVSWorker = function (workerData, streamPipe) { 36 | let newWorker = undefined; 37 | 38 | let workerPromise = new Promise((resolve, reject) => { 39 | newWorker = new Worker('./zoom-kvsWorker.js', { workerData }); 40 | newWorker.on('message', (message) => { 41 | if(message.type === 'chunk') { 42 | //console.log('writing chunk to ffmpeg'); 43 | try { 44 | streamPipe.write(message.chunk); 45 | // console.log(util.inspect(message.chunk)); 46 | } catch(error) { 47 | console.log('error writing to pipe', error); 48 | } 49 | } 50 | if (message.type === 'speaker_change') { 51 | currentSpeaker = message.speaker; 52 | // updateUI(true); 53 | } 54 | if (message.type === 'new_fragment') { 55 | currentFragment = message.fragment; 56 | console.log("=== new Fragment === " + currentFragment + " " + currentSpeaker + ": " + currentTranscript); 57 | //updateUI(true); 58 | } 59 | if(message.type === 'lastFragment') { 60 | console.log('last fragment:', message.streamName, message.lastFragment); 61 | resolve(message.lastFragment); 62 | } 63 | }); 64 | newWorker.on('error', reject); 65 | newWorker.on('exit', (code) => { 66 | if (code !== 0) 67 | reject(new Error(`Worker stopped with exit code ${code}`)); 68 | }); 69 | }); 70 | workerPromise.worker = newWorker; 71 | return workerPromise; 72 | } 73 | 74 | const readTranscripts = async function (tsStream, meetingId) { 75 | try { 76 | for await (const chunk of tsStream) { 77 | // console.log(JSON.stringify(chunk)); 78 | const result = chunk.TranscriptEvent.Transcript.Results[0]; 79 | if (!result) { 80 | currentTranscript = ''; 81 | continue; 82 | } 83 | const transcript = result.Alternatives[0]; 84 | if (!transcript.Transcript) continue; 85 | //console.log(currentSpeaker + ": " + transcript.Transcript); 86 | currentTranscript = transcript.Transcript; 87 | updateUI(true); 88 | //console.log(JSON.stringify(chunk)); 89 | //writeTranscriptionSegment(chunk, meetingId); 90 | } 91 | } 92 | catch(error) { 93 | console.error("Error writing transcription segment.", JSON.stringify(error)); 94 | } finally { 95 | // Do nothing for now 96 | } 97 | } 98 | 99 | const go = async function (meetingId, streamName, lastFragment) { 100 | 101 | let firstChunkToTranscribe = true; 102 | const passthroughStream = new stream.PassThrough({ highWaterMark: 128 }); 103 | const audioStream = async function* () { 104 | try { 105 | for await (const payloadChunk of passthroughStream) { 106 | if(firstChunkToTranscribe) { 107 | firstChunkToTranscribe = false; 108 | console.log('Sending first chunk to transcribe: ', payloadChunk.length); 109 | } 110 | yield { AudioEvent: { AudioChunk: payloadChunk } }; 111 | } 112 | } catch(error) { 113 | console.log("Error reading passthrough stream or yielding audio chunk."); 114 | } 115 | }; 116 | 117 | /* good for debugging - this writes the raw audio 118 | let dateISONow = new Date().toISOString().substring(0, 16).replace(/:/, ''); 119 | let tempRecordingFilename = meetingId + '-'+ dateISONow + ".raw"; 120 | let tempRecordingFilepath= TEMP_FILE_PATH +tempRecordingFilename 121 | const writeRecordingFStream = fs.createWriteStream(tempRecordingFilepath); 122 | 123 | // good for debugging -> get the raw data 124 | passthroughStream.on('data', (chunk)=>{ 125 | writeRecordingFStream.write(chunk); 126 | }); 127 | */ 128 | 129 | const tsClient = new TranscribeStreamingClient({ region: REGION}); 130 | let tsParams = { 131 | /* SessionId: sessionId, */ // Pass a session id here if you need it 132 | LanguageCode: TRANSCRIBE_LANGUAGE_CODE, 133 | MediaEncoding: "pcm", 134 | MediaSampleRateHertz: 32000, 135 | //NumberOfChannels: 2, 136 | //EnableChannelIdentification: true, 137 | AudioStream: audioStream(), 138 | } 139 | 140 | if(IS_CONTENT_REDACTION_ENABLED) { 141 | tsParams.ContentRedactionType = CONTENT_REDACTION_TYPE; 142 | if(PII_ENTITY_TYPES) tsParams.PiiEntityTypes = PII_ENTITY_TYPES; 143 | } 144 | 145 | if(CUSTOM_VOCABULARY_NAME) { 146 | tsParams.VocabularyName = CUSTOM_VOCABULARY_NAME; 147 | } 148 | 149 | const tsCmd = new StartStreamTranscriptionCommand(tsParams); 150 | const tsResponse = await tsClient.send(tsCmd); 151 | 152 | //console.log(tsResponse); 153 | sessionId = tsResponse['SessionId']; 154 | 155 | const tsStream = stream.Readable.from(tsResponse.TranscriptResultStream); 156 | 157 | var kvsWorker = runKVSWorker({ 158 | region: REGION, 159 | streamName: streamName, 160 | lastFragment: lastFragment 161 | }, passthroughStream); 162 | 163 | timeToStop = false; 164 | stopTimer = setTimeout(() => { 165 | timeToStop = true; 166 | kvsWorker.worker.postMessage('time to stop the worker'); 167 | }, TIMEOUT); 168 | 169 | var transcribePromise = readTranscripts(tsStream, meetingId); 170 | 171 | var lastFragmentRead = await kvsWorker; 172 | 173 | //we are done with transcribe. 174 | //passthroughStream.write(Buffer.alloc(0)); 175 | passthroughStream.end(); 176 | 177 | var transcribeResult = await transcribePromise; 178 | 179 | console.log("Last fragment read: ", lastFragmentRead); 180 | 181 | // stop the timer so when we finish and upload to s3 this doesnt kick in 182 | if(timeToStop === false) { 183 | clearTimeout(stopTimer); 184 | timeToStop = false; 185 | } 186 | } 187 | 188 | go(process.argv[2], process.argv[3], process.argv[4], undefined); 189 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/raw-templates/windows-container-pipeline-template.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | AWSTemplateFormatVersion: 2010-09-09 5 | Transform: "AWS::Serverless-2016-10-31" 6 | Description: Windows Server container CI/CD pipeline 7 | Metadata: 8 | AWS::CloudFormation::Interface: 9 | ParameterGroups: 10 | - Label: 11 | default: "General" 12 | Parameters: 13 | - ProjectId 14 | ParameterLabels: 15 | ProjectId: 16 | default: "Project ID" 17 | 18 | Parameters: 19 | # You can provide these parameters in your CreateProject API call. 20 | ProjectId: 21 | Type: String 22 | Description: Prefix that will be used for AWS resources generated by the template. 23 | Default: windows-container-cicd 24 | 25 | RepositoryArn: 26 | Type: String 27 | Description: CodeCommit repository ARN from CDK Pipeline application. 28 | 29 | # Custom Action 30 | CustomActionProviderName: 31 | Type: String 32 | Description: Name of the custom action provider (used in CodePipeline Console UI). 33 | CustomActionProviderCategory: 34 | Type: String 35 | Description: Category of the custom action provider (used in CodePipeline Console UI). 36 | AllowedValues: 37 | - Build 38 | - Deploy 39 | - Invoke 40 | - Test 41 | CustomActionProviderVersion: 42 | Type: String 43 | Description: Version of the custom action provider (used in CodePipeline Console UI). 44 | 45 | LatestECSOptimizedAMI: 46 | Description: AMI ID 47 | Type: AWS::SSM::Parameter::Value 48 | Default: /aws/service/ami-windows-latest/Windows_Server-2019-English-Core-ECS_Optimized/image_id 49 | 50 | ECRPrivateRepoName: 51 | Type: String 52 | Description: Name of the ECR Repo for the vpf coontariner image 53 | 54 | Resources: 55 | ArtifactsBucket: 56 | Type: AWS::S3::Bucket 57 | DeletionPolicy: Retain 58 | Description: Amazon S3 bucket for AWS CodePipeline artifacts 59 | Properties: 60 | BucketName: !Sub "${ProjectId}-${AWS::AccountId}-${AWS::Region}-artifacts" 61 | VersioningConfiguration: 62 | Status: Enabled 63 | BucketEncryption: 64 | ServerSideEncryptionConfiguration: 65 | - ServerSideEncryptionByDefault: 66 | SSEAlgorithm: AES256 67 | 68 | DockerRepository: 69 | Type: AWS::ECR::Repository 70 | Properties: 71 | RepositoryName: !Sub "${ECRPrivateRepoName}" 72 | 73 | BuilderStageDockerRepository: 74 | Type: AWS::ECR::Repository 75 | Properties: 76 | RepositoryName: !Sub "${ECRPrivateRepoName}-builder-stage" 77 | 78 | # Role for CI/CD pipeline execution 79 | CodePipelineRole: 80 | Type: AWS::IAM::Role 81 | Description: Creating service role in IAM for AWS CodePipeline 82 | Properties: 83 | AssumeRolePolicyDocument: 84 | Statement: 85 | - Action: sts:AssumeRole 86 | Effect: Allow 87 | Principal: 88 | Service: 89 | - codepipeline.amazonaws.com 90 | Sid: 1 91 | Path: / 92 | Policies: 93 | - PolicyName: !Sub "${ProjectId}-codepipeline-policy" 94 | PolicyDocument: 95 | Statement: 96 | # Your pipeline will generally need permissions to store and retrieve artifacts in Amazon S3. 97 | # It will also need permissions to detect changes to your repository, start 98 | # a build against your AWS CodeBuild project, and create an AWS CloudFormation stack 99 | # containing your runtime resources. Adjust these policies as needed. 100 | - Action: 101 | - s3:PutObject 102 | - s3:ListObjects 103 | - s3:ListBucket 104 | - s3:GetObjectVersion 105 | - s3:GetObject 106 | - s3:GetBucketLocation 107 | - codecommit:GetCommit 108 | - codecommit:GetBranch 109 | - codecommit:UploadArchive 110 | - codecommit:GetUploadArchiveStatus 111 | - codebuild:StartBuild 112 | - codebuild:BatchGetBuilds 113 | Effect: Allow 114 | Resource: "*" 115 | 116 | AmazonCloudWatchEventRole: 117 | Type: AWS::IAM::Role 118 | Properties: 119 | AssumeRolePolicyDocument: 120 | Version: 2012-10-17 121 | Statement: 122 | - Effect: Allow 123 | Principal: 124 | Service: 125 | - events.amazonaws.com 126 | Action: sts:AssumeRole 127 | Path: / 128 | Policies: 129 | - PolicyName: cwe-pipeline-execution 130 | PolicyDocument: 131 | Version: 2012-10-17 132 | Statement: 133 | - Effect: Allow 134 | Action: codepipeline:StartPipelineExecution 135 | Resource: !Sub "arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${WindowsContainerPipeline}" 136 | 137 | AmazonCloudWatchEventRule: 138 | Type: AWS::Events::Rule 139 | Properties: 140 | EventPattern: 141 | source: 142 | - aws.codecommit 143 | detail-type: 144 | - "CodeCommit Repository State Change" 145 | resources: 146 | - !Ref RepositoryArn 147 | detail: 148 | event: 149 | - referenceCreated 150 | - referenceUpdated 151 | referenceType: 152 | - branch 153 | referenceName: 154 | - main 155 | Targets: 156 | - Arn: !Sub "arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${WindowsContainerPipeline}" 157 | RoleArn: !GetAtt AmazonCloudWatchEventRole.Arn 158 | Id: windows-container-pipeline 159 | 160 | WindowsContainerPipeline: 161 | Type: AWS::CodePipeline::Pipeline 162 | Description: Creating a deployment pipeline for your project in AWS CodePipeline 163 | DependsOn: 164 | - ArtifactsBucket 165 | Properties: 166 | Name: !Sub "${ProjectId}-pipeline" 167 | ArtifactStore: 168 | Location: !Ref ArtifactsBucket 169 | Type: S3 170 | RoleArn: !Sub "${CodePipelineRole.Arn}" 171 | Stages: 172 | - Name: Source 173 | Actions: 174 | - Name: SourceAction 175 | ActionTypeId: 176 | Category: Source 177 | Owner: AWS 178 | Version: 1 179 | Provider: CodeCommit 180 | OutputArtifacts: 181 | - Name: Source 182 | Configuration: 183 | BranchName: main 184 | RepositoryName: !Select [5, !Split [":", !Ref RepositoryArn]] 185 | PollForSourceChanges: false 186 | RunOrder: 1 187 | 188 | - Name: Package 189 | Actions: 190 | - Name: Publish-Docker-Image 191 | RunOrder: 1 192 | ActionTypeId: 193 | Owner: Custom 194 | Category: !Ref CustomActionProviderCategory 195 | Provider: !Ref CustomActionProviderName 196 | Version: !Ref CustomActionProviderVersion 197 | Configuration: 198 | ImageId: !Ref LatestECSOptimizedAMI 199 | InstanceType: m5.2xlarge 200 | Command: !Sub "Set-Location 'src'; dir; .\\build-and-publish-docker-image.ps1 -accountId ${AWS::AccountId} -region ${AWS::Region} -repositoryName ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${DockerRepository}" 201 | InputArtifacts: 202 | - Name: Source 203 | 204 | Outputs: 205 | ArtifactsBucket: 206 | Description: Bucket for AWS CodePipeline artifacts 207 | Value: !Ref ArtifactsBucket 208 | DockerRepositoryName: 209 | Description: Docker container registry name 210 | Value: !Ref DockerRepository 211 | Export: 212 | Name: DockerRepositoryName 213 | DockerRepositoryArn: 214 | Description: Docker container registry ARN 215 | Value: !GetAtt DockerRepository.Arn 216 | Export: 217 | Name: DockerRepositoryArn 218 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/raw-templates/lambda/poller/lambda.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import os 5 | import json 6 | import uuid 7 | import boto3 8 | import traceback 9 | from enum import Enum 10 | 11 | # Environment Variables 12 | STATE_MACHINE_ARN = os.environ["STATE_MACHINE_ARN"] 13 | CUSTOM_ACTION_PROVIDER_NAME = os.environ["CUSTOM_ACTION_PROVIDER_NAME"] 14 | CUSTOM_ACTION_PROVIDER_CATEGORY = os.environ["CUSTOM_ACTION_PROVIDER_CATEGORY"] 15 | CUSTOM_ACTION_PROVIDER_VERSION = os.environ["CUSTOM_ACTION_PROVIDER_VERSION"] 16 | 17 | # Load necessary AWS SDK clients 18 | code_pipeline = boto3.client("codepipeline") 19 | step_functions = boto3.client("stepfunctions") 20 | 21 | print( 22 | f"Loading function. " 23 | f"Provider name: {CUSTOM_ACTION_PROVIDER_NAME}, " 24 | f"category: {CUSTOM_ACTION_PROVIDER_CATEGORY}, " 25 | f"version: {CUSTOM_ACTION_PROVIDER_VERSION}" 26 | ) 27 | 28 | 29 | class JobFlowStatus(Enum): 30 | Running = 1 31 | Succeeded = 2 32 | Failed = 3 33 | 34 | 35 | def should_process_event(event: object) -> bool: 36 | """ 37 | Whether or not lambda function should process the incoming event. 38 | :param event: Event object, passed as lambda argument. 39 | :return: True if the event should be processed; False otherwise. 40 | """ 41 | source = event.get("source", "") 42 | 43 | # always poll CodePipeline if triggered by CloudWatch scheduled event 44 | if source == "aws.events": 45 | return True 46 | 47 | # process CodePipeline events 48 | if source == "aws.codepipeline": 49 | action_type = event.get("detail", {}).get("type", {}) 50 | owner = action_type.get("owner", "") 51 | provider = action_type.get("provider", "") 52 | category = action_type.get("category", "") 53 | version = action_type.get("version", "") 54 | 55 | return all( 56 | [ 57 | owner == "Custom", 58 | provider == CUSTOM_ACTION_PROVIDER_NAME, 59 | category == CUSTOM_ACTION_PROVIDER_CATEGORY, 60 | version == CUSTOM_ACTION_PROVIDER_VERSION, 61 | ] 62 | ) 63 | 64 | 65 | def lambda_handler(event, context): 66 | # Log the received event 67 | print("Received event: " + json.dumps(event, indent=2)) 68 | 69 | # Handle only custom events 70 | if not should_process_event(event): 71 | return 72 | 73 | try: 74 | jobs = get_active_jobs() 75 | 76 | for job in jobs: 77 | job_id = job["id"] 78 | continuation_token = get_job_attribute(job, "continuationToken", "") 79 | print( 80 | f"processing job: {job_id} with continuationToken: {continuation_token}" 81 | ) 82 | 83 | try: 84 | process_job(job, job_id, continuation_token) 85 | except Exception: 86 | print(f"error during processing job: {job_id}") 87 | traceback.print_exc() 88 | mark_job_failed(job_id, continuation_token) 89 | 90 | except Exception: 91 | traceback.print_exc() 92 | raise 93 | 94 | 95 | def process_job(job, job_id, continuation_token): 96 | # inform CodePipeline about that 97 | ack_response = code_pipeline.acknowledge_job(jobId=job_id, nonce=job["nonce"]) 98 | if not continuation_token: 99 | print("this is a new job, start the flow") 100 | start_new_job(job, job_id) 101 | else: 102 | # Get current job flow status 103 | job_flow_status = get_job_flow_status(continuation_token) 104 | print("current job status: " + job_flow_status.name) 105 | 106 | if job_flow_status == JobFlowStatus.Running: 107 | mark_job_in_progress(job_id, continuation_token) 108 | elif job_flow_status == JobFlowStatus.Succeeded: 109 | mark_job_succeeded(job_id, continuation_token) 110 | elif job_flow_status == JobFlowStatus.Failed: 111 | mark_job_failed(job_id, continuation_token) 112 | 113 | 114 | def get_active_jobs(): 115 | # Call DescribeJobs 116 | response = code_pipeline.poll_for_jobs( 117 | actionTypeId={ 118 | "owner": "Custom", 119 | "category": CUSTOM_ACTION_PROVIDER_CATEGORY, 120 | "provider": CUSTOM_ACTION_PROVIDER_NAME, 121 | "version": CUSTOM_ACTION_PROVIDER_VERSION, 122 | }, 123 | maxBatchSize=10, 124 | ) 125 | jobs = response.get("jobs", []) 126 | return jobs 127 | 128 | 129 | def start_new_job(job, job_id): 130 | # start job execution flow 131 | execution_arn = start_job_flow(job_id, job) 132 | # report progress to have a proper link on the console 133 | # and "register" continuation token for subsequent jobs 134 | progress_response = code_pipeline.put_job_success_result( 135 | jobId=job_id, 136 | continuationToken=execution_arn, 137 | executionDetails={ 138 | "summary": "Starting EC2 Build...", 139 | "externalExecutionId": execution_arn, 140 | "percentComplete": 0, 141 | }, 142 | ) 143 | 144 | 145 | def mark_job_failed(job_id, continuation_token): 146 | print("mark job as failed") 147 | 148 | failure_details = {"type": "JobFailed", "message": "Job Flow Failed miserably..."} 149 | 150 | if continuation_token: 151 | failure_details["externalExecutionId"] = continuation_token 152 | 153 | progress_response = code_pipeline.put_job_failure_result( 154 | jobId=job_id, failureDetails=failure_details 155 | ) 156 | 157 | 158 | def mark_job_succeeded(job_id, continuation_token): 159 | print("completing the job") 160 | progress_response = code_pipeline.put_job_success_result( 161 | jobId=job_id, 162 | executionDetails={ 163 | "summary": "Finishing EC2 Build...", 164 | "externalExecutionId": continuation_token, 165 | "percentComplete": 100, 166 | }, 167 | ) 168 | 169 | 170 | def mark_job_in_progress(job_id, continuation_token): 171 | print("completing the job, preserving continuationToken") 172 | progress_response = code_pipeline.put_job_success_result( 173 | jobId=job_id, continuationToken=continuation_token 174 | ) 175 | 176 | 177 | def get_job_attribute(job, attribute, default): 178 | return job.get("data", {}).get(attribute, default) 179 | 180 | 181 | def get_job_flow_status(flow_id) -> JobFlowStatus: 182 | response = step_functions.describe_execution(executionArn=flow_id) 183 | status = response.get("status", "FAILED") 184 | 185 | if status == "RUNNING": 186 | return JobFlowStatus.Running 187 | elif status == "SUCCEEDED": 188 | return JobFlowStatus.Succeeded 189 | else: 190 | return JobFlowStatus.Failed 191 | 192 | 193 | def start_job_flow(job_id, job): 194 | # job model reference: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codepipeline.html#CodePipeline.Client.get_job_details 195 | input_artifacts = get_job_attribute(job, "inputArtifacts", []) 196 | output_artifacts = get_job_attribute(job, "outputArtifacts", []) 197 | 198 | input_artifact = get_first_artifact(input_artifacts) 199 | output_artifact = get_first_artifact(output_artifacts) 200 | 201 | configuration = get_job_attribute(job, "actionConfiguration", {}).get( 202 | "configuration", {} 203 | ) 204 | image_id = configuration.get("ImageId") 205 | instance_type = configuration.get("InstanceType") 206 | command_text = configuration.get("Command") 207 | working_directory = configuration.get("WorkingDirectory", "") 208 | output_artifact_path = configuration.get("OutputArtifactPath", "") 209 | 210 | pipeline_context = get_job_attribute(job, "pipelineContext", {}) 211 | pipeline_execution_id = pipeline_context.get("pipelineExecutionId") 212 | pipeline_arn = pipeline_context.get("pipelineArn") 213 | pipeline_name = pipeline_context.get("pipelineName") 214 | 215 | sfn_input = { 216 | "params": { 217 | "pipeline": { 218 | "jobId": job_id, 219 | "executionId": pipeline_execution_id, 220 | "arn": pipeline_arn, 221 | "name": pipeline_name, 222 | }, 223 | "artifacts": { 224 | "input": { 225 | "bucketName": input_artifact.get("bucketName"), 226 | "objectKey": input_artifact.get("objectKey"), 227 | }, 228 | "output": { 229 | "path": output_artifact_path, 230 | "bucketName": output_artifact.get("bucketName", ""), 231 | "objectKey": output_artifact.get("objectKey", ""), 232 | }, 233 | }, 234 | "instance": { 235 | "imageId": image_id, 236 | "instanceType": instance_type, 237 | "keyName": "", 238 | }, 239 | "command": { 240 | "commandText": command_text, 241 | "workingDirectory": working_directory, 242 | "timeout": 28800, 243 | }, 244 | } 245 | } 246 | 247 | sfn_results = step_functions.start_execution( 248 | stateMachineArn=STATE_MACHINE_ARN, 249 | name=uuid.uuid4().hex, 250 | input=json.dumps(sfn_input), 251 | ) 252 | 253 | return sfn_results.get("executionArn", "") 254 | 255 | 256 | def get_first_artifact(input_artifacts): 257 | input_artifact = {} 258 | if input_artifacts: 259 | input_artifact = input_artifacts[0].get("location", {}).get("s3Location", {}) 260 | return input_artifact 261 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/lib/zoom-meeting-bot-cdk-app-stack.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import * as cdk from 'aws-cdk-lib'; 5 | import { Fn, Tags } from 'aws-cdk-lib'; 6 | import * as ec2 from 'aws-cdk-lib/aws-ec2'; 7 | import * as ecr from 'aws-cdk-lib/aws-ecr'; 8 | import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets'; 9 | import * as ecs from 'aws-cdk-lib/aws-ecs'; 10 | import * as iam from "aws-cdk-lib/aws-iam"; 11 | import { Code, Function, InlineCode, LayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda'; 12 | import * as logs from "aws-cdk-lib/aws-logs"; 13 | import * as sm from "aws-cdk-lib/aws-secretsmanager"; 14 | import * as sns from "aws-cdk-lib/aws-sns"; 15 | import { Construct } from 'constructs'; 16 | import path = require('path'); 17 | 18 | const lambdaLayerLookup = { 19 | "ap-northeast-1": "249908578461", 20 | "us-east-1": "668099181075", 21 | "ap-southeast-1": "468957933125", 22 | "eu-west-1": "399891621064", 23 | "us-west-1": "325793726646", 24 | "ap-east-1": "118857876118", 25 | "ap-northeast-2": "296580773974", 26 | "ap-northeast-3": "961244031340", 27 | "ap-south-1": "631267018583", 28 | "ap-southeast-2": "817496625479", 29 | "ca-central-1": "778625758767", 30 | "eu-central-1": "292169987271", 31 | "eu-north-1": "642425348156", 32 | "eu-west-2": "142628438157", 33 | "eu-west-3": "959311844005", 34 | "sa-east-1": "640010853179", 35 | "us-east-2": "259788987135", 36 | "us-west-2": "420165488524", 37 | "cn-north-1": "683298794825", 38 | "cn-northwest-1": "382066503313", 39 | "us-gov-west": "556739011827", 40 | "us-gov-east": "138526772879" 41 | } 42 | 43 | export class ZoomMeetingBotCdkAppStack extends cdk.Stack { 44 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 45 | super(scope, id, props); 46 | 47 | const vpc = this.buildVpc(); 48 | 49 | const s3GatewayEndpoint = vpc.addGatewayEndpoint('s3-endpoint', { 50 | service: ec2.GatewayVpcEndpointAwsService.S3, 51 | }); 52 | const secretsManagerInterfaceEndpoint = vpc.addInterfaceEndpoint('sm-endpoint', { 53 | service: ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER, 54 | subnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, 55 | }); 56 | const ecrInterfaceEndpoint = vpc.addInterfaceEndpoint('ecr-endpoint', { 57 | service: ec2.InterfaceVpcEndpointAwsService.ECR, 58 | subnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, 59 | }); 60 | const ecrDockerInterfaceEndpoint = vpc.addInterfaceEndpoint('ecr-docker-endpoint', { 61 | service: ec2.InterfaceVpcEndpointAwsService.ECR_DOCKER, 62 | subnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS } 63 | }); 64 | const snsInterfaceEndpoint = vpc.addInterfaceEndpoint('sns-endpoint', { 65 | service: ec2.InterfaceVpcEndpointAwsService.SNS, 66 | subnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS } 67 | }); 68 | 69 | const fargateResources = this.buildFargateResources(vpc); 70 | 71 | const fargateClientLambda = new Function(this, 'FargateClientFunction', { 72 | runtime: Runtime.PYTHON_3_7, 73 | handler: 'index.lambda_handler', 74 | code: Code.fromAsset(path.join(__dirname, 'lambda-py37/fargate-client')), 75 | vpc: vpc, 76 | vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, 77 | securityGroups: [fargateResources.securityGroup], 78 | }); 79 | 80 | const layerAccount = lambdaLayerLookup[this.node.tryGetContext("region") as keyof typeof lambdaLayerLookup]; 81 | 82 | fargateClientLambda.addLayers(LayerVersion.fromLayerVersionArn(this, "PythonLambdaLayer", `arn:aws:lambda:${cdk.Aws.REGION}:${layerAccount}:layer:AWSLambda-Python-AWS-SDK:4`)); 83 | } 84 | 85 | private buildVpc(): cdk.aws_ec2.Vpc { 86 | const vpc = new ec2.Vpc(this, 'zoom-cdk-vpc', { 87 | cidr: '10.0.0.0/16', 88 | maxAzs: 2, 89 | subnetConfiguration: [ 90 | { 91 | name: 'private-zoom-', 92 | subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, 93 | cidrMask: 24, 94 | }, 95 | { 96 | name: 'public-zoom-', 97 | subnetType: ec2.SubnetType.PUBLIC, 98 | cidrMask: 24, 99 | } 100 | ], 101 | }); 102 | 103 | return vpc; 104 | } 105 | 106 | private buildFargateResources(vpc: cdk.aws_ec2.Vpc) { 107 | 108 | const cluster = new ecs.Cluster(this, 'FargateCluster', { 109 | vpc: vpc, 110 | }); 111 | 112 | const cfnClusterCapacityProviderAssociations = new ecs.CfnClusterCapacityProviderAssociations(this, 'CapacityProviderAssociations', { 113 | cluster: cluster.clusterName, 114 | capacityProviders: [ 115 | 'FARGATE_SPOT', 116 | 'FARGATE' 117 | ], 118 | defaultCapacityProviderStrategy: [ 119 | { 120 | 'capacityProvider': 'FARGATE' 121 | } 122 | ] 123 | }); 124 | 125 | const taskDefinition = new ecs.TaskDefinition(this, 'ZoomTaskDefinition', { 126 | memoryMiB: '4096', 127 | cpu: '2048', 128 | compatibility: ecs.Compatibility.FARGATE, 129 | runtimePlatform: { 130 | operatingSystemFamily: ecs.OperatingSystemFamily.WINDOWS_SERVER_2019_CORE 131 | }, 132 | }); 133 | 134 | const importedContainerRepositoryArn = cdk.Fn.importValue('DockerRepositoryArn'); 135 | const importedContainerRepositoryName = cdk.Fn.importValue('DockerRepositoryName'); 136 | 137 | const repo = ecr.Repository.fromRepositoryAttributes(this, 'ZoomMeetingBotRepo', { 138 | "repositoryName": importedContainerRepositoryName.toString(), 139 | "repositoryArn": importedContainerRepositoryArn.toString() 140 | }); 141 | 142 | const zoomSecret = sm.Secret.fromSecretCompleteArn(this, "ZoomSecret", this.node.tryGetContext('zoomsecret_arn')) 143 | const userSecret = sm.Secret.fromSecretCompleteArn(this, "UserSecret", this.node.tryGetContext('usersecret_arn')) 144 | 145 | const topic = new sns.Topic(this, "ZoomTopic"); 146 | 147 | const container = taskDefinition.addContainer('zoom-meeting-bot-container', { 148 | image: ecs.ContainerImage.fromEcrRepository(repo), 149 | entryPoint: ["sdk_demo_v2.exe"], 150 | essential: true, 151 | logging: ecs.LogDrivers.awsLogs({streamPrefix: 'zoom-task-group-logs', logRetention: 30}), 152 | environment: { 153 | "KVS_STREAM_SUFFIX": "zoom", 154 | "AWS_DEFAULT_REGION": this.node.tryGetContext('region'), 155 | "SNS_TOPIC_ARN": topic.topicArn, 156 | "GST_DEBUG": "4" 157 | }, 158 | secrets: { 159 | "AWS_ACCESS_KEY_ID": ecs.Secret.fromSecretsManager(userSecret, "AWS_ACCESS_KEY_ID"), 160 | "AWS_SECRET_ACCESS_KEY": ecs.Secret.fromSecretsManager(userSecret, "AWS_SECRET_ACCESS_KEY"), 161 | "ZOOM_APP_KEY": ecs.Secret.fromSecretsManager(zoomSecret, "ZOOM_APP_KEY"), 162 | "ZOOM_APP_SECRET": ecs.Secret.fromSecretsManager(zoomSecret, "ZOOM_APP_SECRET") 163 | } 164 | }); 165 | 166 | taskDefinition.taskRole.addManagedPolicy( 167 | iam.ManagedPolicy.fromAwsManagedPolicyName( 168 | "service-role/AmazonECSTaskExecutionRolePolicy" 169 | ) 170 | ); 171 | 172 | taskDefinition.taskRole.attachInlinePolicy(new iam.Policy(this, 'task-policy', { 173 | statements: [ 174 | new iam.PolicyStatement({ 175 | actions: ["ssmmessages:CreateControlChannel", 176 | "ssmmessages:CreateDataChannel", 177 | "ssmmessages:OpenControlChannel", 178 | "ssmmessages:OpenDataChannel"], 179 | resources: ['*'], 180 | }), 181 | new iam.PolicyStatement({ 182 | actions: ["secretsmanager:GetSecretValue"], 183 | resources: [ 184 | this.node.tryGetContext('zoomsecret_arn'), 185 | this.node.tryGetContext('usersecret_arn') 186 | ], 187 | }), 188 | new iam.PolicyStatement({ 189 | actions: [ 190 | "kinesisvideo:CreateStream", 191 | "kinesisvideo:PutMedia", 192 | "kinesisvideo:GetDataEndpoint", 193 | "kinesisvideo:DescribeStream"], 194 | resources: ['*'], 195 | }) 196 | ], 197 | })); 198 | 199 | const logGroup = new logs.LogGroup(this, 'FarGateLogGroup'); 200 | 201 | const logDriver = new ecs.AwsLogDriver({ 202 | logGroup: logGroup, 203 | streamPrefix: "ZoomMeetingBotFargateTask" 204 | }); 205 | 206 | const securityGroup = new ec2.SecurityGroup(this, 'FargateSecurityGroup', { 207 | vpc: vpc, 208 | securityGroupName: "Fargate", 209 | description: "Allow access to fargate", 210 | }); 211 | 212 | securityGroup.addEgressRule( 213 | ec2.Peer.anyIpv4(), 214 | ec2.Port.allTraffic(), 215 | ); 216 | 217 | securityGroup.addIngressRule( 218 | ec2.Peer.ipv4('10.0.0.0/16'), 219 | ec2.Port.allTraffic() 220 | ) 221 | 222 | return { cluster, taskDefinition, securityGroup }; 223 | } 224 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/src/README.md: -------------------------------------------------------------------------------- 1 | # Zoom Meeting virtual participant application 2 | 3 | 4 | > :warning: The Zoom Meeting sample **IS NO LONGER Functional**. As of Feb 3, 2024 the version of the Zoom Meeting Windows SDK used as a dependency is no longer compatible with Zoom Service. This means the sample app (i.e. the bot application that is created by Virtual Patricipant Framework) can no longer join a Zoom meeting as a participant. Unfortunately newer versions of the Zoom Meeting Windows SDK wrappend in a Docker container can no longer stream media to Amazon Kinesis Video Stream rendering them incompatible with the sample. After a lengthy investigation we have not been able to resolve the compatiblity issue, highlighting the fragility of this sample solution. This bring our Virtual Participant Framework sample and the experiment to End of Life. For any community support or questions please connect with the Zoom developer ecosystem team via the [Zoom Developer Forum](https://devforum.zoom.us/). 5 | 6 | ## About 7 | 8 | This is a subproject of the AWS virtual participant framework for Zoom Meeting. It contains the artifacts needed to build virtual participant Windows applications using the Zoom Meeting Windows SDK which can be downloaded from the [Zoom App Marketplace](https://marketplace.zoom.us/docs/sdk/native-sdks/windows/). 9 | 10 | The simplest way to run the virtual participant framework in your AWS account is using infrastructure as code project in the parent folder. However, if you wish to edit, build and test the Zoom Meeting sdk application locally or on a remote windows machine you can follow the instructions in this sub-project. 11 | 12 | ## Important 13 | 14 | - As an AWS best practice, grant this code least privilege, or only the 15 | permissions required to perform a task. For more information, see 16 | [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege) 17 | in the *AWS Identity and Access Management User Guide*. 18 | - This code has not been tested in all AWS Regions. Some AWS services are 19 | available only in specific AWS Regions. For more information, see the 20 | [AWS Regional Services List](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/) 21 | on the AWS website. 22 | - Running this code might result in charges to your AWS account. 23 | 24 | ## Prerequisites 25 | 26 | > NOTE: This prototype was built/tested on Windows 10 64-bit Professional edition. Other versions of Windows may or may not be compatible with this project. 27 | 28 | * Windows 10 64-bit operating system (or Windows Server 2019) 29 | * Visual Studio Community (, Professional or Enterprise) 2019 (or 2022); please "ensure that you install Desktop development with C++ option from Workloads and the platform SDKs that you are building for" as described in [Zoom Meeting Windows SDK Getting Started - Prerequisites](https://marketplace.zoom.us/docs/sdk/native-sdks/windows/getting-started/prerequisites/), including the option for **Windows 10 SDK (10.0.18362.0)** 30 | * [Python3 for Windows 64-bit](https://www.python.org/downloads/windows/) 31 | * Node version 16 [e.g. using NVM]](https://github.com/nvm-sh/nvm); more recent versions may not work with deployment 32 | * [Install Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 33 | 34 | ## Getting Started 35 | 36 | Before building the project the right resources and credentials must be created in [AWS](console.aws.amazon.com/) and on [Zoom App Marketplace](https://marketplace.zoom.us/docs/sdk/native-sdks/windows/) 37 | 38 | ### Provision cloud resources 39 | 40 | To test full functionality of streaming multimedia from a Zoom Meeting to AWS, the following cloud resources must be provisioned. These steps must be taken prior to building this project locally or for custom CI/CD 41 | 42 | 1. Choose an AWS account and select a region that support Amazon Kinesis Video Stream (KVS) 43 | 2. Create an Amazon SNS topic with the right rousource policy and subscription 44 | 3. Create an IAM User whose credential will be used by the Zoom Meeting Windows SDK application to connect to AWS 45 | 4. Create an IAM Policy for the IAM User allowing it to publish to SNS and act as an Amazon KVS producer 46 | 47 | > Note: Please refer to [Amazon SNS](https://docs.aws.amazon.com/sns/latest/dg/sns-security.html) and [Amazon KVS](https://docs.aws.amazon.com/kinesisvideostreams/latest/dg/security.html) documentations on Security for best practices. 48 | 49 | ### Get developer keys from the Zoom App Marketplace 50 | 51 | The virtual participant runs as a Windows app built ontop of Zoom Meeting Windows SDK. For connectivity to Zoom Service and to download the SDK: 52 | 53 | 1. Login to [Zoom App Marketplace](https://marketplace.zoom.us/) (Use your Zoom login - free accounts work!) 54 | 2. From the "Develop" drop-down choose "Build App" 55 | 3. From the options available "Create" a "Meeting SDK" app type and follow the instructions 56 | 4. In the resulting screen, click on "App Credential" from the sidebar to make note of "SDK Key" and "SDK Secret" 57 | 58 | > Note: Later you will need to download the Windows SDK from this screen using the "Download" option on the sidebar. 59 | 60 | ### Apply Zoom SDK patch 61 | 62 | If you haven't completed this step from the parent directory instructions 63 | 64 | 1. Download `zoom-sdk-windows-5.13.10.13355.zip` from the [Zoom App Marketplace](https://marketplace.zoom.us/docs/sdk/native-sdks/windows/) 65 | 66 | 2. Unzip the code archive to the this directory, so there is a **zoom-sdk-windows-5.13.10.13355** directory here. 67 | 68 | 3. Verify that you can successfully run: 69 | 70 | ```batch 71 | dir zoom-sdk-windows-5.13.10.13355 72 | ``` 73 | 74 | 4. Apply the Zoom Windows SDK patch: 75 | 76 | ```batch 77 | git apply -p1 --directory zoom-sdk-windows-5.13.10.13355 --verbose --reject --whitespace=fix zoom_sdk_demo_v2.patch 78 | ``` 79 | 80 | ## Building the Zoom Meeting Windows SDK Application Locally 81 | 82 | The following instructions will: 83 | 84 | * Install GStreamer (required for building KVS Producer SDK) 85 | * Install StrawberryPerl and Netwide Assembler (NASM) (required for building Amazon KVS Producer SDK) 86 | * Clone, patch, and build the [Amazon Kinesis Video Streams Producer SDK for C++](https://github.com/awslabs/amazon-kinesis-video-streams-producer-sdk-cpp) 87 | * Installing additional dependencies with [vcpkg package manager for C/C++](https://vcpkg.io/en/index.html) 88 | * build and run the 89 | 90 | ### Install GStreamer for Windows (v1.20.3) 91 | 92 | Download and install the following *.msi installation packages. During installation, select **Complete** installation. 93 | 94 | * [GStreamer 1.20.3 runtime installer - MSVC 64-bit (VS 2019, Release CRT)](https://gstreamer.freedesktop.org/data/pkg/windows/1.20.3/msvc/gstreamer-1.0-msvc-x86_64-1.20.3.msi) 95 | * [GStreamer 1.20.3 development installer - MSVC 64-bit (VS 2019, Release CRT)](https://gstreamer.freedesktop.org/data/pkg/windows/1.20.3/msvc/gstreamer-1.0-devel-msvc-x86_64-1.20.3.msi) 96 | 97 | ### Install StrawberryPerl and Netwide Assembler (NASM) for Windows 98 | 99 | Perform the following tasks: 100 | 101 | 1. [Install Chocolately package manager for Windows)](https://chocolatey.org/install) 102 | 103 | 2. Install NASM and Strawverryperl (and Git if not accessible via command prompt) 104 | 105 | ```batch 106 | choco install nasm strawberryperl git -y 107 | ``` 108 | 3. To enable Git to use pathnames longer than 260 characters, run 109 | 110 | ```batch 111 | git config --system core.longpaths true 112 | ``` 113 | 114 | ### Build the Amazon Kinesis Video Streams Producer SDK for C++ 115 | 116 | Perform the following tasks in windows command prompt (make sure you have Git installed first!): 117 | 118 | 1. Run the following commands 119 | 120 | ```batch 121 | cd C: 122 | git clone https://github.com/awslabs/amazon-kinesis-video-streams-producer-sdk-cpp.git 123 | ``` 124 | 3. Copy `kvs_msvc.patch` and `build_kvs_windows.bat` files from this repo to `C:\amazon-kinesis-video-streams-producer-sdk-cpp` 125 | 4. Run the following commands 126 | 127 | ```batch 128 | cd C:\amazon-kinesis-video-streams-producer-sdk-cpp 129 | git apply kvs_msvc.patch 130 | call build_kvs_windows.bat 131 | setx GST_PLUGIN_SYSTEM_PATH "%GST_PLUGIN_SYSTEM_PATH%;C:\amazon-kinesis-video-streams-producer-sdk-cpp\build" 132 | setx /M PATH "%PATH%;C:\amazon-kinesis-video-streams-producer-sdk-cpp\open-source\local\bin;C:\amazon-kinesis-video-streams-producer-sdk-cpp\open-source\local\lib" 133 | ``` 134 | 135 | > NOTE: You may need to edit the first lines of `build_kvs_windows.bat` based on the version of Visual Studio Community (, Professional or Enterprise) 2019 (or 2022) you have installed. 136 | 137 | ### Install dependencies with vcpkg 138 | 139 | 1. Open a new command prompt window and run: 140 | 141 | ```batch 142 | cd C: 143 | ``` 144 | 145 | > Note: If you have vcpkg already installed, skip the following. If you do **NOT** already have vcpkg installed, run the following commands: 146 | > 147 | 148 | ```batch 149 | git clone --depth 1 --branch 2023.02.24 https://github.com/Microsoft/vcpkg.git 150 | call .\vcpkg\bootstrap-vcpkg.bat 151 | ``` 152 | 153 | 2. Install the dependencies required for the prototype. Run the following commands: 154 | 155 | ```batch 156 | vcpkg\vcpkg install "aws-sdk-cpp[sns]" --recurse --triplet x64-windows 157 | vcpkg\vcpkg install "crow" --triplet x64-windows 158 | vcpkg\vcpkg install "cpp-jwt" --triplet x64-windows 159 | vcpkg\vcpkg integrate install 160 | ``` 161 | 162 | ### Set environment variables 163 | 164 | Set the following environment variables on the local windows machine: 165 | 166 | ```batch 167 | AWS_ACCESS_KEY_ID= 168 | AWS_SECRET_ACCESS_KEY= 169 | KVS_STREAM_SUFFIX= 170 | AWS_DEFAULT_REGION= 171 | SNS_TOPIC_ARN=::" 172 | ZOOM_APP_KEY= 173 | ZOOM_APP_SECRET= 174 | ``` 175 | 176 | > :warning: These variables contain sensitive AWS and Zoom account information. Ensure they are not exposed inadvertently. 177 | 178 | ### Build and run application with Visual Studio 179 | 180 | To test the functionality of the applicaiton perform the following tasks: 181 | 182 | 1. In Visual Studio open the solution in `zoom-sdk-windows-5.13.10.13355\x64\demo\sdk_demo_v2` directory 183 | 184 | 2. Choose **"Release"** and **"x64"** configuration and build solution. 185 | 186 | 3. Launch application with debugger (shortcut: F5) and accept network access. 187 | 188 | 4. If Zoom app SDK credentials are ok you should see the following dialog: 189 | 190 | Join Meeting App Dialog 191 | 192 | 5. Start a Zoom meeting as host with a native desktop/mobile client and take note of meeting id and passcode 193 | 194 | > Note: to avoid audio echo, it is best to run Zoom meeting as host on a different machine as the one running your application. 195 | 196 | 6. Enter the following in the application dialog and press the **join** button 197 | 198 | * `meeting id` (from host meeting) 199 | * `diplay name` (virtual partipant name) 200 | * `meeting passcode` (from host meeting) 201 | 202 | 7. After the virtual participant has joined, allow the virtual participant to locally record the meeting from the Zoom Meeting host client machine. This will trigger the virtual participant application to stream multimedia (Mixed-audio for now) to Amazon Kinesis Video. 203 | 204 | > Notes: 205 | > 206 | > i. To read on the capabilities of the Zoom Meeting Winodws SDK please refer to the [docs on Zoom App Marketplace](https://marketplace.zoom.us/docs/sdk/native-sdks/windows/) 207 | > 208 | > ii. The audio streamed to Amazon KVS is PCM-uLaw format. This format is not supported for playback in the AWS console or using HLS/MPEG-DASH. However it can be used a supported format for transcription in Amazon Transcribe. 209 | 210 | ## Run Amazon Transcribe Tester NodeJS app 211 | 212 | To see live transcription of Zoom Meeting audio and verify transcription, run the tester app in the [transcribe_tester_app](./transcribe_tester_app/) folder. See [transcribe_tester_app/README.md](./transcribe_tester_app/README.md) for details. 213 | 214 | ## Clean Up 215 | 216 | From AWS console, delete all Amazon KVS streams and the SNS Topic. If you created an IAM User for testing purposes, either remove it entirely or remove the Amazon SNS and Amazon KVS policies associated with it. 217 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/README.md: -------------------------------------------------------------------------------- 1 | # Virtual Participant Orchestrator for Zoom Meeting 2 | 3 | > :warning: The Zoom Meeting sample **IS NO LONGER Functional**. As of Feb 3, 2024 the version of the Zoom Meeting Windows SDK used as a dependency is no longer compatible with Zoom Service. This means the sample app (i.e. the bot application that is created by Virtual Patricipant Framework) can no longer join a Zoom meeting as a participant. Unfortunately newer versions of the Zoom Meeting Windows SDK wrappend in a Docker container can no longer stream media to Amazon Kinesis Video Stream rendering them incompatible with the sample. After a lengthy investigation we have not been able to resolve the compatiblity issue, highlighting the fragility of this sample solution. This bring our Virtual Participant Framework sample and the experiment to End of Life. For any community support or questions please connect with the Zoom developer ecosystem team via the [Zoom Developer Forum](https://devforum.zoom.us/). 4 | 5 | ## About 6 | 7 | This virtual participant orchestrator for Zoom Meeting is developed by AWS Prototyping and Cloud Engineering (PACE) team and Solutions Architects. The objectives of this prototype include: 8 | 9 | * successfully capturing audio from a Zoom meeting, and publishing to Amazon Kinesis Video Streams within the Zoom Meeting C++ SDK for Windows demo application 10 | * successfully publishing messages to Amazon Simple Notification Service (SNS) within the Zoom Meeting C++ SDK for Windows demo application 11 | * packaging the orchestrator architecture and Zoom meeting SDK based virtual participant app as a standalone, deployable artifact 12 | 13 | > :warning: This sample **IS NOT PRODUCTION READY!** and should serve as an example and for learning purposes. 14 | 15 | ## Architecture 16 | 17 | ![Prototype Architecture](docs/images/Zoom-Virtual-Participant.png) 18 | 19 | ## Important 20 | 21 | * As an AWS best practice, follow the principle of least privilege, and only grant permissions required to perform a task in any AWS account that is shared with other users, holding sensitive data, and/or tied to a production system. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege) in the *AWS Identity and Access Management User Guide*. This sample solution is intended for sandbox dev/test environments and requires the AWS account user to have `AdministratorAccess` privillage. 22 | * The solution deployment has not been tested in all AWS Regions, only us-east-1. Some dependant AWS services are available only in specific AWS Regions. For more information, see the [AWS Regional Services List](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/) on the AWS website. This sample solution is expected to function in any AWS Region where Amazon Kinesis Video Stream is available. 23 | * Running this code will likely result in charges to your AWS account. 24 | * If you are new to AWS, first follow [this tutorial](https://aws.amazon.com/getting-started/guides/setup-environment/) to get started. 25 | 26 | ## Setup 27 | 28 | ### Install Prerequisites 29 | 30 | * An AWS test account with no access to sensitive or production data 31 | * Ability to create an AWS IAM User, attach policies, and create access keys 32 | * [Install Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 33 | * [Install AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html) 34 | * [Instal CDK CLI](https://docs.aws.amazon.com/cdk/v2/guide/cli.html) 35 | 36 | > Note: The AWS IAM user that performs actions via the AWS CLI **MUST have** `AdministratorAccess` priviledge for the account. You cannot proceed with this sample solution without that managed policy applied to the IAM User that connects to you AWS account via the CLI. It is strongly advised **NOT to use** the AWS account's `root` user to perform actions via the AWS CLI or API. Instead create a new IAM User with administrator access by following [Getting started with IAM Identity Center](https://docs.aws.amazon.com/singlesignon/latest/userguide/getting-started.html). 37 | 38 | ### Get Developer Keys from Zoom 39 | 40 | The virtual participant runs as a Windows app built ontop of Zoom Meeting Windows SDK. For connectivity to Zoom Service you must register your Zoom Meeting SDK app: 41 | 42 | 1. Login to [Zoom App Marketplace](https://marketplace.zoom.us/) (Use your Zoom login - free accounts work!) 43 | 2. From the "Develop" drop-down choose "Build App" 44 | 3. From the options available "Create" a "Meeting SDK" app type and follow the instructions 45 | 4. In the resulting screen, click on "App Credential" from the sidebar to make note of **"Client ID"** and **"Client Secret"** 46 | 47 | > Note: Leave browser tab open. Later you will need to download the Windows SDK from this screen using the "Download" option on the sidebar. 48 | 49 | ### Create Repository and Secrets for CDK Deployment 50 | 51 | 1. Create a new AWS CodeCommit repository, and change default branch to `main` 52 | 53 | Example on linux (replace `MyVPFRepoName` and description): 54 | 55 | ```shell 56 | export CODECOMMIT_REPO_NAME=MyVPFRepo 57 | aws codecommit create-repository --repository-name $CODECOMMIT_REPO_NAME --repository-description "My VPF repository" 58 | ``` 59 | 60 | Example on windows (replace `MyVPFRepoName` and description): 61 | 62 | ```batch 63 | set CODECOMMIT_REPO_NAME=MyVPFRepo 64 | aws codecommit create-repository --repository-name %CODECOMMIT_REPO_NAME% --repository-description "My VPF repository" 65 | ``` 66 | 67 | 2. Clone CodeCommit repo (see repository on AWS Console and [docs](https://docs.aws.amazon.com/codecommit/latest/userguide/repositories.html) for instructions) to a local directory that is outside the path of the directories cloned from GitHub. 68 | 69 | > Note: The CodeCommit repository will include proprietary and licensed Zoom Windows SDK files. Changes commited to the CodeCommit repo **should not** be pushed upstream to the OSS GitHub repository. If you are interested in contributing to the source that links to Zoom Window SDK libraries, please create a GitHub issue or reach out to the maintainers for instructions. 70 | 71 | 3. `cd` into your new checked out directory and ensure you are on the **main** branch (not **master**) 72 | 73 | ```shell 74 | git checkout -b main 75 | ``` 76 | 77 | 4. Copy the contents of the [virtual-participant-orchestrator-for-zoom-meeting/](../virtual-participant-orchestrator-for-zoom-meeting/) subproject - not the root directory of the GitHub repo - to the local directory where the CodeCommit repo was cloned above. 78 | 79 | 5. From the CodeCommit console take note of the repository ARN from the "Repositories > Settings" side panel on the left. 80 | 81 | 6. Create a new IAM User (eg. vpf_user) with the following attached policy statment. Be sure to replace AWS account ID and region in the **"Resource"** elements with the appropriate values: 82 | 83 | ```json 84 | "Statement": [ 85 | { 86 | "Sid": "HandCraftedForVPF0", 87 | "Effect": "Allow", 88 | "Action": [ 89 | "kinesisvideo:PutMedia", 90 | "kinesisvideo:GetDataEndpoint", 91 | "kinesisvideo:DescribeStream", 92 | "kinesisvideo:CreateStream", 93 | "sns:Publish" 94 | ], 95 | "Resource": [ 96 | "arn:aws:kinesisvideo:us-east-1:111111111111:stream/*/*", 97 | "arn:aws:sns:us-east-1:111111111111:*" 98 | ] 99 | } 100 | ] 101 | ``` 102 | 103 | > Note: This is not the same IAM User as the one for the administrator performing deployments via the AWS CLI. This new IAM User policy follows least privillage principal and is assigned in runtime to Amazon ECS tasks that connect to Zoom as a virtual participant. Please see [Controlling access to Kinesis Video Streams resources using IAM](https://docs.aws.amazon.com/kinesisvideostreams/latest/dg/how-iam.html) and [Identity and access management in Amazon SNS](https://docs.aws.amazon.com/sns/latest/dg/security-iam.html) for more details. 104 | 105 | 7. From IAM Console create access keys for the user ([Learn more](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html)). 106 | 107 | > Note: Do not download .csv file. Instead keep the browser tab open until you record the **"access key"** and **"secret access key"** securely in AWS Secret Manager in the next step. In case you accidentally closed this tab, simply create a new accessss key and make the old one inactive. 108 | 109 | 8. Create two secrets in AWS Secrets Manager named `zoomsecret` and `usersecret`. 110 | 111 | The secret `usersecret` plaintext value should use the following format: 112 | 113 | ```json 114 | {"AWS_ACCESS_KEY_ID":"","AWS_SECRET_ACCESS_KEY":""} 115 | ``` 116 | 117 | The secret `zoomsecret` plaintext value should use the following format: 118 | 119 | ```json 120 | {"ZOOM_APP_KEY":"","ZOOM_APP_SECRET":""} 121 | ``` 122 | 123 | 9. Update `cdk.json` with the following updated attributes: 124 | 125 | * `account` 126 | * `region` 127 | * `codecommit_arn` 128 | * `zoomsecret_arn` 129 | * `usersecret_arn` 130 | 131 | ### Apply Zoom SDK Patch 132 | 133 | 1. Download the `zoom-sdk-windows-5.13.10.13355.zip` code archive from the [Zoom App Marketplace](https://marketplace.zoom.us/docs/sdk/native-sdks/windows/). 134 | 135 | 2. Unzip the code archive to the `src` directory, so there is a `src/zoom-sdk-windows-5.13.10.13355` path. 136 | 137 | 3. Apply the Zoom Windows SDK patch: 138 | 139 | ```shell 140 | git apply -p1 --directory src/zoom-sdk-windows-5.13.10.13355 --verbose --reject --whitespace=fix src/zoom_sdk_demo_v2.patch 141 | ``` 142 | 143 | ### Package CloudFormation Template 144 | 145 | 1. Create S3 bucket: 146 | 147 | a) Create an environment variable for `CF_TEMPLATE_S3_BUCKET` 148 | 149 | Example on linux (replace account # and region): 150 | 151 | ```shell 152 | export CF_TEMPLATE_S3_BUCKET=codepipeline-custom-action-111111111111-us-east-1 153 | ``` 154 | 155 | Example on Windows (replace account # and region): 156 | 157 | ```batch 158 | set CF_TEMPLATE_S3_BUCKET=codepipeline-custom-action-111111111111-us-east-1 159 | ``` 160 | 161 | b) Create bucket: 162 | 163 | On Linux: 164 | 165 | ```shell 166 | aws s3 mb s3://$CF_TEMPLATE_S3_BUCKET 167 | ``` 168 | 169 | On Windows: 170 | 171 | ```batch 172 | aws s3 mb s3://%CF_TEMPLATE_S3_BUCKET% 173 | ``` 174 | 175 | 2. Package template: 176 | 177 | On Linux: 178 | 179 | ```shell 180 | aws cloudformation package --template-file raw-templates/codepipeline-custom-action-template.yaml --output-template-file raw-templates/codepipeline-custom-action-deployment.yaml --s3-bucket $CF_TEMPLATE_S3_BUCKET 181 | ``` 182 | 183 | On Windows: 184 | 185 | ```batch 186 | aws cloudformation package --template-file raw-templates/codepipeline-custom-action-template.yaml --output-template-file raw-templates/codepipeline-custom-action-deployment.yaml --s3-bucket %CF_TEMPLATE_S3_BUCKET% 187 | ``` 188 | 189 | ### Deploy CDK Application 190 | 191 | 1. Install required dependencies: 192 | 193 | ```shell 194 | npm install 195 | ``` 196 | 197 | 2. Bootstrap your account/region for CDK deployment: 198 | 199 | ```shell 200 | cdk bootstrap 201 | ``` 202 | 203 | 3. Commit all changes to git and push code to your CodeCommit repository (Do not push upstream to GitHub): 204 | 205 | ```shell 206 | git add . 207 | git commit -m 'init' 208 | git push -u origin main 209 | ``` 210 | 211 | 4. Synthesize the CDK application into deployment templates: 212 | 213 | ```shell 214 | cdk synth 215 | ``` 216 | 217 | 5. Deploy the CDK application: 218 | 219 | ```shell 220 | cdk deploy 221 | ``` 222 | ___ 223 | 🟩 The `cdk deploy` task completes in less than 5 minutes. However, the overall deployment takes about **2️⃣ hours** to complete due to the long Windows container image build cycle. If successful, you should see a container image tagged as *latest* in an Amazon ECR repository named **zoom-virtual-participant-windows**. You must wait for this image to appear in the ECR repo before proceeding further. To track deployment progress you can look at the infrastructure as code deployment pipelines in AWS CodePipeline generated by CDK. 224 | ___ 225 | 🟥 If container image in ECR repository does not show up well beyond 2 hours, log into AWS console using the admin user and: 226 | 1. Check both **CDKPipeline** and **windows-container-cicd-pipeline** pipelines are green in CodePipeline. Then, 227 | 2. Check the status latest execution of the **ec2-codepipeline-builders-build-flow** state machine in AWS StepFunction. And finally, 228 | 3. Inspect the windows build and container image generation build logs in the latest AWS CloudWatch log stream under the log group beggining with **/aws/ssm/PipelineAppStage-CodePipelineCustomActionStack-RunBuildJobOnEc2Instance-** 229 | 4. To re-attempt a new deployment you must first follow steps 2, 3, and 4 in [Clean up](#clean-up) 230 | ___ 231 | 232 | ### Run Virtual Participant Demo 233 | 234 | 1. Edit the following attributes in the `etc/run-task.json` file: 235 | 236 | * `cluster` (from ECS console) 237 | * `subnets` (private subnet IDs with names ending with **"private-zoom-Subnet#"** with # as 1, 2, or 3, ...) 238 | * `securityGroups` (SG IDs with the field **"Security group name"** as **"Fargate"**) 239 | * `taskDefinition` (from ECS console) 240 | 241 | 2. Run the following command to start a new ECS Fargate task: 242 | 243 | ```shell 244 | aws ecs run-task --cli-input-json file://etc/run-task.json 245 | ``` 246 | 247 | 3. Start a Zoom meeting with a desktop/web/mobile client. 248 | 249 | 4. After the Fargate task has successfully started running, use the following payload for the FargateClientLambda lambda function to join the Zoom Meeting: 250 | 251 | Be sure to edit the IP address (from Fargate task), meeting ID, and passcode 252 | 253 | ```json 254 | { 255 | "ip_address": "10.0.0.10", 256 | "path": "join", 257 | "meeting_id": "1111111111", 258 | "meeting_passcode": "aaaaaa", 259 | "display_name": "vpf-on-fargate-1" 260 | } 261 | ``` 262 | 263 | 5. After the virtual participant has joined, allow the virtual participant to locally record the meeting. 264 | 265 | ## Run Amazon Transcribe Tester NodeJS app 266 | 267 | To see live transcription of Zoom Meeting audio and verify transcription, run the tester app in the [transcribe_tester_app](src/transcribe_tester_app/) folder. See [transcribe_tester_app/README.md](src/transcribe_tester_app/README.md) for details. 268 | 269 | ## Clean Up 270 | 271 | To cleanup complete these tasks: 272 | 273 | 1. To leave the meeting, use the following payload for the FargateClientLambda lambda function. 274 | 275 | Be sure to edit the IP address (from Fargate task), meeting ID, and passcode 276 | 277 | ```json 278 | { 279 | "ip_address": "10.0.0.10", 280 | "path": "leave", 281 | "meeting_id": "1111111111", 282 | "meeting_passcode": "aaaaaa", 283 | "display_name": "vpf-on-fargate-1" 284 | } 285 | ``` 286 | 287 | 2. Remove the CDK stack with the `cdk destroy` command 288 | 289 | ___ 290 | 🟥 If `cdk deploy` fails, you must delete the AWS CloudFormation stacks manually in the following sequence (most recent to oldest) starting deletion of one stack only after the previous stack is successfully deleted: _1/_ **PipelineAppStage-PipelineAppStack** _2/_ **PipelineAppStage-WindowsContainerPipelineStack** _3/_ **PipelineAppStage-CodePipelineCustomActionStack** _4/_ **ZoomMeetingBotCdkStack** _5/_ **CDKToolkit**. 291 | 292 | If you accidentally hit delete on all the stacks same time, or did not follow the one step process above, the CloudFormation stacks enter a bad state. This prevents some of stacks from being deleted. Deleting stacks out of sequence causes an IAM Role which stacks depend on to be disposed prematurely. To resolve, first manually re-create the IAM role: `cdk-hnb659fds-cfn-exec-role--` (replacing aws account number and region). Then provide assume rights to CloudFormation via the role's "Trust relationships" tab in the AWS IAM console Role view. Then assign `AdministratorAccess` to the role in console view's "Permissions" tab. Finally delete the stacks following the remaining sequence mentioned above. 293 | ____ 294 | 295 | 3. Cleanup the s3 bucket and remove it 296 | 4. Delete AWS CodeCommit repository 297 | 5. Remove secrets stored in AWS Secret Manager 298 | 6. Delete the IAM user with Amazon KVS and Amazon SNS access policy 299 | 300 | 301 | ## Credits 302 | 303 | This prototype includes modified CloudFormation templates from the [open source repository](https://github.com/aws-samples/aws-codepipeline-custom-action) featured in the AWS blog post, ["Building Windows containers with AWS CodePipeline and custom actions"](https://aws.amazon.com/blogs/devops/building-windows-containers-with-aws-codepipeline-and-custom-actions/). 304 | 305 | We'd like to thank the Zoom Developer Platform team for their support and guidance in building this solution. 306 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/raw-templates/codepipeline-custom-action-template.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | AWSTemplateFormatVersion: 2010-09-09 5 | Transform: "AWS::Serverless-2016-10-31" 6 | Description: Custom AWS CodePipeline action that enables EC2 build nodes. 7 | Metadata: 8 | AWS::CloudFormation::Interface: 9 | ParameterGroups: 10 | - Label: 11 | default: "General" 12 | Parameters: 13 | - ProjectId 14 | - Label: 15 | default: "Custom Action Settings" 16 | Parameters: 17 | - CustomActionProviderName 18 | - CustomActionProviderCategory 19 | - CustomActionProviderVersion 20 | ParameterLabels: 21 | ProjectId: 22 | default: "Project ID" 23 | CustomActionProviderName: 24 | default: "Custom Action Provider Name" 25 | CustomActionProviderCategory: 26 | default: "Custom Action Provider Category" 27 | CustomActionProviderVersion: 28 | default: "Custom Action Provider Version" 29 | 30 | Parameters: 31 | # You can provide these parameters in your CreateProject API call. 32 | ProjectId: 33 | Type: String 34 | Description: Prefix that will be used for AWS resources generated by the template. 35 | Default: ec2-codepipeline-builders 36 | 37 | CustomActionProviderName: 38 | Type: String 39 | Description: Name of the custom action provider (used in CodePipeline Console UI). 40 | Default: EC2-CodePipeline-Builder 41 | 42 | CustomActionProviderCategory: 43 | Type: String 44 | Description: Category of the custom action provider (used in CodePipeline Console UI). 45 | AllowedValues: 46 | - Build 47 | - Deploy 48 | - Invoke 49 | - Test 50 | Default: Build 51 | 52 | CustomActionProviderVersion: 53 | Type: String 54 | Description: Version of the custom action provider (used in CodePipeline Console UI). 55 | 56 | Resources: 57 | # Supporting Lambda Functions 58 | JobCompletionHandler: 59 | Type: AWS::Serverless::Function 60 | Properties: 61 | Description: Handles result of job flow execution. 62 | CodeUri: lambda/job-completion-handler 63 | Handler: lambda.lambda_handler 64 | Runtime: python3.7 65 | Role: !GetAtt JobCompletionHandlerExecutionRole.Arn 66 | MemorySize: 128 67 | Timeout: 15 68 | 69 | InstanceApi: 70 | Type: AWS::Serverless::Function 71 | Properties: 72 | Description: Manages EC2 instances that carry out custom action jobs. 73 | CodeUri: lambda/instance-api 74 | Handler: lambda.lambda_handler 75 | Runtime: python3.7 76 | Role: !GetAtt InstanceApiExecutionRole.Arn 77 | MemorySize: 128 78 | Timeout: 15 79 | Environment: 80 | Variables: 81 | BUILDER_INSTANCE_PROFILE_ARN: !Sub "${Ec2BuilderInstanceProfile.Arn}" 82 | 83 | JobApi: 84 | Type: AWS::Serverless::Function 85 | Properties: 86 | Description: Runs and tracks SSM commands on EC2 instances. 87 | CodeUri: lambda/job-api 88 | Handler: lambda.lambda_handler 89 | Runtime: python3.7 90 | Role: !GetAtt JobApiExecutionRole.Arn 91 | MemorySize: 128 92 | Timeout: 15 93 | Environment: 94 | Variables: 95 | SSM_DOCUMENT_NAME: !Ref RunBuildJobOnEc2Instance 96 | 97 | # CodePipeline Polling Function 98 | CodePipelinePoller: 99 | Type: AWS::Serverless::Function 100 | Properties: 101 | Description: Polls CodePipeline for Custom Actions. 102 | CodeUri: lambda/poller 103 | Handler: lambda.lambda_handler 104 | Runtime: python3.7 105 | Role: !GetAtt CodePipelinePollerExecutionRole.Arn 106 | MemorySize: 128 107 | Timeout: 15 108 | Environment: 109 | Variables: 110 | STATE_MACHINE_ARN: !Ref Ec2BuilderStateMachine 111 | CUSTOM_ACTION_PROVIDER_NAME: !Ref CustomActionProviderName 112 | CUSTOM_ACTION_PROVIDER_CATEGORY: !Ref CustomActionProviderCategory 113 | CUSTOM_ACTION_PROVIDER_VERSION: !Ref CustomActionProviderVersion 114 | Events: 115 | # This event is used to react on started instances of the Custom Action 116 | CodePipelineActionStartedEvent: 117 | Type: CloudWatchEvent 118 | Properties: 119 | Pattern: 120 | source: 121 | - "aws.codepipeline" 122 | detail-type: 123 | - "CodePipeline Action Execution State Change" 124 | detail: 125 | state: 126 | - "STARTED" 127 | # This event is needed to make Custom Actions as completed once build is done. 128 | # TODO: replace with custom CloudWatch event at the end of Step Functions flow 129 | CheckCodePipelineScheduledEvent: 130 | Type: Schedule 131 | Properties: 132 | Schedule: rate(1 minute) 133 | 134 | # CodePipeline Custom Action 135 | Ec2BuildActionType: 136 | Type: AWS::CodePipeline::CustomActionType 137 | Properties: 138 | Category: !Ref CustomActionProviderCategory 139 | Provider: !Ref CustomActionProviderName 140 | Version: !Ref CustomActionProviderVersion 141 | ConfigurationProperties: 142 | - Name: ImageId 143 | Description: AMI to use for EC2 build instances. 144 | Key: true 145 | Required: true 146 | Secret: false 147 | Queryable: false 148 | Type: String 149 | - Name: InstanceType 150 | Description: Instance type for EC2 build instances. 151 | Key: true 152 | Required: true 153 | Secret: false 154 | Queryable: false 155 | Type: String 156 | - Name: Command 157 | Description: Command(s) to execute. 158 | Key: true 159 | Required: true 160 | Secret: false 161 | Queryable: false 162 | Type: String 163 | - Name: WorkingDirectory 164 | Description: Working directory for the command to execute. 165 | Key: true 166 | Required: false 167 | Secret: false 168 | Queryable: false 169 | Type: String 170 | - Name: OutputArtifactPath 171 | Description: Path of the file(-s) or directory(-es) to use as custom action output artifact. 172 | Key: true 173 | Required: false 174 | Secret: false 175 | Queryable: false 176 | Type: String 177 | InputArtifactDetails: 178 | MaximumCount: 1 179 | MinimumCount: 0 180 | OutputArtifactDetails: 181 | MaximumCount: 1 182 | MinimumCount: 0 183 | Settings: 184 | EntityUrlTemplate: !Sub "https://${AWS::Region}.console.aws.amazon.com/systems-manager/documents/${RunBuildJobOnEc2Instance}" 185 | ExecutionUrlTemplate: !Sub "https://${AWS::Region}.console.aws.amazon.com/states/home#/executions/details/{ExternalExecutionId}" 186 | 187 | # SSM Document 188 | RunBuildJobOnEc2Instance: 189 | Type: "AWS::SSM::Document" 190 | Properties: 191 | DocumentType: Command 192 | Content: 193 | schemaVersion: "2.2" 194 | description: Downloads build artifacts from S3 and runs specified build scripts. 195 | parameters: 196 | inputBucketName: 197 | description: "(Required) Specify the S3 bucket name of the input artifact." 198 | type: String 199 | default: "" 200 | maxChars: 4096 201 | inputObjectKey: 202 | description: "(Required) Specify the S3 objectKey of the input artifact." 203 | type: String 204 | default: "" 205 | maxChars: 4096 206 | commands: 207 | description: "(Required) Specify the commands to run or the paths to existing scripts on the instance." 208 | type: String 209 | displayType: textarea 210 | executionId: 211 | description: "(Required) Specify the pipeline execution ID" 212 | type: String 213 | default: "" 214 | maxChars: 4096 215 | pipelineArn: 216 | description: "(Required) Specify the pipeline ARN" 217 | type: String 218 | default: "" 219 | maxChars: 4096 220 | pipelineName: 221 | description: "(Required) Specify the pipeline Name" 222 | type: String 223 | default: "" 224 | maxChars: 4096 225 | workingDirectory: 226 | type: String 227 | default: "" 228 | description: "(Optional) The path where the content will be downloaded and executed from on your instance." 229 | maxChars: 4096 230 | outputArtifactPath: 231 | type: String 232 | default: "" 233 | description: "(Optional) The path of the output artifact to upload to S3." 234 | maxChars: 4096 235 | outputBucketName: 236 | description: "(Optional) Specify the S3 bucket name of the output artifact." 237 | type: String 238 | default: "" 239 | maxChars: 4096 240 | outputObjectKey: 241 | description: "(Optional) Specify the S3 objectKey of the output artifact." 242 | type: String 243 | default: "" 244 | maxChars: 4096 245 | executionTimeout: 246 | description: 247 | "(Optional) The time in seconds for a command to complete before 248 | it is considered to have failed. Default is 3600 (1 hour). Maximum is 28800 249 | (8 hours)." 250 | type: String 251 | default: "28800" 252 | allowedPattern: "([1-9][0-9]{0,3})|(1[0-9]{1,4})|(2[0-7][0-9]{1,3})|(28[0-7][0-9]{1,2})|(28800)" 253 | 254 | mainSteps: 255 | # Windows steps 256 | - name: windows_script 257 | precondition: 258 | StringEquals: [platformType, Windows] 259 | action: aws:runPowerShellScript 260 | inputs: 261 | runCommand: 262 | # Ensure that if a command fails the script does not proceed to the following commands 263 | - '$ErrorActionPreference = "Stop"' 264 | - '$ENV:PipelineExecutionId = "{{ executionId }}"' 265 | - '$ENV:PipelineArn = "{{ pipelineArn }}"' 266 | - '$ENV:PipelineName = "{{ pipelineName }}"' 267 | 268 | - '$jobDirectory = "{{ workingDirectory }}"' 269 | # Create temporary folder for build artifacts, if not provided 270 | - "if ([string]::IsNullOrEmpty($jobDirectory)) {" 271 | - " $parent = [System.IO.Path]::GetTempPath()" 272 | - " [string] $name = [System.Guid]::NewGuid()" 273 | - " $jobDirectory = (Join-Path $parent $name)" 274 | - " New-Item -ItemType Directory -Path $jobDirectory" 275 | # Set current location to the new folder 276 | - " Set-Location -Path $jobDirectory" 277 | - "}" 278 | 279 | # Download/unzip input artifact 280 | - "Read-S3Object -BucketName {{ inputBucketName }} -Key {{ inputObjectKey }} -File artifact.zip" 281 | - "Expand-Archive -Path artifact.zip -DestinationPath ." 282 | 283 | # Run the build commands 284 | - "$directory = Convert-Path ." 285 | - '$env:PATH += ";$directory"' 286 | - "{{ commands }}" 287 | # We need to check exit code explicitly here 288 | - "if (-not ($?)) { exit $LASTEXITCODE }" 289 | 290 | # Compress output artifacts, if specified 291 | - '$outputArtifactPath = "{{ outputArtifactPath }}"' 292 | - "if ($outputArtifactPath) {" 293 | - " Compress-Archive -Path $outputArtifactPath -DestinationPath output-artifact.zip" 294 | # Upload compressed artifact to S3 295 | - ' $bucketName = "{{ outputBucketName }}"' 296 | - ' $objectKey = "{{ outputObjectKey }}"' 297 | - " if ($bucketName -and $objectKey) {" 298 | # Don't forget to encrypt the artifact - CodePipeline bucket has a policy to enforce this 299 | - " Write-S3Object -BucketName $bucketName -Key $objectKey -File output-artifact.zip -ServerSideEncryption aws:kms" 300 | - " }" 301 | - "}" 302 | workingDirectory: "{{ workingDirectory }}" 303 | timeoutSeconds: "{{ executionTimeout }}" 304 | 305 | # Step Functions Flow 306 | Ec2BuilderStateMachine: 307 | Type: AWS::StepFunctions::StateMachine 308 | Properties: 309 | StateMachineName: !Sub "${ProjectId}-build-flow" 310 | DefinitionString: !Sub |- 311 | { 312 | "Comment": "An example of the Amazon States Language that runs an AWS Batch job and monitors the job until it completes.", 313 | "StartAt": "Acquire Builder Flow", 314 | "States": { 315 | "Acquire Builder Flow": { 316 | "Type": "Parallel", 317 | "Branches": [ 318 | { 319 | "StartAt": "Start EC2 Instance", 320 | "States": { 321 | "Start EC2 Instance": { 322 | "Type": "Task", 323 | "Resource": "${InstanceApi.Arn}", 324 | "InputPath": "$.params.instance", 325 | "ResultPath": "$.status.instance", 326 | "Parameters": { 327 | "command": "start", 328 | "imageId.$": "$.imageId", 329 | "instanceType.$": "$.instanceType", 330 | "keyName.$": "$.keyName" 331 | }, 332 | "Next": "Wait Start" 333 | }, 334 | "Wait Start": { 335 | "Type": "Wait", 336 | "Seconds": 30, 337 | "Next": "Check Builder Start Status" 338 | }, 339 | "Check Builder Start Status": { 340 | "Type": "Task", 341 | "Resource": "${InstanceApi.Arn}", 342 | "InputPath": "$.status.instance", 343 | "ResultPath": "$.status.instance", 344 | "Parameters": { 345 | "command": "status_ssm", 346 | "instanceId.$": "$.instanceId" 347 | }, 348 | "Next": "Builder Started?" 349 | }, 350 | "Builder Started?": { 351 | "Type": "Choice", 352 | "Choices": [ 353 | { 354 | "Variable": "$.status.instance.status", 355 | "StringEquals": "STARTED", 356 | "Next": "Started" 357 | }, 358 | { 359 | "Variable": "$.status.instance.status", 360 | "StringEquals": "STOPPED", 361 | "Next": "Failed to Start" 362 | } 363 | ], 364 | "Default": "Wait Start" 365 | }, 366 | "Started": { 367 | "Type": "Pass", 368 | "End": true 369 | }, 370 | "Failed to Start": { 371 | "Type": "Fail", 372 | "Error": "StartError", 373 | "Cause": "EC2 instance failed to start." 374 | } 375 | } 376 | } 377 | ], 378 | "Retry": [ 379 | { 380 | "ErrorEquals": [ 381 | "Lambda.ServiceException", 382 | "Lambda.AWSLambdaException", 383 | "Lambda.SdkClientException" 384 | ], 385 | "IntervalSeconds": 2, 386 | "MaxAttempts": 3, 387 | "BackoffRate": 2 388 | } 389 | ], 390 | "Catch": [ 391 | { 392 | "ErrorEquals": [ 393 | "States.ALL" 394 | ], 395 | "ResultPath": "$.errorDetails", 396 | "Next": "Report Completion" 397 | } 398 | ], 399 | "OutputPath": "$[0]", 400 | "Next": "Run Command Flow" 401 | }, 402 | "Run Command Flow": { 403 | "Type": "Parallel", 404 | "Branches": [ 405 | { 406 | "StartAt": "Start Command Execution", 407 | "States": { 408 | "Start Command Execution": { 409 | "Type": "Task", 410 | "Resource": "${JobApi.Arn}", 411 | "ResultPath": "$.status.command", 412 | "Parameters": { 413 | "command": "run", 414 | "instanceId.$": "$.status.instance.instanceId", 415 | "commandText.$": "$.params.command.commandText", 416 | "workingDirectory.$": "$.params.command.workingDirectory", 417 | "timeout.$": "$.params.command.timeout", 418 | "inputBucketName.$": "$.params.artifacts.input.bucketName", 419 | "inputObjectKey.$": "$.params.artifacts.input.objectKey", 420 | "outputArtifactPath.$": "$.params.artifacts.output.path", 421 | "outputBucketName.$": "$.params.artifacts.output.bucketName", 422 | "outputObjectKey.$": "$.params.artifacts.output.objectKey", 423 | "executionId.$": "$.params.pipeline.executionId", 424 | "pipelineArn.$": "$.params.pipeline.arn", 425 | "pipelineName.$": "$.params.pipeline.name" 426 | }, 427 | "Next": "Wait Command Completion" 428 | }, 429 | "Wait Command Completion": { 430 | "Type": "Wait", 431 | "Seconds": 30, 432 | "Next": "Check Command Status" 433 | }, 434 | "Check Command Status": { 435 | "Type": "Task", 436 | "Resource": "${JobApi.Arn}", 437 | "ResultPath": "$.status.command", 438 | "Parameters": { 439 | "command": "status", 440 | "instanceId.$": "$.status.instance.instanceId", 441 | "commandId.$": "$.status.command.commandId" 442 | }, 443 | "Next": "Command Completed?" 444 | }, 445 | "Command Completed?": { 446 | "Type": "Choice", 447 | "Choices": [ 448 | { 449 | "Variable": "$.status.command.status", 450 | "StringEquals": "SUCCESS", 451 | "Next": "Completed" 452 | }, 453 | { 454 | "Variable": "$.status.command.status", 455 | "StringEquals": "FAILED", 456 | "Next": "Failed to Complete" 457 | } 458 | ], 459 | "Default": "Wait Command Completion" 460 | }, 461 | "Completed": { 462 | "Type": "Pass", 463 | "End": true 464 | }, 465 | "Failed to Complete": { 466 | "Type": "Fail", 467 | "Error": "RunCommandError", 468 | "Cause": "SSM command completed with failures." 469 | } 470 | } 471 | } 472 | ], 473 | "Retry": [ 474 | { 475 | "ErrorEquals": [ 476 | "Lambda.ServiceException", 477 | "Lambda.AWSLambdaException", 478 | "Lambda.SdkClientException" 479 | ], 480 | "IntervalSeconds": 2, 481 | "MaxAttempts": 3, 482 | "BackoffRate": 2 483 | } 484 | ], 485 | "Catch": [ 486 | { 487 | "ErrorEquals": [ 488 | "States.ALL" 489 | ], 490 | "ResultPath": "$.errorDetails", 491 | "Next": "Release Builder Flow" 492 | } 493 | ], 494 | "OutputPath": "$[0]", 495 | "Next": "Release Builder Flow" 496 | }, 497 | "Release Builder Flow": { 498 | "Type": "Parallel", 499 | "Branches": [ 500 | { 501 | "StartAt": "Stop EC2 Instance", 502 | "States": { 503 | "Stop EC2 Instance": { 504 | "Type": "Task", 505 | "Resource": "${InstanceApi.Arn}", 506 | "InputPath": "$.status.instance", 507 | "ResultPath": "$.status.instance", 508 | "Parameters": { 509 | "command": "stop", 510 | "instanceId.$": "$.instanceId" 511 | }, 512 | "Next": "Wait Stop" 513 | }, 514 | "Wait Stop": { 515 | "Type": "Wait", 516 | "Seconds": 30, 517 | "Next": "Check Builder Stop Status" 518 | }, 519 | "Check Builder Stop Status": { 520 | "Type": "Task", 521 | "Resource": "${InstanceApi.Arn}", 522 | "InputPath": "$.status.instance", 523 | "ResultPath": "$.status.instance", 524 | "Parameters": { 525 | "command": "status_ec2", 526 | "instanceId.$": "$.instanceId" 527 | }, 528 | "Next": "Builder Stopped?" 529 | }, 530 | "Builder Stopped?": { 531 | "Type": "Choice", 532 | "Choices": [ 533 | { 534 | "Variable": "$.status.instance.status", 535 | "StringEquals": "STOPPED", 536 | "Next": "Stopped" 537 | }, 538 | { 539 | "Variable": "$.status.instance.status", 540 | "StringEquals": "STARTED", 541 | "Next": "Failed to Stop" 542 | } 543 | ], 544 | "Default": "Wait Stop" 545 | }, 546 | "Stopped": { 547 | "Type": "Pass", 548 | "End": true 549 | }, 550 | "Failed to Stop": { 551 | "Type": "Fail", 552 | "Error": "StopError", 553 | "Cause": "EC2 instance failed to stop." 554 | } 555 | } 556 | } 557 | ], 558 | "Retry": [ 559 | { 560 | "ErrorEquals": [ 561 | "Lambda.ServiceException", 562 | "Lambda.AWSLambdaException", 563 | "Lambda.SdkClientException" 564 | ], 565 | "IntervalSeconds": 2, 566 | "MaxAttempts": 3, 567 | "BackoffRate": 2 568 | } 569 | ], 570 | "Catch": [ 571 | { 572 | "ErrorEquals": [ 573 | "States.ALL" 574 | ], 575 | "ResultPath": "$.errorDetails", 576 | "Next": "Report Completion" 577 | } 578 | ], 579 | "OutputPath": "$[0]", 580 | "Next": "Report Completion" 581 | }, 582 | "Report Completion": { 583 | "Type": "Task", 584 | "Resource": "${JobCompletionHandler.Arn}", 585 | "End": true 586 | } 587 | } 588 | } 589 | RoleArn: !GetAtt Ec2BuilderStateMachineExecutionRole.Arn 590 | 591 | # Lambda Roles 592 | 593 | JobCompletionHandlerExecutionRole: 594 | Type: AWS::IAM::Role 595 | Properties: 596 | AssumeRolePolicyDocument: 597 | Version: "2012-10-17" 598 | Statement: 599 | - Effect: Allow 600 | Principal: 601 | Service: 602 | - lambda.amazonaws.com 603 | Action: 604 | - sts:AssumeRole 605 | Path: "/" 606 | ManagedPolicyArns: 607 | - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 608 | 609 | CodePipelinePollerExecutionRole: 610 | Type: AWS::IAM::Role 611 | Properties: 612 | AssumeRolePolicyDocument: 613 | Version: "2012-10-17" 614 | Statement: 615 | - Effect: Allow 616 | Principal: 617 | Service: 618 | - lambda.amazonaws.com 619 | Action: 620 | - sts:AssumeRole 621 | Path: "/" 622 | ManagedPolicyArns: 623 | - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 624 | Policies: 625 | - PolicyName: root 626 | PolicyDocument: 627 | Version: "2012-10-17" 628 | Statement: 629 | - Effect: Allow 630 | Action: 631 | - codepipeline:PollForJobs 632 | - codepipeline:GetJobDetails 633 | - codepipeline:AcknowledgeJob 634 | - codepipeline:PutJobSuccessResult 635 | - codepipeline:PutJobFailureResult 636 | Resource: "*" 637 | - Effect: Allow 638 | Action: 639 | - states:DescribeExecution 640 | - states:StartExecution 641 | Resource: "*" 642 | 643 | InstanceApiExecutionRole: 644 | Type: AWS::IAM::Role 645 | Properties: 646 | AssumeRolePolicyDocument: 647 | Version: "2012-10-17" 648 | Statement: 649 | - Effect: Allow 650 | Principal: 651 | Service: 652 | - lambda.amazonaws.com 653 | Action: 654 | - sts:AssumeRole 655 | Path: "/" 656 | ManagedPolicyArns: 657 | - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 658 | Policies: 659 | - PolicyName: root 660 | PolicyDocument: 661 | Version: "2012-10-17" 662 | Statement: 663 | # This is necessary to start EC2 instance with Ec2BuilderRole 664 | - Effect: Allow 665 | Action: 666 | - iam:PassRole 667 | Resource: !GetAtt Ec2BuilderRole.Arn 668 | - Effect: Allow 669 | Action: 670 | - ec2:CreateTags 671 | - ec2:RunInstances 672 | - ec2:TerminateInstances 673 | - ec2:DescribeInstances 674 | - ec2:DescribeInstanceStatus 675 | Resource: "*" 676 | - Effect: Allow 677 | Action: 678 | - ssm:DescribeInstanceInformation 679 | Resource: "*" 680 | 681 | JobApiExecutionRole: 682 | Type: AWS::IAM::Role 683 | Properties: 684 | AssumeRolePolicyDocument: 685 | Version: "2012-10-17" 686 | Statement: 687 | - Effect: Allow 688 | Principal: 689 | Service: 690 | - lambda.amazonaws.com 691 | Action: 692 | - sts:AssumeRole 693 | Path: "/" 694 | ManagedPolicyArns: 695 | - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 696 | Policies: 697 | - PolicyName: root 698 | PolicyDocument: 699 | Version: "2012-10-17" 700 | Statement: 701 | - Effect: Allow 702 | Action: 703 | - ssm:SendCommand 704 | - ssm:ListCommands 705 | Resource: "*" 706 | 707 | # EC2 Role & Instance Profile 708 | 709 | Ec2BuilderRole: 710 | Type: AWS::IAM::Role 711 | Properties: 712 | AssumeRolePolicyDocument: 713 | Version: "2012-10-17" 714 | Statement: 715 | - Effect: Allow 716 | Principal: 717 | Service: 718 | - ec2.amazonaws.com 719 | Action: 720 | - sts:AssumeRole 721 | ManagedPolicyArns: 722 | # To be able to connect to SSM and execute commands 723 | - arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM 724 | # To be able to push docker images to ECR repositories 725 | - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser 726 | 727 | Ec2BuilderInstanceProfile: 728 | Type: AWS::IAM::InstanceProfile 729 | Properties: 730 | Roles: 731 | - !Ref Ec2BuilderRole 732 | 733 | # Step Functions Role 734 | 735 | Ec2BuilderStateMachineExecutionRole: 736 | Type: AWS::IAM::Role 737 | Properties: 738 | AssumeRolePolicyDocument: 739 | Version: "2012-10-17" 740 | Statement: 741 | - Effect: "Allow" 742 | Principal: 743 | Service: 744 | - !Sub states.${AWS::Region}.amazonaws.com 745 | Action: "sts:AssumeRole" 746 | Path: "/" 747 | Policies: 748 | - PolicyName: StatesExecutionPolicy 749 | PolicyDocument: 750 | Version: "2012-10-17" 751 | Statement: 752 | - Effect: Allow 753 | Action: 754 | - "lambda:InvokeFunction" 755 | # TODO: restrict resources 756 | Resource: "*" 757 | -------------------------------------------------------------------------------- /virtual-participant-orchestrator-for-zoom-meeting/docs/images/Zoom-Virtual-Participant.xml: -------------------------------------------------------------------------------- 1 | 2 | 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 --------------------------------------------------------------------------------