├── .nvmrc ├── .prettierignore ├── .eslintignore ├── app-architecture.png ├── .gitignore ├── lambda ├── constants.ts ├── listHandler.ts ├── util.ts ├── deleteHandler.ts ├── createHandler.ts ├── stageJoinHandler.ts ├── stageDisconnectHandler.ts └── stack.ts ├── .prettierrc.js ├── bin └── cdk.ts ├── src ├── sdk │ ├── video.ts │ ├── realtime.ts │ ├── room.ts │ └── ddb.ts ├── types.ts ├── disconnect.ts ├── tokens.ts ├── join.ts ├── delete.ts └── create.ts ├── tsconfig.json ├── package.json ├── cdk.json ├── architecture-description.md ├── .eslintrc.cjs └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/hydrogen -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build/ 2 | coverage/ 3 | dist/ 4 | node_modules/ 5 | public/ -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /coverage/ 3 | /dist/ 4 | /node_modules/ 5 | /public/ 6 | !.eslintrc.js 7 | !.prettierrc.js 8 | 9 | -------------------------------------------------------------------------------- /app-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-multi-host-serverless-demo/HEAD/app-architecture.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !jest.config.js 2 | *.d.ts 3 | node_modules 4 | 5 | # CDK asset staging directory 6 | .cdk.staging 7 | cdk.out 8 | 9 | # Misc 10 | **/.DS_Store 11 | .eslintcache 12 | .DS_Store 13 | log.txt 14 | npm-debug.log* -------------------------------------------------------------------------------- /lambda/constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | The DynamoDB Table Name. Must conform to the DynamoDB naming rules. 3 | For details, visit the following: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.NamingRules 4 | */ 5 | const DDBTableName = "AmazonIVSMultiHostDemoTable"; 6 | const ResourceTags = { AmazonIVSDemoResource: "AmazonIVSMultiHostResource" }; 7 | 8 | export { DDBTableName, ResourceTags }; 9 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "all", 3 | overrides: [ 4 | { 5 | files: ["*.ts", "*.tsx"], 6 | options: { parser: "babel-ts" }, 7 | }, 8 | { 9 | files: "*.js", 10 | options: { parser: "babel" }, 11 | }, 12 | { 13 | files: "*.json", 14 | options: { parser: "json" }, 15 | }, 16 | { 17 | files: "*.md", 18 | options: { 19 | parser: "markdown", 20 | }, 21 | }, 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /lambda/listHandler.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayEvent, APIGatewayProxyResult } from "aws-lambda"; 2 | 3 | import { scanGroups } from "../src/sdk/ddb"; 4 | import { createApiGwResponse } from "./util"; 5 | 6 | async function listHandler( 7 | event: APIGatewayEvent, 8 | ): Promise { 9 | let result; 10 | 11 | try { 12 | result = await scanGroups(); 13 | } catch (err) { 14 | return createApiGwResponse(400, { 15 | error: (err as Error).toString(), 16 | }); 17 | } 18 | 19 | return createApiGwResponse(200, result); 20 | } 21 | 22 | export { listHandler }; 23 | -------------------------------------------------------------------------------- /bin/cdk.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "source-map-support/register"; 3 | 4 | import { App } from "aws-cdk-lib"; 5 | 6 | import AmazonIVSMultiHostStack from "../lambda/stack"; 7 | 8 | const app = new App(); 9 | // eslint-disable-next-line no-new 10 | new AmazonIVSMultiHostStack(app, "AmazonIVSMultiHostServerlessStack", { 11 | /* Uncomment the next line to specialize this stack for the AWS Account 12 | * and Region that are implied by the current CLI configuration. */ 13 | env: { 14 | account: process.env.CDK_DEFAULT_ACCOUNT, 15 | region: process.env.CDK_DEFAULT_REGION, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /src/sdk/video.ts: -------------------------------------------------------------------------------- 1 | import { IVS } from "aws-sdk"; 2 | 3 | import { ResourceTags } from "../../lambda/constants"; 4 | 5 | const region = process.env.AWS_REGION; 6 | 7 | const ivsVideoClient = new IVS({ 8 | correctClockSkew: true, 9 | region, 10 | }); 11 | 12 | /** 13 | * IVS channels 14 | */ 15 | function createChannel() { 16 | return ivsVideoClient.createChannel({ tags: ResourceTags }).promise(); 17 | } 18 | 19 | function deleteChannel(arn: IVS.Types.ChannelArn) { 20 | return ivsVideoClient.deleteChannel({ arn }).promise(); 21 | } 22 | 23 | function stopStream(channelArn: IVS.Types.ChannelArn) { 24 | return ivsVideoClient.stopStream({ channelArn }).promise(); 25 | } 26 | 27 | export { createChannel, deleteChannel, ivsVideoClient, stopStream }; 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "alwaysStrict": true, 5 | "esModuleInterop": true, 6 | "experimentalDecorators": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "noEmit": true, 9 | "noErrorTruncation": true, 10 | "noImplicitAny": true, 11 | "noImplicitThis": true, 12 | "noImplicitReturns": true, 13 | "resolveJsonModule": true, 14 | "rootDir": ".", 15 | "skipLibCheck": true, 16 | "strict": true, 17 | "target": "es2020", 18 | "module": "commonjs", 19 | "moduleResolution": "node", 20 | "paths": { 21 | "*": ["./src/*", "./lambda/*"], 22 | } 23 | }, 24 | "exclude": [ 25 | "node_modules", 26 | "cdk.out" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /lambda/util.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyResult } from "aws-lambda"; 2 | 3 | function getResponseHeaders() { 4 | return { 5 | "Content-Type": "application/json", 6 | "Access-Control-Allow-Headers": "Content-Type", 7 | "Access-Control-Allow-Origin": "*", 8 | }; 9 | } 10 | 11 | function createApiGwResponse( 12 | statusCode: number, 13 | result: unknown, 14 | ): APIGatewayProxyResult { 15 | const headers = getResponseHeaders(); 16 | 17 | const body = JSON.stringify(result); 18 | 19 | return { 20 | headers, 21 | statusCode, 22 | body, 23 | }; 24 | } 25 | 26 | function isBodyMissingKey(body: unknown, key: string) { 27 | const keyExistsOnBody = Object.prototype.hasOwnProperty.call(body, key); 28 | 29 | return !keyExistsOnBody; 30 | } 31 | 32 | export { createApiGwResponse, getResponseHeaders, isBodyMissingKey }; 33 | -------------------------------------------------------------------------------- /lambda/deleteHandler.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayEvent, APIGatewayProxyResult } from "aws-lambda"; 2 | 3 | import { deleteGroupAndResources } from "../src/delete"; 4 | import { createApiGwResponse, isBodyMissingKey } from "./util"; 5 | 6 | async function deleteHandler( 7 | event: APIGatewayEvent, 8 | ): Promise { 9 | let body; 10 | let result; 11 | 12 | try { 13 | body = JSON.parse(event.body as string); 14 | } catch (err) { 15 | return createApiGwResponse(400, { 16 | error: `Failed to parse request body: ${(err as Error).toString()}`, 17 | }); 18 | } 19 | 20 | if (isBodyMissingKey(body, "groupId")) { 21 | return createApiGwResponse(400, { 22 | error: `Missing required parameter 'groupId'`, 23 | }); 24 | } 25 | 26 | try { 27 | await deleteGroupAndResources(body.groupId); 28 | result = ""; 29 | } catch (err) { 30 | return createApiGwResponse(400, { 31 | error: (err as Error).toString(), 32 | }); 33 | } 34 | 35 | return createApiGwResponse(200, result); 36 | } 37 | 38 | export { deleteHandler }; 39 | -------------------------------------------------------------------------------- /lambda/createHandler.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayEvent, APIGatewayProxyResult } from "aws-lambda"; 2 | 3 | import { create } from "../src/create"; 4 | import { createApiGwResponse, isBodyMissingKey } from "./util"; 5 | 6 | async function createHandler( 7 | event: APIGatewayEvent, 8 | ): Promise { 9 | let body; 10 | let result; 11 | 12 | try { 13 | body = JSON.parse(event.body as string); 14 | } catch (err) { 15 | return createApiGwResponse(400, { 16 | error: `Failed to parse request body: ${(err as Error).toString()}`, 17 | }); 18 | } 19 | 20 | if (isBodyMissingKey(body, "userId")) { 21 | return createApiGwResponse(400, { 22 | error: `Missing required parameter 'userId'`, 23 | }); 24 | } 25 | 26 | try { 27 | result = await create( 28 | body.groupId || "", 29 | body.userId, 30 | body.attributes || {}, 31 | ); 32 | } catch (err) { 33 | return createApiGwResponse(400, { 34 | error: (err as Error).toString(), 35 | }); 36 | } 37 | 38 | return createApiGwResponse(200, result); 39 | } 40 | 41 | export { createHandler }; 42 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { IVS, Ivschat, IVSRealTime } from "aws-sdk"; 2 | 3 | type UserAttributes = { 4 | avatarUrl: string; 5 | username: string; 6 | }; 7 | 8 | type ChannelResponse = { 9 | id: IVS.Types.ChannelArn; 10 | playbackUrl: IVS.Types.PlaybackURL; 11 | ingestEndpoint: IVS.Types.IngestEndpoint; 12 | streamKey: IVS.Types.StreamKeyValue; 13 | }; 14 | 15 | type RoomResponse = { 16 | id: Ivschat.Types.RoomIdentifier; 17 | token: Ivschat.Types.CreateChatTokenResponse; 18 | }; 19 | 20 | type StageResponse = { 21 | id: IVSRealTime.Types.StageArn; 22 | token: IVSRealTime.Types.ParticipantToken; 23 | }; 24 | 25 | type Headers = { 26 | "Content-Type": string; 27 | "Access-Control-Allow-Headers": string; 28 | "Access-Control-Allow-Origin": string; 29 | "Access-Control-Allow-Methods": string; 30 | }; 31 | 32 | type MultiHostGroup = { 33 | groupId: string; 34 | channelId: IVS.Types.ChannelArn; 35 | roomId: Ivschat.Types.RoomIdentifier; 36 | stageAttributes: UserAttributes; 37 | stageId: IVSRealTime.Types.StageArn; 38 | }; 39 | 40 | export { 41 | ChannelResponse, 42 | Headers, 43 | MultiHostGroup, 44 | RoomResponse, 45 | StageResponse, 46 | UserAttributes, 47 | }; 48 | -------------------------------------------------------------------------------- /src/disconnect.ts: -------------------------------------------------------------------------------- 1 | import { getGroup } from "./sdk/ddb"; 2 | import { disconnectParticipant } from "./sdk/realtime"; 3 | import { disconnectChatUser } from "./sdk/room"; 4 | 5 | /** 6 | * A function that disconnects a user from a group's stage and chat room 7 | */ 8 | 9 | async function disconnect( 10 | groupId: string, 11 | userId: string, 12 | participantId: string, 13 | reason: string, 14 | ) { 15 | let roomId; 16 | let stageId; 17 | 18 | // Find resources 19 | try { 20 | ({ stageId, roomId } = await getGroup(groupId)); 21 | } catch (err) { 22 | throw new Error( 23 | `Failed to find associated resources: ${(err as Error).toString()}`, 24 | ); 25 | } 26 | 27 | // Disconnect from stage 28 | try { 29 | await disconnectParticipant(stageId as string, participantId, reason); 30 | } catch (err) { 31 | throw new Error( 32 | `Failed to disconnect from stage: ${(err as Error).toString()}`, 33 | ); 34 | } 35 | 36 | // Disconnect from chat 37 | try { 38 | await disconnectChatUser(roomId as string, userId, reason); 39 | } catch (err) { 40 | throw new Error( 41 | `Failed to disconnect from room: ${(err as Error).toString()}`, 42 | ); 43 | } 44 | 45 | return { 46 | status: "success", 47 | }; 48 | } 49 | 50 | // eslint-disable-next-line import/prefer-default-export 51 | export { disconnect }; 52 | -------------------------------------------------------------------------------- /src/tokens.ts: -------------------------------------------------------------------------------- 1 | import { Ivschat, IVSRealTime } from "aws-sdk"; 2 | 3 | import { createStageToken } from "./sdk/realtime"; 4 | import { createChatToken } from "./sdk/room"; 5 | import { UserAttributes } from "./types"; 6 | 7 | /** 8 | * A function that creates a stage and chat room token 9 | */ 10 | 11 | async function createTokens( 12 | roomId: Ivschat.Types.RoomArn, 13 | stageId: IVSRealTime.Types.StageArn, 14 | userId: string, 15 | attributes: UserAttributes, 16 | ) { 17 | let chatTokenData: Ivschat.Types.CreateChatTokenResponse; 18 | let stageTokenData: IVSRealTime.Types.ParticipantToken; 19 | const isHost = "true"; 20 | 21 | try { 22 | chatTokenData = await createChatToken(roomId, userId, isHost, attributes); 23 | } catch (err) { 24 | throw new Error( 25 | `Failed to create chat token: ${(err as Error).toString()}`, 26 | ); 27 | } 28 | 29 | try { 30 | stageTokenData = await createStageToken(stageId, { 31 | userId, 32 | attributes: { 33 | ...attributes, 34 | isHost, 35 | }, 36 | }); 37 | } catch (err) { 38 | throw new Error( 39 | `Failed to create stage participant token: ${(err as Error).toString()}`, 40 | ); 41 | } 42 | 43 | return { 44 | chatTokenData, 45 | stageTokenData, 46 | }; 47 | } 48 | 49 | // eslint-disable-next-line import/prefer-default-export 50 | export { createTokens }; 51 | -------------------------------------------------------------------------------- /lambda/stageJoinHandler.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayEvent, APIGatewayProxyResult } from "aws-lambda"; 2 | 3 | import { join } from "../src/join"; 4 | import { createApiGwResponse, isBodyMissingKey } from "./util"; 5 | 6 | async function stageJoinHandler( 7 | event: APIGatewayEvent, 8 | ): Promise { 9 | let body; 10 | let result; 11 | 12 | try { 13 | body = JSON.parse(event.body as string); 14 | } catch (err) { 15 | return createApiGwResponse(400, { 16 | error: `Failed to parse request body: ${(err as Error).toString()}`, 17 | }); 18 | } 19 | 20 | if (isBodyMissingKey(body, "groupId")) { 21 | return createApiGwResponse(400, { 22 | error: `Missing required parameter 'groupId'`, 23 | }); 24 | } 25 | 26 | if (isBodyMissingKey(body, "userId")) { 27 | return createApiGwResponse(400, { 28 | error: `Missing required parameter 'userId'`, 29 | }); 30 | } 31 | 32 | if (isBodyMissingKey(body, "attributes")) { 33 | return createApiGwResponse(400, { 34 | error: `Missing required parameter 'attributes'`, 35 | }); 36 | } 37 | 38 | try { 39 | result = await join(body.groupId, body.userId, body.attributes || {}); 40 | } catch (err) { 41 | return createApiGwResponse(400, { 42 | error: (err as Error).toString(), 43 | }); 44 | } 45 | 46 | return createApiGwResponse(200, result); 47 | } 48 | 49 | export { stageJoinHandler }; 50 | -------------------------------------------------------------------------------- /src/sdk/realtime.ts: -------------------------------------------------------------------------------- 1 | import { IVSRealTime } from "aws-sdk"; 2 | 3 | import { ResourceTags } from "../../lambda/constants"; 4 | 5 | const region = process.env.AWS_REGION; 6 | 7 | const ivsRealtimeClient = new IVSRealTime({ 8 | correctClockSkew: true, 9 | region, 10 | }); 11 | 12 | /** 13 | * IVS stages 14 | */ 15 | async function createStage() { 16 | const { stage } = await ivsRealtimeClient 17 | .createStage({ tags: ResourceTags }) 18 | .promise(); 19 | return stage as Required; 20 | } 21 | 22 | async function createStageToken( 23 | stageArn: IVSRealTime.Types.StageArn, 24 | participant: IVSRealTime.Types.ParticipantTokenConfiguration, 25 | ) { 26 | const { participantToken } = await ivsRealtimeClient 27 | .createParticipantToken({ stageArn, ...participant }) 28 | .promise(); 29 | return participantToken as Required; 30 | } 31 | 32 | function deleteStage(arn: IVSRealTime.Types.StageArn) { 33 | return ivsRealtimeClient.deleteStage({ arn }).promise(); 34 | } 35 | 36 | function disconnectParticipant( 37 | stageArn: IVSRealTime.Types.StageArn, 38 | participantId: IVSRealTime.Types.ParticipantTokenUserId, 39 | reason: IVSRealTime.Types.DisconnectParticipantReason, 40 | ) { 41 | return ivsRealtimeClient 42 | .disconnectParticipant({ 43 | stageArn, 44 | participantId, 45 | reason, 46 | }) 47 | .promise(); 48 | } 49 | 50 | export { createStage, createStageToken, deleteStage, disconnectParticipant }; 51 | -------------------------------------------------------------------------------- /lambda/stageDisconnectHandler.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayEvent, APIGatewayProxyResult } from "aws-lambda"; 2 | 3 | import { disconnect } from "../src/disconnect"; 4 | import { createApiGwResponse, isBodyMissingKey } from "./util"; 5 | 6 | async function stageDisconnectHandler( 7 | event: APIGatewayEvent, 8 | ): Promise { 9 | let body; 10 | let result; 11 | 12 | try { 13 | body = JSON.parse(event.body as string); 14 | } catch (err) { 15 | return createApiGwResponse(400, { 16 | error: `Failed to parse request body: ${(err as Error).toString()}`, 17 | }); 18 | } 19 | 20 | if (isBodyMissingKey(body, "groupId")) { 21 | return createApiGwResponse(400, { 22 | error: `Missing required parameter 'groupId'`, 23 | }); 24 | } 25 | 26 | if (isBodyMissingKey(body, "userId")) { 27 | return createApiGwResponse(400, { 28 | error: `Missing required parameter 'userId'`, 29 | }); 30 | } 31 | 32 | if (isBodyMissingKey(body, "participantId")) { 33 | return createApiGwResponse(400, { 34 | error: `Missing required parameter 'participantId'`, 35 | }); 36 | } 37 | 38 | try { 39 | await disconnect( 40 | body.groupId, 41 | body.userId, 42 | body.participantId, 43 | body.reason || "", 44 | ); 45 | result = ""; 46 | } catch (err) { 47 | return createApiGwResponse(400, { 48 | error: (err as Error).toString(), 49 | }); 50 | } 51 | 52 | return createApiGwResponse(200, result); 53 | } 54 | 55 | export { stageDisconnectHandler }; 56 | -------------------------------------------------------------------------------- /src/sdk/room.ts: -------------------------------------------------------------------------------- 1 | import { Ivschat } from "aws-sdk"; 2 | 3 | import { ResourceTags } from "../../lambda/constants"; 4 | import { UserAttributes } from "../types"; 5 | 6 | const region = process.env.AWS_REGION; 7 | 8 | const ivsChatClient = new Ivschat({ 9 | correctClockSkew: true, 10 | region, 11 | }); 12 | 13 | function createRoom() { 14 | return ivsChatClient.createRoom({ tags: ResourceTags }).promise(); 15 | } 16 | 17 | async function createChatToken( 18 | roomIdentifier: Ivschat.Types.RoomArn, 19 | userId: string, 20 | isHost: string, 21 | attributes: UserAttributes, 22 | ) { 23 | // If the user is the host, provide additional capabilities 24 | const capabilities = 25 | isHost === "true" 26 | ? ["SEND_MESSAGE", "DELETE_MESSAGE", "DISCONNECT_USER"] 27 | : ["SEND_MESSAGE"]; 28 | 29 | const token = await ivsChatClient 30 | .createChatToken({ 31 | capabilities, 32 | roomIdentifier, 33 | userId, 34 | attributes: { 35 | ...attributes, 36 | isHost, 37 | }, 38 | }) 39 | .promise(); 40 | 41 | return token as Ivschat.Types.CreateChatTokenResponse; 42 | } 43 | 44 | function disconnectChatUser( 45 | roomIdentifier: Ivschat.Types.RoomArn, 46 | userId: string, 47 | reason: string, 48 | ) { 49 | return ivsChatClient 50 | .disconnectUser({ 51 | roomIdentifier, 52 | userId, 53 | reason, 54 | }) 55 | .promise(); 56 | } 57 | 58 | function deleteRoom(identifier: Ivschat.Types.RoomIdentifier) { 59 | return ivsChatClient.deleteRoom({ identifier }).promise(); 60 | } 61 | 62 | export { 63 | createChatToken, 64 | createRoom, 65 | deleteRoom, 66 | disconnectChatUser, 67 | ivsChatClient, 68 | }; 69 | -------------------------------------------------------------------------------- /src/join.ts: -------------------------------------------------------------------------------- 1 | import { Ivschat, IVSRealTime } from "aws-sdk"; 2 | 3 | import { getGroup } from "./sdk/ddb"; 4 | import { createTokens } from "./tokens"; 5 | import { RoomResponse, StageResponse, UserAttributes } from "./types"; 6 | 7 | /** 8 | * A function that creates creates a stage token and chat token for the 9 | * stage and room associated with the provided `groupId` 10 | */ 11 | 12 | async function join( 13 | groupId: string, 14 | userId: string, 15 | attributes: UserAttributes, 16 | ) { 17 | let roomResponse: RoomResponse; 18 | let stageResponse: StageResponse; 19 | 20 | // Find resources 21 | try { 22 | const { stageId, roomId } = await getGroup(groupId); 23 | stageResponse = { 24 | id: stageId as IVSRealTime.Types.StageArn, 25 | token: "" as IVSRealTime.Types.ParticipantToken, 26 | }; 27 | roomResponse = { 28 | id: roomId as Ivschat.Types.RoomIdentifier, 29 | token: "" as Ivschat.Types.CreateChatTokenResponse, 30 | }; 31 | } catch (err) { 32 | throw new Error( 33 | `Failed to find associated resources: ${(err as Error).toString()}`, 34 | ); 35 | } 36 | 37 | // Create tokens 38 | try { 39 | const { chatTokenData, stageTokenData } = await createTokens( 40 | roomResponse.id as string, 41 | stageResponse.id as string, 42 | userId as string, 43 | attributes as UserAttributes, 44 | ); 45 | roomResponse.token = chatTokenData; 46 | stageResponse.token = stageTokenData; 47 | } catch (err) { 48 | throw new Error(`Failed to create tokens: ${(err as Error).toString()}`); 49 | } 50 | 51 | return { 52 | chat: roomResponse, 53 | stage: stageResponse, 54 | }; 55 | } 56 | 57 | // eslint-disable-next-line import/prefer-default-export 58 | export { join }; 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@amzn/multi-host-serverless", 3 | "version": "1.0.0", 4 | "description": "Amazon Interactive Video Service Multi-host Serverless", 5 | "bin": { 6 | "cdk": "bin/cdk.js" 7 | }, 8 | "scripts": { 9 | "build": "tsc", 10 | "watch": "tsc -w", 11 | "lint": "tsc --project ./", 12 | "cdk": "cdk", 13 | "bootstrap": "cdk bootstrap", 14 | "deploy": "cdk deploy", 15 | "destroy": "cdk destroy" 16 | }, 17 | "devDependencies": { 18 | "@types/aws-lambda": "^8.10.111", 19 | "@types/jest": "^29.4.0", 20 | "@types/node": "18.14.6", 21 | "@typescript-eslint/eslint-plugin": "^5.56.0", 22 | "@typescript-eslint/parser": "^5.56.0", 23 | "aws-cdk": "^2.1004.0", 24 | "aws-lambda": "^1.0.7", 25 | "aws-sdk": "^2.1354.0", 26 | "esbuild": "^0.25.0", 27 | "eslint": "^8.36.0", 28 | "eslint-config-airbnb": "^19.0.4", 29 | "eslint-config-prettier": "^8.7.0", 30 | "eslint-import-resolver-typescript": "^3.5.3", 31 | "eslint-plugin-babel": "^5.3.1", 32 | "eslint-plugin-cypress": "^2.12.1", 33 | "eslint-plugin-formatjs": "^4.9.0", 34 | "eslint-plugin-import": "^2.27.5", 35 | "eslint-plugin-jest": "^27.2.1", 36 | "eslint-plugin-json": "^3.1.0", 37 | "eslint-plugin-jsx-a11y": "^6.7.1", 38 | "eslint-plugin-lodash": "^7.4.0", 39 | "eslint-plugin-markdown": "^3.0.0", 40 | "eslint-plugin-prettier": "^4.2.1", 41 | "eslint-plugin-promise": "^6.1.1", 42 | "eslint-plugin-simple-import-sort": "^10.0.0", 43 | "jest": "^29.5.0", 44 | "ts-jest": "^29.0.5", 45 | "ts-node": "^10.9.1", 46 | "typescript": "~4.9.5" 47 | }, 48 | "dependencies": { 49 | "aws-cdk-lib": "2.189.1", 50 | "constructs": "^10.0.0", 51 | "source-map-support": "^0.5.21" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/cdk.ts", 3 | "watch": { 4 | "include": ["**"], 5 | "exclude": [ 6 | "README.md", 7 | "cdk*.json", 8 | "**/*.d.ts", 9 | "**/*.js", 10 | "tsconfig.json", 11 | "package*.json", 12 | "yarn.lock", 13 | "node_modules", 14 | "test" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 19 | "@aws-cdk/core:checkSecretUsage": true, 20 | "@aws-cdk/core:target-partitions": ["aws", "aws-cn"], 21 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 22 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 23 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 24 | "@aws-cdk/aws-iam:minimizePolicies": true, 25 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 26 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 27 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 28 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 29 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 30 | "@aws-cdk/core:enablePartitionLiterals": true, 31 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 32 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 33 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 34 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 35 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 36 | "@aws-cdk/aws-route53-patters:useCertificate": true, 37 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 38 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 39 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 40 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 41 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 42 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 43 | "@aws-cdk/aws-redshift:columnId": true 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/sdk/ddb.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDB, IVS, Ivschat, IVSRealTime } from "aws-sdk"; 2 | 3 | import { MultiHostGroup, UserAttributes } from "../types"; 4 | 5 | const region = process.env.AWS_REGION; 6 | const { TABLE_NAME } = process.env; 7 | 8 | const ddbClient = new DynamoDB.DocumentClient({ 9 | correctClockSkew: true, 10 | region, 11 | }); 12 | 13 | async function scanGroups() { 14 | let tableData: DynamoDB.Types.ScanOutput; 15 | 16 | try { 17 | tableData = await ddbClient 18 | .scan({ TableName: TABLE_NAME as string, Select: "ALL_ATTRIBUTES" }) 19 | .promise(); 20 | } catch (err) { 21 | throw new Error(`Failed to get stage data: ${(err as Error).toString()}`); 22 | } 23 | 24 | return tableData.Items as MultiHostGroup[]; 25 | } 26 | 27 | async function getGroup(groupId: string) { 28 | let tableData: DynamoDB.Types.GetItemOutput; 29 | 30 | try { 31 | tableData = await ddbClient 32 | .get({ 33 | TableName: TABLE_NAME as string, 34 | Key: { groupId }, 35 | }) 36 | .promise(); 37 | } catch (err) { 38 | throw new Error(`Failed to get stage data: ${(err as Error).toString()}`); 39 | } 40 | 41 | return tableData.Item as MultiHostGroup; 42 | } 43 | 44 | async function putGroup( 45 | groupId: string, 46 | stageId: IVSRealTime.Types.StageArn, 47 | channelId: IVS.Types.ChannelArn, 48 | roomId: Ivschat.Types.RoomArn, 49 | hostAttributes: UserAttributes, 50 | ) { 51 | // Write info to DynamoDB 52 | try { 53 | await ddbClient 54 | .put({ 55 | TableName: TABLE_NAME as string, 56 | Item: { 57 | groupId, 58 | stageId, 59 | channelId, 60 | roomId, 61 | stageAttributes: hostAttributes, 62 | }, 63 | }) 64 | .promise(); 65 | } catch (err) { 66 | throw new Error( 67 | `Failed to update table ${TABLE_NAME}: ${(err as Error).toString()}`, 68 | ); 69 | } 70 | } 71 | 72 | function deleteGroup(groupId: string) { 73 | return ddbClient 74 | .delete({ 75 | TableName: TABLE_NAME as string, 76 | Key: { groupId }, 77 | }) 78 | .promise(); 79 | } 80 | 81 | export { ddbClient, deleteGroup, getGroup, putGroup, scanGroups }; 82 | -------------------------------------------------------------------------------- /architecture-description.md: -------------------------------------------------------------------------------- 1 | # App architecture description 2 | 3 |
4 | A diagram showing the architecture of the application. 5 |
6 | 7 | This diagram illustrates the architecture of this demo backend and how it interacts with the broadcast and playback clients. The diagram has three sections from left to right. The first section has an illustration of a computer and mobile phone with a title "Client app, powered by Amazon IVS Broadcast SDKs". The section contains three icons of a user with the label "Participant". Four arrows connect the first section to the second section. From top to bottom, the first arrow is purple and connects the first section to API gateway in the second section and is labelled "Create and manage resources". The second arrow is blue and connects the first section to Amazon IVS and is labelled "Publish and subscribe to stage (WebRTC)". The third arrow is green and connects the first section to Amazon IVS and is labelled "Live streamed stage video (RTMPS)". The fourth arrow is red and connects the first section to Amazon IVS and is labelled "Send and receive chat messages (Websocket)". 8 | 9 | The second section is titled "AWS Cloud" and contains four icons. From left to right, the first icon is the Amazon API Gateway logo and is titled "API Gateway". It is connected by an arrow to an icon with the AWS Lambda logo and is titled "AWS Lambda". The AWS Lambda icon has an arrow on the right side labeled "Store resource details", which is connected an icon of the Amazon DynamoDB logo titled "Amazon DynamoDB". The AWS Lambda icon has another arrow on the bottom side of the icon labeled "Create & manage resources. Channels, Chat rooms, Stages" that is connected to an icon of the Amazon IVS logo titled "Amazon IVS". 10 | 11 | The third section has an illustration of a computer and mobile phone with a title "Client app, powered by Amazon IVS Player SDKs". The section contains three icons of a user with the label "Viewer". The Amazon IVS icon in the second section is connected to the third section with two arrows. From top to bottom, the first arrow is green and labeled "Live streamed stage video (RTMPS)". The second arrow is red and labeled "Send and receive chat messages (Websocket)". 12 | 13 |
14 |
15 | -------------------------------------------------------------------------------- /src/delete.ts: -------------------------------------------------------------------------------- 1 | import { IVS, Ivschat, IVSRealTime } from "aws-sdk"; 2 | 3 | import { deleteGroup, getGroup } from "./sdk/ddb"; 4 | import { deleteStage } from "./sdk/realtime"; 5 | import { deleteRoom } from "./sdk/room"; 6 | import { deleteChannel, stopStream } from "./sdk/video"; 7 | 8 | /** 9 | * A function that destroys a group and it's related resources. 10 | */ 11 | 12 | async function deleteGroupAndResources(groupId: string) { 13 | let channelId; 14 | let stageId; 15 | let roomId; 16 | 17 | // Find resources 18 | try { 19 | ({ channelId, stageId, roomId } = await getGroup(groupId)); 20 | } catch (err) { 21 | throw new Error( 22 | `Failed to find associated resources: ${(err as Error).toString()}`, 23 | ); 24 | } 25 | 26 | // Deleted db entry 27 | try { 28 | await deleteGroup(groupId); 29 | } catch (err) { 30 | throw new Error( 31 | `Failed to delete stage table entry: ${(err as Error).toString()}`, 32 | ); 33 | } 34 | 35 | // Stop streaming 36 | try { 37 | await stopStream(channelId as IVS.Types.ChannelArn); 38 | } catch (err) { 39 | // Silently report ChannelNotBroadcasting errors. 40 | if (err instanceof Error) { 41 | if (err.name === "ChannelNotBroadcasting") { 42 | console.log((err as Error).toString()); 43 | } else { 44 | throw new Error(`Failed to stop stream: ${(err as Error).toString()}`); 45 | } 46 | } else { 47 | throw new Error(`Failed to stop stream: ${(err as Error).toString()}`); 48 | } 49 | } 50 | 51 | // Delete channel 52 | try { 53 | await deleteChannel(channelId as IVS.Types.ChannelArn); 54 | } catch (err) { 55 | throw new Error(`Failed to delete channel: ${(err as Error).toString()}`); 56 | } 57 | 58 | // Delete stage 59 | try { 60 | await deleteStage(stageId as IVSRealTime.Types.StageArn); 61 | } catch (err) { 62 | throw new Error(`Failed to delete stage: ${(err as Error).toString()}`); 63 | } 64 | 65 | // Delete room 66 | try { 67 | await deleteRoom(roomId as Ivschat.Types.RoomIdentifier); 68 | } catch (err) { 69 | throw new Error(`Failed to delete room: ${(err as Error).toString()}`); 70 | } 71 | } 72 | 73 | // eslint-disable-next-line import/prefer-default-export 74 | export { deleteGroupAndResources }; 75 | -------------------------------------------------------------------------------- /src/create.ts: -------------------------------------------------------------------------------- 1 | import { IVS, Ivschat, IVSRealTime } from "aws-sdk"; 2 | 3 | import { putGroup } from "./sdk/ddb"; 4 | import { createStage } from "./sdk/realtime"; 5 | import { createRoom } from "./sdk/room"; 6 | import { createChannel } from "./sdk/video"; 7 | import { createTokens } from "./tokens"; 8 | import { 9 | ChannelResponse, 10 | RoomResponse, 11 | StageResponse, 12 | UserAttributes, 13 | } from "./types"; 14 | 15 | /** 16 | * A function that creates a group with the provided `groupId` and the following 17 | * associated resources: Amazon IVS channel, chat room, stage. 18 | * Returns a stage token and chat token 19 | */ 20 | 21 | async function create( 22 | groupIdParam: string, 23 | userId: string, 24 | attributes: UserAttributes, 25 | ) { 26 | let groupId: string; 27 | let channelResponse: ChannelResponse; 28 | let roomResponse: RoomResponse; 29 | let stageResponse: StageResponse; 30 | 31 | // Create channel 32 | try { 33 | const { channel, streamKey } = await createChannel(); 34 | channelResponse = { 35 | id: channel?.arn as IVS.Types.ChannelArn, 36 | playbackUrl: channel?.playbackUrl as IVS.Types.PlaybackURL, 37 | ingestEndpoint: channel?.ingestEndpoint as IVS.Types.IngestEndpoint, 38 | streamKey: streamKey?.value as IVS.Types.StreamKeyValue, 39 | }; 40 | } catch (err) { 41 | throw new Error(`Failed to create channel: ${(err as Error).toString()}`); 42 | } 43 | 44 | // Create room 45 | try { 46 | const room = await createRoom(); 47 | roomResponse = { 48 | id: room.arn as Ivschat.Types.RoomIdentifier, 49 | token: "" as Ivschat.Types.CreateChatTokenResponse, 50 | }; 51 | } catch (err) { 52 | throw new Error(`Failed to create chat room: ${(err as Error).toString()}`); 53 | } 54 | 55 | // Create stage 56 | try { 57 | const stage = await createStage(); 58 | stageResponse = { 59 | id: stage.arn as IVSRealTime.Types.StageArn, 60 | token: "" as IVSRealTime.Types.ParticipantToken, 61 | }; 62 | } catch (err) { 63 | throw new Error(`Failed to create stage: ${(err as Error).toString()}`); 64 | } 65 | 66 | // Write to db 67 | try { 68 | groupId = groupIdParam || stageResponse.id; 69 | await putGroup( 70 | groupId, 71 | stageResponse.id, 72 | channelResponse.id, 73 | roomResponse.id, 74 | attributes, 75 | ); 76 | } catch (err) { 77 | throw new Error(`Failed to write details: ${(err as Error).toString()}`); 78 | } 79 | 80 | // Create tokens 81 | try { 82 | const { chatTokenData, stageTokenData } = await createTokens( 83 | roomResponse.id, 84 | stageResponse.id, 85 | userId, 86 | attributes, 87 | ); 88 | roomResponse.token = chatTokenData; 89 | stageResponse.token = stageTokenData; 90 | } catch (err) { 91 | throw new Error(`Failed to create tokens: ${(err as Error).toString()}`); 92 | } 93 | 94 | return { 95 | groupId, 96 | channel: channelResponse, 97 | stage: stageResponse, 98 | chat: roomResponse, 99 | }; 100 | } 101 | 102 | // eslint-disable-next-line import/prefer-default-export 103 | export { create }; 104 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const prettierRc = require("./.prettierrc.js"); 3 | 4 | module.exports = { 5 | parser: "@typescript-eslint/parser", 6 | parserOptions: { 7 | ecmaVersion: 6, 8 | sourceType: "module", 9 | }, 10 | plugins: [ 11 | "lodash", 12 | "prettier", 13 | "json", 14 | "markdown", 15 | "simple-import-sort", 16 | "formatjs", 17 | ], 18 | extends: [ 19 | "airbnb", 20 | "plugin:prettier/recommended", 21 | "plugin:jsx-a11y/recommended", 22 | "plugin:json/recommended", 23 | "plugin:@typescript-eslint/recommended", 24 | "prettier", 25 | ], 26 | env: { 27 | node: true, 28 | browser: true, 29 | es6: true, 30 | }, 31 | rules: { 32 | "@typescript-eslint/no-use-before-define": ["error"], 33 | "@typescript-eslint/no-shadow": ["error"], 34 | "arrow-body-style": ["error", "as-needed"], 35 | "class-methods-use-this": "off", 36 | curly: "error", 37 | "formatjs/no-id": "error", 38 | "formatjs/enforce-description": "error", 39 | "formatjs/enforce-default-message": "error", 40 | "import/order": "off", // Handled by simple-import-sort 41 | "import/extensions": "off", 42 | "lodash/import-scope": ["error", "method"], 43 | "padding-line-between-statements": [ 44 | "error", 45 | { blankLine: "always", prev: "*", next: "block-like" }, 46 | { blankLine: "always", prev: "block-like", next: "*" }, 47 | ], 48 | "prefer-destructuring": [ 49 | "error", 50 | { 51 | VariableDeclarator: { 52 | array: false, 53 | object: true, 54 | }, 55 | AssignmentExpression: { 56 | array: false, 57 | object: false, 58 | }, 59 | }, 60 | ], 61 | "prettier/prettier": ["error", prettierRc], 62 | "simple-import-sort/imports": "error", 63 | "simple-import-sort/exports": "error", 64 | "sort-imports": "off", // Handled by simple-import-sort 65 | "no-restricted-exports": [ 66 | // https://github.com/airbnb/javascript/issues/2500 67 | "error", 68 | { 69 | restrictedNamedExports: [ 70 | // "default", // use `export default` to provide a default export 71 | "then", // this will cause tons of confusion when your module is dynamically `import()`ed, and will break in most node ESM versions 72 | ], 73 | }, 74 | ], 75 | "no-restricted-syntax": [ 76 | "error", 77 | { 78 | selector: "ForInStatement", 79 | message: 80 | "for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.", 81 | }, 82 | { 83 | selector: "LabeledStatement", 84 | message: 85 | "Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.", 86 | }, 87 | { 88 | selector: "WithStatement", 89 | message: 90 | "`with` is disallowed in strict mode because it makes code impossible to predict and optimize.", 91 | }, 92 | ], 93 | "no-use-before-define": "off", 94 | "no-shadow": "off", 95 | }, 96 | settings: { 97 | "import/resolver": { 98 | typescript: { 99 | project: ["./tsconfig.json"], 100 | }, 101 | }, 102 | }, 103 | 104 | overrides: [ 105 | { 106 | files: ["*.test.ts"], 107 | rules: { 108 | "@typescript-eslint/no-empty-function": "off", 109 | "import/no-extraneous-dependencies": "off", 110 | }, 111 | }, 112 | { 113 | files: ["lambda/*"], 114 | rules: { 115 | "import/no-extraneous-dependencies": "off", 116 | "import/prefer-default-export": "off", 117 | "@typescript-eslint/no-var-requires": "off", 118 | "no-console": "off", 119 | }, 120 | }, 121 | { 122 | files: ["src/**"], 123 | rules: { 124 | "import/no-extraneous-dependencies": "off", 125 | "@typescript-eslint/no-var-requires": "off", 126 | "no-console": "off", 127 | }, 128 | }, 129 | ], 130 | }; 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon IVS Multi-host Serverless Demo 2 | 3 | This readme includes instructions for deploying the Amazon IVS Multi-host Serverless Demo to an AWS Account. This serverless application supports the following Amazon IVS demos: 4 | 5 | - [Amazon IVS Multi-host for iOS Demo](https://github.com/aws-samples/amazon-ivs-multi-host-for-ios-demo) 6 | - [Amazon IVS Multi-host for Android Demo](https://github.com/aws-samples/amazon-ivs-multi-host-for-android-demo) 7 | 8 | **\*IMPORTANT NOTE:** Deploying this demo application in your AWS account will create and consume AWS resources, which will cost money.\* 9 | 10 | ## Application overview 11 | 12 | A diagram showing the architecture of the application. 13 | 14 | A full description of the diagram is available in the [architecture description](./architecture-description.md). 15 | 16 | ## Prerequisites 17 | 18 | - [AWS CLI Version 2](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) 19 | - [NodeJS](https://nodejs.org/en/) and `npm` (npm is usually installed with NodeJS). 20 | - If you have [node version manager](https://github.com/nvm-sh/nvm) installed, run `nvm use` to sync your node version with this project. 21 | - Access to an AWS Account with at least the following permissions: 22 | - Create IAM roles 23 | - Create Lambda Functions 24 | - Create Amazon IVS Channels, Stages, and Chat rooms 25 | - Create Amazon S3 Buckets 26 | - Create Amazon DynamoDB Tables 27 | 28 | ### Configure the AWS CLI 29 | 30 | Before you start, run the following command to make sure you're in the correct AWS account (or configure as needed): 31 | 32 | ```bash 33 | aws configure 34 | ``` 35 | 36 | For configuration specifics, refer to the [AWS CLI User Guide](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html) 37 | 38 | ## Run this app locally 39 | 40 | To run the app locally, first install the [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html#install-sam-cli-instructions) and [Docker](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-docker.html#install-docker-instructions). With AWS SAM CLI installed and Docker running on your machine, take the following steps: 41 | 42 | 1. Install the required packages: `npm install` 43 | 2. Bootstrap the required resources: `npm run bootstrap` 44 | 3. Run the application: `sam local start-api -t ./cdk.out/AmazonIVSMultiHostServerlessStack.template.json` 45 | 46 | ## Deploy this app to AWS 47 | 48 | With NodeJS and NPM installed, take the following steps: 49 | 50 | 1. Install the required packages: `npm install` 51 | 2. Bootstrap the required resources: `npm run bootstrap` 52 | 3. Run the application: `npm run deploy` 53 | 54 | ### Use your deployed backend in the client applications 55 | 56 | When the deployment successfully completes, copy the URL provided in the `Outputs` of the script. The URL will be similar to the following format: 57 | 58 | ```bash 59 | https://.execute-api..amazonaws.com/prod/ 60 | ``` 61 | 62 | This URL can be used to run the following demo applications: 63 | 64 | - [Amazon IVS Multi-host for iOS Demo](https://github.com/aws-samples/amazon-ivs-multi-host-for-ios-demo) 65 | - [Amazon IVS Multi-host for Android Demo](https://github.com/aws-samples/amazon-ivs-multi-host-for-android-demo) 66 | 67 | ### Accessing the deployed application 68 | 69 | If needed, you can retrieve the Cloudformation stack outputs by running the following command: 70 | 71 | ```bash 72 | aws cloudformation describe-stacks --stack-name AmazonIVSMultiHostServerlessStack \ 73 | --query 'Stacks[].Outputs' 74 | ``` 75 | 76 | ## Cleanup 77 | 78 | To delete all resources associated with this demo, **including the DynamoDB table!** run the following command: 79 | 80 | ```base 81 | npm run destroy 82 | ``` 83 | 84 | This command may not delete all associated Amazon IVS stages, channels, or chat rooms. Visit the [Amazon IVS web console](https://console.aws.amazon.com/ivs/) to delete any lingering resources. 85 | 86 | ## Known issues 87 | 88 | - In some instances, the Amazon IVS stage, channel, or room may fail to delete. To remove resources manually, look for resources tagged with the key `AmazonIVSDemoResource` and value `AmazonIVSMultiHostResource`. 89 | -------------------------------------------------------------------------------- /lambda/stack.ts: -------------------------------------------------------------------------------- 1 | import { Duration, RemovalPolicy, Stack, StackProps } from "aws-cdk-lib"; 2 | import { Cors, LambdaIntegration, RestApi } from "aws-cdk-lib/aws-apigateway"; 3 | import { AttributeType, BillingMode, Table } from "aws-cdk-lib/aws-dynamodb"; 4 | import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam"; 5 | import { Runtime } from "aws-cdk-lib/aws-lambda"; 6 | import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs"; 7 | import { Construct } from "constructs"; 8 | 9 | import { DDBTableName } from "./constants"; 10 | 11 | function getPolicy(): PolicyStatement { 12 | return new PolicyStatement({ 13 | effect: Effect.ALLOW, 14 | actions: ["ivs:*", "ivschat:*"], 15 | resources: ["*"], 16 | }); 17 | } 18 | 19 | class AmazonIVSMultiHostStack extends Stack { 20 | constructor(scope: Construct, id: string, props?: StackProps) { 21 | super(scope, id, props); 22 | 23 | const initialPolicy = [getPolicy()]; 24 | const runtime = Runtime.NODEJS_18_X; 25 | const environment = { TABLE_NAME: DDBTableName }; 26 | const timeout = Duration.minutes(1); 27 | 28 | const stagesTable = new Table(this, DDBTableName, { 29 | tableName: DDBTableName, 30 | partitionKey: { 31 | name: "groupId", 32 | type: AttributeType.STRING, 33 | }, 34 | billingMode: BillingMode.PAY_PER_REQUEST, 35 | removalPolicy: RemovalPolicy.DESTROY, 36 | }); 37 | 38 | const createFunction = new NodejsFunction( 39 | this, 40 | "AmazonIVSMultiHostDemoCreateFunction", 41 | { 42 | entry: "lambda/createHandler.ts", 43 | handler: "createHandler", 44 | initialPolicy, 45 | runtime, 46 | environment, 47 | timeout, 48 | }, 49 | ); 50 | 51 | const deleteFunction = new NodejsFunction( 52 | this, 53 | "AmazonIVSMultiHostDemoDeleteFunction", 54 | { 55 | entry: "lambda/deleteHandler.ts", 56 | handler: "deleteHandler", 57 | initialPolicy, 58 | runtime, 59 | environment, 60 | timeout, 61 | }, 62 | ); 63 | 64 | const listFunction = new NodejsFunction( 65 | this, 66 | "AmazonIVSMultiHostDemoListFunction", 67 | { 68 | entry: "lambda/listHandler.ts", 69 | handler: "listHandler", 70 | initialPolicy, 71 | runtime, 72 | environment, 73 | timeout, 74 | }, 75 | ); 76 | 77 | const stageJoinFunction = new NodejsFunction( 78 | this, 79 | "AmazonIVSMultiHostDemoJoinFunction", 80 | { 81 | entry: "lambda/stageJoinHandler.ts", 82 | handler: "stageJoinHandler", 83 | initialPolicy, 84 | runtime, 85 | environment, 86 | timeout, 87 | }, 88 | ); 89 | 90 | const stageDisconnectFunction = new NodejsFunction( 91 | this, 92 | "AmazonIVSMultiHostDemoDisconnectFunction", 93 | { 94 | entry: "lambda/stageDisconnectHandler.ts", 95 | handler: "stageDisconnectHandler", 96 | initialPolicy, 97 | runtime, 98 | environment, 99 | timeout, 100 | }, 101 | ); 102 | 103 | stagesTable.grantWriteData(createFunction); 104 | stagesTable.grantReadWriteData(deleteFunction); 105 | stagesTable.grantReadData(listFunction); 106 | stagesTable.grantReadData(stageJoinFunction); 107 | stagesTable.grantReadData(stageDisconnectFunction); 108 | 109 | const api = new RestApi(this, "AmazonIVSMultiHostDemoApi", { 110 | defaultCorsPreflightOptions: { 111 | allowOrigins: Cors.ALL_ORIGINS, 112 | allowMethods: ["POST", "DELETE"], 113 | allowHeaders: Cors.DEFAULT_HEADERS, 114 | }, 115 | }); 116 | 117 | const createPath = api.root.addResource("create"); 118 | createPath.addMethod("POST", new LambdaIntegration(createFunction)); 119 | 120 | const deletePath = api.root.addResource("delete"); 121 | deletePath.addMethod("DELETE", new LambdaIntegration(deleteFunction)); 122 | 123 | const listPath = api.root.addResource("list"); 124 | listPath.addMethod("POST", new LambdaIntegration(listFunction)); 125 | 126 | const disconnectPath = api.root.addResource("disconnect"); 127 | disconnectPath.addMethod( 128 | "POST", 129 | new LambdaIntegration(stageDisconnectFunction), 130 | ); 131 | 132 | const joinPath = api.root.addResource("join"); 133 | joinPath.addMethod("POST", new LambdaIntegration(stageJoinFunction)); 134 | } 135 | } 136 | 137 | export default AmazonIVSMultiHostStack; 138 | --------------------------------------------------------------------------------