├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── THIRD-PARTY-LICENSES.txt
├── amazon-ivs-dvr-demo.png
├── architecture.png
├── cdk
├── .gitignore
├── .npmignore
├── Makefile
├── bin
│ └── cdk.ts
├── cdk.json
├── config.json
├── lambdas
│ ├── getLatestRecordingStartMeta.ts
│ ├── saveRecordingStartMeta.ts
│ └── utils.ts
├── lib
│ ├── utils.ts
│ └── vod-rendition-playlist-stack.ts
├── package-lock.json
├── package.json
├── sanitize-output.js
└── tsconfig.json
├── docs
├── multi-channel-architecture.png
└── multi-channel.md
└── web-ui
├── .eslintrc.json
├── .gitignore
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
└── src
├── App.css
├── App.jsx
├── Player
├── Controls
│ ├── BackToLiveBtn
│ │ ├── BackToLiveBtn.css
│ │ ├── BackToLiveBtn.jsx
│ │ └── index.js
│ ├── Controls.css
│ ├── Controls.jsx
│ └── index.js
├── FadeInOut
│ ├── FadeInOut.jsx
│ └── index.js
├── Notification
│ ├── Notification.css
│ ├── Notification.jsx
│ └── index.js
├── Player.css
├── Player.jsx
├── Spinner
│ ├── Spinner.css
│ ├── Spinner.jsx
│ └── index.js
├── StatusPill
│ ├── StatusPill.css
│ ├── StatusPill.jsx
│ └── index.js
└── index.js
├── SinglePlayer
├── Controls
│ ├── Controls.jsx
│ └── index.js
├── Player.jsx
├── StatusPill
│ ├── StatusPill.css
│ ├── StatusPill.jsx
│ └── index.js
└── index.js
├── assets
└── icons
│ ├── backward-60.jsx
│ ├── forward-60.jsx
│ ├── live.jsx
│ ├── pause.jsx
│ ├── play.jsx
│ ├── recorded.jsx
│ ├── spinner.jsx
│ └── warning.jsx
├── constants.js
├── contexts
├── Controls
│ ├── context.js
│ ├── provider.jsx
│ └── useControls.js
├── MobileBreakpoint
│ ├── context.js
│ ├── provider.jsx
│ └── useMobileBreakpoint.js
├── Playback
│ ├── context.js
│ ├── provider.jsx
│ ├── usePlayback.js
│ └── utils.js
└── SinglePlayerPlayback
│ ├── context.js
│ ├── provider.jsx
│ └── usePlayback.js
├── hooks
├── useFirstMountState.js
├── usePlayer.js
├── usePrevious.js
├── useSeekBar.js
├── useSinglePlayer.js
├── useSinglePlayerSeekBar.js
└── useStateWithCallback.js
├── index.css
├── index.js
├── utils.js
└── workers
└── amazon-ivs-service-worker-loader.js
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | ## Code of Conduct
2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
4 | opensource-codeofconduct@amazon.com with any additional questions or comments.
5 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional
4 | documentation, we greatly value feedback and contributions from our community.
5 |
6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary
7 | information to effectively respond to your bug report or contribution.
8 |
9 |
10 | ## Reporting Bugs/Feature Requests
11 |
12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features.
13 |
14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already
15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
16 |
17 | * A reproducible test case or series of steps
18 | * The version of our code being used
19 | * Any modifications you've made relevant to the bug
20 | * Anything unusual about your environment or deployment
21 |
22 |
23 | ## Contributing via Pull Requests
24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:
25 |
26 | 1. You are working against the latest source on the *master* 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 |
61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes.
62 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this
4 | software and associated documentation files (the "Software"), to deal in the Software
5 | without restriction, including without limitation the rights to use, copy, modify,
6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
7 | permit persons to whom the Software is furnished to do so.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Amazon IVS DVR web demo
2 |
3 | A demo web application intended as an educational tool for demonstrating how you can implement a Live to VOD (DVR) experience using [Amazon IVS](https://aws.amazon.com/ivs/) and the auto-record-to-s3 feature using [Amazon S3](https://aws.amazon.com/s3/). The VOD content is served with [Amazon CloudFront](https://aws.amazon.com/cloudfront/) and [Amazon Lambda@Edge](https://aws.amazon.com/lambda/edge/).
4 |
5 | This demo also uses [AWS Cloud Development Kit](https://aws.amazon.com/cdk/) (AWS CDK v2).
6 |
7 | 
8 |
9 | **This project is intended for education purposes only and not for production usage.**
10 |
11 | ## Prerequisites
12 |
13 | - AWS CLI ([Installing the AWS CLI version 2](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html))
14 | - NodeJS ([Installing Node.js](https://nodejs.org/))
15 |
16 | ## To use and deploy this project
17 |
18 | **IMPORTANT NOTE:** this demo will create and use AWS resources on your AWS account, which will cost money.
19 |
20 | Deploying the CDK stack will:
21 |
22 | - create an Amazon IVS channel
23 | - set up auto-record-to-S3 for that channel
24 | - create Lambda and Lambda@Edge resources to process VOD content
25 | - create a CloudFront distribution to serve the VOD content
26 |
27 | ### Architecture
28 |
29 | 
30 |
31 |
32 | ### Configuration
33 |
34 | The `cdk/config.json` file provides two configurable options:
35 |
36 | - `channelType` can be set to `BASIC`, `STANDARD`, `ADVANCED_SD`, or `ADVANCED_HD`
37 | - `allowedOrigins` is a list of origins (domain names) that CloudFront uses as the value for the `Access-Control-Allow-Origin` HTTP response header. You can add your custom domain to this list, or specify `['*']` to allow all origins.
38 | - `insecureRTMPIngest` is a boolean flag that, when set to true, allows the channel to accept RTMP streams without requiring a secure connection.
39 | - `transcodePreset` can only be set for `ADVANCED_HD` and `ADVANCED_SD` channel types, with allowed values `HIGHER_BANDWIDTH_DELIVERY` and `CONSTRAINED_BANDWIDTH_DELIVERY`. For `BASIC` and `STANDARD` channel types, preset should be an empty string (`""`).
40 |
41 | By default, the demo will expect you to deploy the backend prior to running the frontend application, which will create a `cdk_output.json` file containing the CloudFront distribution domain name outputted from the deployment. However, if you wish to run your frontend against a different DVR demo backend, this behavior can be overridden by setting the following environment variable:
42 |
43 | ```shell
44 | REACT_APP_DISTRIBUTION_DOMAIN_NAME=
45 | ```
46 |
47 | Where `` is the domain name of the CloudFront distribution, such as `d111111abcdef8.cloudfront.net`. If you have chosen to set this environment variable, you may proceed straight to step 2 in the _Deployment_ section below.
48 |
49 | **NOTE:** You can add or modify the allowed origins for CORS requests by modifying the ResponseHeaders policy through the CloudFront console, however this is not advisable and we recommend you make such changes by re-deploying the CDK stack with your changes in the `allowedOrigins` list in the `config.json` file.
50 |
51 | ### Deployment
52 |
53 | 1. To set up the backend, in the `cdk` directory, run:
54 |
55 | ```shell
56 | make app
57 | ```
58 |
59 | **NOTE:** this demo uses [AWS Lambda@Edge](https://aws.amazon.com/lambda/edge/), which is currently only available in the US East, N. Virginia (us-east-1) region. To comply with this requirement, this demo is configured to deploy to the us-east-1 region of the account specified in your AWS CLI profile.
60 |
61 | _In the `cdk` directory, run `make help` to see a list of available targets and other configuration options._
62 |
63 | The script will give you 2 important pieces of information:
64 |
65 | - `DVRdemoStack.ingestServer`, the ingest server address to use in your broadcasting software ([learn how to stream to Amazon IVS](https://aws.amazon.com/blogs/media/setting-up-for-streaming-with-amazon-ivs/))
66 | - `DVRdemoStack.streamKey`, the stream key for your newly created Amazon IVS channel
67 |
68 |
69 | At this point, you may configure your broadcasting software and begin streaming before moving on to the next step.
70 |
71 | 2. Go to the `web-ui` directory and run the following commands to start the React frontend host:
72 |
73 | ```shell
74 | npm install
75 | npm start
76 | ```
77 |
78 | Once you go live, you will be able to see your live video stream on the hosted frontend. About 50 seconds since you started broadcasting, the VOD will become available and you will be able to seamlessly scrub between Live and VOD in the player.
79 |
80 | ## Frontend Player Options
81 |
82 | The app supports two options for running the frontend: a dual-player option and a single-player option.
83 |
84 | ### Dual-Player Option (Default)
85 | Available at the `/` route, this option switches between two player instances - one for live playback and one for VOD playback. When a viewer is watching content on one player, the other player remains hidden but continues to buffer and play content in the background. This ensures immediate content availability when switching modes, providing the smoothest possible user experience with no interruptions during transitions.
86 |
87 | While this approach provides optimal playback continuity, it will:
88 | - Incur higher costs due to concurrent streaming of both live and VOD content
89 | - Require more system resources
90 | - Increase data usage
91 |
92 | ### Single-Player Option
93 | Available at the `/alt` route, this option uses a single player instance and loads the appropriate playback URL when switching between live and VOD modes. The player reinitializes with the new content when transitioning between modes, which maintains efficient resource usage but may result in brief loading periods.
94 |
95 | This approach provides a balanced experience that:
96 | - Reduces operational costs through single-stream playback
97 | - Minimizes system resource consumption
98 | - Optimizes data usage efficiency
99 |
100 | Choose the option that best balances your needs for user experience and resource optimization.
101 |
102 | ## Backend Specification
103 |
104 | ```
105 | GET https:///recording-started-latest.json
106 | ```
107 |
108 | Response Schema:
109 |
110 | ```ts
111 | {
112 | isChannelLive: boolean,
113 | livePlaybackUrl?: string,
114 | masterKey?: string,
115 | playlistDuration: number | null,
116 | recordingStartedAt?: string,
117 | }
118 | ```
119 |
120 | - `isChannelLive` is an indicator of the current status of the IVS channel
121 | - `livePlaybackUrl` is the playback URL for the livestream.
122 | - `masterKey` is the S3 key path for the `byte-range-multivariant.m3u8` file of the VOD playback
123 | - `playlistDuration` is the duration of the latest VOD playlist (used only on iOS mobile browsers)
124 | - `recordingStartedAt` is the date time that the stream recording started at, in ISO 8601 format
125 |
126 | The response object returned by this endpoint will only contain the relevant properties based on the current state of the channel.
127 |
128 | ## Backend Teardown
129 |
130 | To avoid unexpected charges to your account, be sure to destroy your CDK stack when you are finished:
131 |
132 | 1. If you are currently streaming, stop the stream
133 |
134 | 2. In the `cdk` directory, run:
135 |
136 | ```shell
137 | make destroy
138 | ```
139 |
140 | After running this command, you will notice that the stack deletion process will fail. This is expected, as only the _associations_ between the Lambda@Edge functions and the CloudFront distribution are removed.
141 |
142 | The remaining Lambda@Edge function replicas will typically be automatically deleted by CloudFront within a few hours, at which point you will be able to run `make destroy` once again to complete deleting the stack, along with the Lambda functions that failed to delete from earlier.
143 |
144 | Alternatively, you may choose to manually delete the CloudFormation stack from the AWS console while retaining the Lambda@Edge functions for you to delete at a later time, allowing you to immediately re-deploy the stack if needed.
145 |
146 | ## Limitations and Known Issues
147 |
148 | - Full functionality for iOS mobile browsers is limited due to player-related constraints. As a consequence, on iOS devices only, the user _may not_ be able to seek within the last 30 seconds of the VOD content.
149 | - This demo uses Lambda@Edge, which is currently only supported in the us-east-1 (N.Virginia) region.
150 |
151 | ## About Amazon IVS
152 |
153 | Amazon Interactive Video Service (Amazon IVS) is a managed live streaming solution that is quick and easy to set up, and ideal for creating interactive video experiences. [Learn more](https://aws.amazon.com/ivs/).
154 |
155 | - [Amazon IVS docs](https://docs.aws.amazon.com/ivs/)
156 | - [User Guide](https://docs.aws.amazon.com/ivs/latest/userguide/)
157 | - [API Reference](https://docs.aws.amazon.com/ivs/latest/APIReference/)
158 | - [Setting Up for Streaming with Amazon Interactive Video Service](https://aws.amazon.com/blogs/media/setting-up-for-streaming-with-amazon-ivs/)
159 | - [Learn more about Amazon IVS on IVS.rocks](https://ivs.rocks/)
160 | - [View more demos like this](https://ivs.rocks/examples)
161 |
162 | ## Security
163 |
164 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information.
165 |
166 | ## License
167 |
168 | This library is licensed under the MIT-0 License. See the LICENSE file.
169 |
--------------------------------------------------------------------------------
/THIRD-PARTY-LICENSES.txt:
--------------------------------------------------------------------------------
1 | The following assets are licensed under Apache 2.0
2 | ** web-ui/src/assets/icons/warning.jsx
3 |
4 | The following open-source libraries are used in this application:
5 | ** React; version 17.0.2 -- https://github.com/facebook/react
6 |
7 | MIT License
8 |
9 | Copyright (c) Facebook, Inc. and its affiliates.
10 |
11 | Permission is hereby granted, free of charge, to any person obtaining a copy
12 | of this software and associated documentation files (the "Software"), to deal
13 | in the Software without restriction, including without limitation the rights
14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15 | copies of the Software, and to permit persons to whom the Software is
16 | furnished to do so, subject to the following conditions:
17 |
18 | The above copyright notice and this permission notice shall be included in all
19 | copies or substantial portions of the Software.
20 |
21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27 | SOFTWARE.
--------------------------------------------------------------------------------
/amazon-ivs-dvr-demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/amazon-ivs-dvr-web-demo/a2682c616b8f35f729d78903123b62fb77f607d0/amazon-ivs-dvr-demo.png
--------------------------------------------------------------------------------
/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/amazon-ivs-dvr-web-demo/a2682c616b8f35f729d78903123b62fb77f607d0/architecture.png
--------------------------------------------------------------------------------
/cdk/.gitignore:
--------------------------------------------------------------------------------
1 | *.js
2 | !jest.config.js
3 | !sanitize-output.js
4 | *.d.ts
5 | node_modules
6 |
7 | # temp
8 | temp_out.json
9 |
10 | # CDK asset staging directory
11 | .cdk.staging
12 | cdk.out
13 |
--------------------------------------------------------------------------------
/cdk/.npmignore:
--------------------------------------------------------------------------------
1 | *.ts
2 | !*.d.ts
3 |
4 | # CDK asset staging directory
5 | .cdk.staging
6 | cdk.out
7 |
--------------------------------------------------------------------------------
/cdk/Makefile:
--------------------------------------------------------------------------------
1 | .DEFAULT_GOAL := help
2 |
3 | .PHONY: help app install bootstrap deploy destroy clean
4 |
5 | AWS_PROFILE_FLAG = --profile $(AWS_PROFILE)
6 | CDK_OPTIONS := $(if $(AWS_PROFILE),$(AWS_PROFILE_FLAG))
7 | STACK ?= DVRdemoStack
8 |
9 | help: ## Shows this help message
10 | @echo "\n$$(tput bold)Available Rules:$$(tput sgr0)\n"
11 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST)\
12 | | sort \
13 | | awk \
14 | 'BEGIN {FS = ":.*?## "}; \
15 | {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}'
16 | @echo "\n$$(tput bold)IMPORTANT!$$(tput sgr0)\n"
17 | @echo "1. If AWS_PROFILE is not exported as an environment variable or provided through the command line, then the default AWS profile is used. \n" | fold -s
18 | @echo " Option 1: export AWS_PROFILE=user1\n"
19 | @echo " Option 2: make AWS_PROFILE=user1\n"
20 | @echo "2. Destroying the stack will result in a partial deletion of the stack resources as only the associations between the Lambda@Edge functions and CloudFront will be deleted, along with the CloudFront distribution. The remaining Lambda@Edge replicas will typically be automatically deleted by CloudFront within a few hours, at which point you will be able to run the 'destroy' target again to delete the stack and remaining Lambda functions or manually delete them from the console." | fold -s
21 |
22 | app: install bootstrap deploy ## Installs NPM dependencies, bootstraps, and deploys the stack
23 |
24 | install: ## Installs NPM dependencies
25 | @echo "Installing NPM dependencies..."
26 | npm install
27 |
28 | bootstrap: ## Deploys the CDK Toolkit staging stack
29 | @echo "Bootstrapping..."
30 | npx cdk bootstrap $(CDK_OPTIONS)
31 |
32 | deploy: ## Deploys the stack and sanitizes the stack output
33 | @echo "Deploying $(STACK)..."
34 | npx cdk deploy $(STACK) --outputs-file temp_out.json $(CDK_OPTIONS)
35 | @echo "Sanitizing CDK output..."
36 | node sanitize-output
37 | rm temp_out.json
38 | @echo "\n$$(tput bold) ✅ $(STACK) Deployed Successfully $$(tput sgr0)\n"
39 |
40 | destroy: clean ## Destroys the stack and cleans up
41 | @echo "Destroying $(STACK)..."
42 | npx cdk destroy $(STACK) $(CDK_OPTIONS)
43 |
44 | clean: ## Deletes the cloud assembly directory (cdk.out)
45 | @echo "Cleaning..."
46 | rm -r cdk.out
47 |
--------------------------------------------------------------------------------
/cdk/bin/cdk.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import 'source-map-support/register';
3 | import { App } from 'aws-cdk-lib';
4 |
5 | import { DVRdemoStack } from '../lib/vod-rendition-playlist-stack';
6 | import { region } from '../config.json';
7 |
8 | new DVRdemoStack(new App(), 'DVRdemoStack', {
9 | env: {
10 | account: process.env.CDK_DEFAULT_ACCOUNT,
11 | region // Note: Lambda@Edge is currently only supported in the us-east-1 region
12 | }
13 | });
14 |
--------------------------------------------------------------------------------
/cdk/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-apigateway:usagePlanKeyOrderInsensitiveId": true,
19 | "@aws-cdk/core:stackRelativeExports": true,
20 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
21 | "@aws-cdk/aws-lambda:recognizeVersionProps": true,
22 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true,
23 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
24 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
25 | "@aws-cdk/core:target-partitions": ["aws", "aws-cn"]
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/cdk/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "region": "us-east-1",
3 | "channelType": "BASIC",
4 | "reconnectWindowSeconds": 60,
5 | "allowedOrigins": ["http://localhost:3000"],
6 | "insecureRTMPIngest": false,
7 | "transcodePreset": ""
8 | }
9 |
--------------------------------------------------------------------------------
/cdk/lambdas/getLatestRecordingStartMeta.ts:
--------------------------------------------------------------------------------
1 | import { CloudFrontRequestEvent } from 'aws-lambda';
2 | import { StreamState } from '@aws-sdk/client-ivs';
3 |
4 | import {
5 | getS3Object,
6 | getActiveStream,
7 | createResponse,
8 | isS3Error
9 | } from './utils';
10 |
11 | interface RecordingStartedMetadata {
12 | isChannelLive: boolean;
13 | livePlaybackUrl?: string;
14 | masterKey?: string;
15 | recordingStartedAt?: string;
16 | playlistDuration?: number;
17 | }
18 |
19 | /**
20 | * Triggered on Origin Requests to retrieve the recording-started-latest.json metadata file
21 | * from the VOD S3 bucket. A response is generated to contain only the required fields from
22 | * the metadata file, along with the live playbackUrl and an indicator for whether or not
23 | * the channel has an active (live) stream.
24 | *
25 | * Responses are sent with a no-cache Cache-Control directive to ensure that the browser does
26 | * not cache the response object (note that this Lambda(at)Edge function is already configured
27 | * with the managed CachingDisabled cache policy to ensure that CloudFront does not cache this
28 | * object either).
29 | *
30 | * If S3 throws a NoSuchKey (404) error, this is likely because we requested the metadata file before
31 | * the recording-started.json file was created by Amazon IVS and saved into the S3 VOD bucket by the S3 Event
32 | * Notification Lambda function (saveRecordingStartMeta). Therefore, instead of responding with the 404
33 | * status code, we send a 200 status code and an empty body, which will be handled accordingly on the
34 | * client-side. This prevents any unnecessary errors from being thrown on the client-side from this type
35 | * of event, which we know will occur frequently when the channel first starts recording.
36 | *
37 | * @param event CloudFront Origin Request event
38 | */
39 | const getLatestRecordingStartMeta = async (event: CloudFrontRequestEvent) => {
40 | const { origin, uri } = event.Records[0].cf.request;
41 | const customHeaders = origin!.s3!.customHeaders;
42 | const channelArn = customHeaders['channel-arn'][0].value || '';
43 | const bucketName = customHeaders['vod-record-bucket-name'][0].value || '';
44 | const key = uri.slice(1);
45 | let response;
46 |
47 | try {
48 | // Get recording-started-latest.json file
49 | const { body: jsonBody } = await getS3Object(key, bucketName);
50 | const {
51 | media: {
52 | hls: { path, byte_range_playlist, renditions }
53 | },
54 | recording_started_at: recordingStartedAt,
55 | streamId: recordedStreamId
56 | } = JSON.parse(jsonBody);
57 | const activeStream = await getActiveStream(channelArn);
58 | const {
59 | playbackUrl: livePlaybackUrl,
60 | state: channelState,
61 | streamId: activeStreamId
62 | } = activeStream || {};
63 | const isChannelLive = channelState === StreamState.StreamLive;
64 |
65 | // Build response body
66 | const recordingStartedMetadata: RecordingStartedMetadata = {
67 | isChannelLive,
68 | livePlaybackUrl: isChannelLive ? livePlaybackUrl : ''
69 | };
70 |
71 | // Only return VOD metadata if the recorded stream metadata is for the currently live stream (if one exists)
72 | if (recordedStreamId === activeStreamId) {
73 | recordingStartedMetadata.masterKey = `${path}/${byte_range_playlist}`;
74 | recordingStartedMetadata.recordingStartedAt = recordingStartedAt;
75 |
76 | try {
77 | const [highestRendition] = renditions;
78 | const renditionKey = `${path}/${highestRendition.path}/${highestRendition.playlist}`;
79 | const { body: textBody } = await getS3Object(renditionKey, bucketName);
80 | const regExp = /EXT-X-TWITCH-TOTAL-SECS:(.+)$/m;
81 | const match = textBody.match(regExp)?.[1];
82 |
83 | if (match) {
84 | recordingStartedMetadata.playlistDuration = parseInt(match);
85 | }
86 | } catch (error) {
87 | /**
88 | * Error out silently:
89 | * playlistDuration is only required for iOS devices when working with an open VOD playlist, other devices will get this value from the player instance on the FE.
90 | */
91 | }
92 | }
93 |
94 | response = createResponse(200, {
95 | body: JSON.stringify(recordingStartedMetadata),
96 | contentType: 'application/json',
97 | maxAge: 1
98 | });
99 | } catch (error) {
100 | if (isS3Error(error) && error.Code === 'NoSuchKey') {
101 | response = createResponse(200, { maxAge: 1, body: JSON.stringify(null) });
102 | } else {
103 | console.error(error);
104 | response = createResponse(500, { maxAge: 1 });
105 | }
106 | }
107 |
108 | return response;
109 | };
110 |
111 | export const handler = getLatestRecordingStartMeta;
112 |
--------------------------------------------------------------------------------
/cdk/lambdas/saveRecordingStartMeta.ts:
--------------------------------------------------------------------------------
1 | import { S3Event } from 'aws-lambda';
2 |
3 | import { getS3Object, putS3Object, getActiveStream } from './utils';
4 |
5 | /**
6 | * Receives the incoming s3:ObjectCreated:Put event containing the key and VOD bucket details
7 | * of the latest recording-start.json metadata file. The file is fetched from the bucket and
8 | * modified to include the full S3 recording prefix in the media/hls path, before being saved
9 | * at the top level of the same VOD bucket under the filename "recording-started-latest.json"
10 | *
11 | * @param event s3:ObjectCreated:Put event notification
12 | */
13 | const saveRecordingStartMeta = async (event: S3Event) => {
14 | const {
15 | object: { key },
16 | bucket: { name: bucketName }
17 | } = event.Records[0].s3;
18 | const s3RecordingKeyPrefix = key.split('/events')[0];
19 |
20 | try {
21 | const { body } = await getS3Object(key, bucketName);
22 | const metadata = JSON.parse(body);
23 | const relativePath = metadata.media.hls.path;
24 | const absolutePath = `${s3RecordingKeyPrefix}/${relativePath}`;
25 | metadata.media.hls.path = absolutePath;
26 |
27 | // add the active stream id to the metadata response
28 | const stream = await getActiveStream(metadata.channel_arn);
29 | metadata.streamId = stream?.streamId || '';
30 |
31 | // saves the latest recording-start.json metadata file at the top-level of the VOD S3 bucket
32 | await putS3Object(
33 | 'recording-started-latest.json',
34 | bucketName,
35 | JSON.stringify(metadata)
36 | );
37 | } catch (error) {
38 | console.error(error);
39 | throw error;
40 | }
41 | };
42 |
43 | export const handler = saveRecordingStartMeta;
44 |
--------------------------------------------------------------------------------
/cdk/lambdas/utils.ts:
--------------------------------------------------------------------------------
1 | import { Readable } from 'stream';
2 |
3 | import { CloudFrontResultResponse } from 'aws-lambda';
4 | import {
5 | S3Client,
6 | GetObjectCommand,
7 | PutObjectCommand,
8 | _Error as S3Error
9 | } from '@aws-sdk/client-s3';
10 | import {
11 | IvsClient,
12 | GetStreamCommand,
13 | ChannelNotBroadcasting,
14 | StreamState
15 | } from '@aws-sdk/client-ivs';
16 |
17 | import { region } from '../config.json';
18 |
19 | const s3 = new S3Client({ region });
20 | const ivs = new IvsClient({ region });
21 |
22 | const streamToString = async (stream: Readable) => {
23 | let result = '';
24 |
25 | for await (const chunk of stream) {
26 | result += chunk;
27 | }
28 |
29 | return result;
30 | };
31 |
32 | interface ResponseOptions {
33 | body?: string;
34 | contentType?: string;
35 | maxAge?: number;
36 | noCache?: boolean;
37 | }
38 |
39 | export const createResponse = (
40 | status: number,
41 | options: ResponseOptions = {}
42 | ) => {
43 | const {
44 | body = '',
45 | contentType = 'text/plain',
46 | maxAge = 0,
47 | noCache = false
48 | } = options;
49 |
50 | const response: CloudFrontResultResponse = {
51 | body,
52 | bodyEncoding: 'text',
53 | headers: { 'content-type': [{ value: `${contentType}` }] },
54 | status: status.toString()
55 | };
56 |
57 | if (noCache) {
58 | response.headers!['cache-control'] = [{ value: `no-cache` }];
59 | } else if (maxAge && maxAge >= 0) {
60 | response.headers!['cache-control'] = [{ value: `max-age=${maxAge}` }];
61 | }
62 |
63 | return response;
64 | };
65 |
66 | export const isS3Error = (error: any): error is S3Error => {
67 | return error && error.Code && error.Key;
68 | };
69 |
70 | export const getS3Object = async (key: string, bucket: string) => {
71 | const params = { Key: key, Bucket: bucket };
72 | const command = new GetObjectCommand(params);
73 | const { Body, LastModified } = await s3.send(command);
74 | const body = await streamToString(Body as Readable);
75 |
76 | return { body, LastModified };
77 | };
78 |
79 | export const putS3Object = async (
80 | key: string,
81 | bucket: string,
82 | body: string
83 | ) => {
84 | const params = {
85 | Body: body,
86 | Bucket: bucket,
87 | Key: key,
88 | ContentType: 'application/json'
89 | };
90 | const command = new PutObjectCommand(params);
91 |
92 | await s3.send(command);
93 | };
94 |
95 | export const getActiveStream = async (channelArn: string) => {
96 | const params = { channelArn };
97 | const command = new GetStreamCommand(params);
98 |
99 | try {
100 | const { stream } = await ivs.send(command);
101 | return stream || null;
102 | } catch (error) {
103 | if (error instanceof ChannelNotBroadcasting) {
104 | return { state: StreamState.StreamOffline };
105 | } else throw error;
106 | }
107 | };
108 |
--------------------------------------------------------------------------------
/cdk/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { join } from 'path';
2 |
3 | export const getLambdaEntryPath = (functionName: string) =>
4 | join(__dirname, '..', 'lambdas', `${functionName}.ts`);
5 |
--------------------------------------------------------------------------------
/cdk/lib/vod-rendition-playlist-stack.ts:
--------------------------------------------------------------------------------
1 | import {
2 | aws_cloudfront as cloudfront,
3 | aws_cloudfront_origins as origins,
4 | aws_iam as iam,
5 | aws_ivs as ivs,
6 | aws_lambda as lambda,
7 | aws_lambda_nodejs as lambdan,
8 | aws_s3 as s3,
9 | aws_s3_notifications as s3n,
10 | CfnOutput,
11 | Stack,
12 | StackProps,
13 | Duration
14 | } from 'aws-cdk-lib';
15 | import { Construct } from 'constructs';
16 |
17 | import {
18 | allowedOrigins,
19 | channelType,
20 | reconnectWindowSeconds,
21 | insecureRTMPIngest,
22 | transcodePreset
23 | } from '../config.json';
24 | import { getLambdaEntryPath } from './utils';
25 |
26 | export class DVRdemoStack extends Stack {
27 | constructor(scope: Construct, id: string, props?: StackProps) {
28 | super(scope, id, props);
29 |
30 | /**
31 | * S3 bucket where the VOD content will be stored
32 | */
33 | const bucket = new s3.Bucket(this, 'vod-record-bucket');
34 | const { bucketName: vodBucketName } = bucket;
35 |
36 | /**
37 | * IVS Channel Recording Configuration
38 | */
39 | const recordingConfig = new ivs.CfnRecordingConfiguration(
40 | this,
41 | 'dvr-recording-config',
42 | {
43 | name: 'dvr-recording-config',
44 | destinationConfiguration: { s3: { bucketName: vodBucketName } },
45 | recordingReconnectWindowSeconds: reconnectWindowSeconds,
46 | thumbnailConfiguration: { recordingMode: 'DISABLED' }
47 | }
48 | );
49 | const { attrArn: recordingConfigurationArn } = recordingConfig;
50 |
51 | /**
52 | * IVS Channel
53 | */
54 | const channel = new ivs.CfnChannel(this, 'DVR-demo-channel', {
55 | latencyMode: 'LOW',
56 | name: 'DVR-demo-channel',
57 | recordingConfigurationArn,
58 | type: channelType,
59 | insecureIngest: insecureRTMPIngest,
60 | preset: transcodePreset
61 | });
62 | const {
63 | attrArn: channelArn,
64 | attrPlaybackUrl: playbackUrl,
65 | attrIngestEndpoint: ingestEndpoint
66 | } = channel;
67 |
68 | // Stream configuration values
69 | const ingestServer = `rtmps://${ingestEndpoint}:443/app/`;
70 | const { attrValue: streamKey } = new ivs.CfnStreamKey(
71 | this,
72 | 'dvr-streamkey',
73 | { channelArn }
74 | );
75 |
76 | // IAM policy statement with GetStream permissions to the IVS channel (attached to the Lambda functions that require it)
77 | const getStreamPolicy = new iam.PolicyStatement({
78 | actions: ['ivs:GetStream'],
79 | effect: iam.Effect.ALLOW,
80 | resources: [channelArn]
81 | });
82 |
83 | // Lambda default configuration
84 | const defaultLambdaConfig = {
85 | runtime: lambda.Runtime.NODEJS_20_X,
86 | bundling: { minify: true }
87 | };
88 |
89 | /**
90 | * Lambda function invoked by an S3 Event Notification to save the latest recording-started.json file
91 | */
92 | const saveRecordingStartMetaLambda = new lambdan.NodejsFunction(
93 | this,
94 | 'SaveRecordingStartMetaHandler',
95 | {
96 | ...defaultLambdaConfig,
97 | entry: getLambdaEntryPath('saveRecordingStartMeta')
98 | }
99 | );
100 |
101 | // Grant the Lambda execution role Read and Put permissions to the VOD S3 bucket
102 | bucket.grantRead(saveRecordingStartMetaLambda, '*/recording-started.json');
103 | bucket.grantPut(
104 | saveRecordingStartMetaLambda,
105 | 'recording-started-latest.json'
106 | );
107 |
108 | // Grant the Lambda execution role GetStream permissions to the IVS channel
109 | saveRecordingStartMetaLambda.addToRolePolicy(getStreamPolicy);
110 |
111 | // Add an S3 Event Notification that invokes the saveRecordingStartMeta Lambda function when a recording-started.json object is created in the VOD S3 bucket
112 | bucket.addEventNotification(
113 | s3.EventType.OBJECT_CREATED_PUT,
114 | new s3n.LambdaDestination(saveRecordingStartMetaLambda),
115 | { suffix: 'recording-started.json' }
116 | );
117 |
118 | /**
119 | * Lambda(at)Edge function triggered on Origin Requests to retrieve the recording-started-latest.json metadata file
120 | */
121 | const getLatestRecordingStartMetaLambda = new lambdan.NodejsFunction(
122 | this,
123 | 'GetLatestRecordingStartMetaHandler',
124 | {
125 | ...defaultLambdaConfig,
126 | entry: getLambdaEntryPath('getLatestRecordingStartMeta')
127 | }
128 | );
129 |
130 | // Grant the Lambda execution role Read permissions to the VOD S3 bucket
131 | bucket.grantRead(
132 | getLatestRecordingStartMetaLambda,
133 | 'recording-started-latest.json'
134 | );
135 | bucket.grantRead(getLatestRecordingStartMetaLambda, '*/playlist.m3u8');
136 |
137 | // Grant the Lambda execution role GetStream permissions to the IVS channel
138 | getLatestRecordingStartMetaLambda.addToRolePolicy(getStreamPolicy);
139 |
140 | /**
141 | * Origin Access Identity (OAI) that CloudFront will use to access the S3 bucket
142 | */
143 | const oai = new cloudfront.OriginAccessIdentity(this, 'vod-oai');
144 | const origin = new origins.S3Origin(bucket, {
145 | originAccessIdentity: oai,
146 | customHeaders: {
147 | 'vod-record-bucket-name': vodBucketName,
148 | 'channel-arn': channelArn
149 | }
150 | });
151 |
152 | /**
153 | * Custom Cache Policy to allow max-age caching values between 0 seconds and 1 year
154 | */
155 | const byteRangeVariantCachePolicy = new cloudfront.CachePolicy(
156 | this,
157 | 'VOD-ByteRangeVariantCaching',
158 | {
159 | cachePolicyName: 'VOD-ByteRangeVariantCaching',
160 | comment: 'Policy for VOD Byte Range Variant Origin',
161 | defaultTtl: Duration.seconds(12),
162 | maxTtl: Duration.days(365),
163 | minTtl: Duration.seconds(0),
164 | enableAcceptEncodingGzip: true
165 | }
166 | );
167 |
168 | /**
169 | * Custom Response Headers Policy to allow only the required origins for CORS requests
170 | */
171 | const responseHeadersPolicy = new cloudfront.ResponseHeadersPolicy(
172 | this,
173 | 'VOD-ResponseHeaders',
174 | {
175 | responseHeadersPolicyName: 'VOD-ResponseHeaders',
176 | comment: 'Allows only the required origins for CORS requests',
177 | corsBehavior: {
178 | accessControlAllowCredentials: false,
179 | accessControlAllowHeaders: ['*'],
180 | accessControlAllowMethods: ['GET'],
181 | accessControlAllowOrigins: allowedOrigins,
182 | originOverride: true
183 | }
184 | }
185 | );
186 |
187 | /**
188 | * CloudFront Distribution for accessing video content from the VOD S3 Origin
189 | */
190 | const distribution = new cloudfront.Distribution(this, 'vod-cdn', {
191 | // Default caching behaviour for fetching video content files (.ts) directly from the VOD S3 bucket
192 | defaultBehavior: {
193 | origin,
194 | originRequestPolicy: cloudfront.OriginRequestPolicy.CORS_S3_ORIGIN,
195 | responseHeadersPolicy
196 | },
197 | additionalBehaviors: {
198 | // Caching behaviour for fetching a byte range variant file from the VOD S3 bucket
199 | '*/byte-range-variant.m3u8': {
200 | origin,
201 | originRequestPolicy: cloudfront.OriginRequestPolicy.CORS_S3_ORIGIN,
202 | responseHeadersPolicy,
203 | cachePolicy: byteRangeVariantCachePolicy
204 | },
205 | // Caching behaviour for invoking a Lambda@Edge function on Origin Requests to fetch the recording-started-latest.json metadata file from the VOD S3 bucket with caching DISABLED
206 | '/recording-started-latest.json': {
207 | origin,
208 | originRequestPolicy: cloudfront.OriginRequestPolicy.CORS_S3_ORIGIN,
209 | responseHeadersPolicy,
210 | cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
211 | edgeLambdas: [
212 | {
213 | functionVersion: getLatestRecordingStartMetaLambda.currentVersion,
214 | eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST
215 | }
216 | ]
217 | }
218 | }
219 | });
220 | const { domainName } = distribution;
221 |
222 | /**
223 | * Stack Outputs
224 | */
225 | new CfnOutput(this, 'ingestServer', { value: ingestServer });
226 | new CfnOutput(this, 'streamKey', { value: streamKey });
227 | new CfnOutput(this, 'playbackUrl', { value: playbackUrl });
228 | new CfnOutput(this, 'distributionDomainName', { value: domainName });
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/cdk/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cdk",
3 | "version": "0.1.0",
4 | "bin": {
5 | "cdk": "bin/cdk.js"
6 | },
7 | "scripts": {
8 | "build": "tsc",
9 | "watch": "tsc -w",
10 | "cdk": "cdk"
11 | },
12 | "devDependencies": {
13 | "@types/aws-lambda": "^8.10.141",
14 | "@types/node": "^20.14.10",
15 | "aws-cdk": "^2.148.0",
16 | "esbuild": "^0.23.0",
17 | "ts-node": "^10.9.1",
18 | "typescript": "^5.5.3"
19 | },
20 | "dependencies": {
21 | "@aws-sdk/client-ivs": "^3.613.0",
22 | "@aws-sdk/client-s3": "^3.613.0",
23 | "aws-cdk-lib": "^2.148.0",
24 | "constructs": "^10.2.44",
25 | "source-map-support": "^0.5.21"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/cdk/sanitize-output.js:
--------------------------------------------------------------------------------
1 | const { readFileSync, writeFileSync } = require('fs');
2 |
3 | const inputPath = 'temp_out.json';
4 | const json = readFileSync(inputPath);
5 | const output = JSON.parse(json);
6 |
7 | const { distributionDomainName } = output.DVRdemoStack;
8 | const publicCdkOutput = { distributionDomainName };
9 |
10 | const outputPath = '../web-ui/src/cdk_output.json';
11 | writeFileSync(outputPath, JSON.stringify(publicCdkOutput));
12 |
--------------------------------------------------------------------------------
/cdk/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2018",
4 | "module": "commonjs",
5 | "lib": ["es2018", "dom"],
6 | "declaration": true,
7 | "strict": true,
8 | "noImplicitAny": true,
9 | "strictNullChecks": true,
10 | "noImplicitThis": true,
11 | "alwaysStrict": true,
12 | "noUnusedLocals": false,
13 | "noUnusedParameters": false,
14 | "noImplicitReturns": true,
15 | "noFallthroughCasesInSwitch": false,
16 | "inlineSourceMap": true,
17 | "inlineSources": true,
18 | "experimentalDecorators": true,
19 | "strictPropertyInitialization": false,
20 | "resolveJsonModule": true,
21 | "esModuleInterop": true,
22 | "typeRoots": ["./node_modules/@types"]
23 | },
24 | "exclude": ["node_modules", "cdk.out"]
25 | }
26 |
--------------------------------------------------------------------------------
/docs/multi-channel-architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/amazon-ivs-dvr-web-demo/a2682c616b8f35f729d78903123b62fb77f607d0/docs/multi-channel-architecture.png
--------------------------------------------------------------------------------
/docs/multi-channel.md:
--------------------------------------------------------------------------------
1 | # Multi-channel Live-to-VOD
2 |
3 | The current Live-to-VOD (DVR) demo has been designed to support a single Amazon IVS channel. However, with some modifications to the infrastructure, it is possible to expand the functionality to support multiple Amazon IVS channels while keeping most of the existing functional components intact.
4 |
5 | This guide does not make any assumptions for how the multiple Amazon IVS channels are created and configured; this implementation detail is left to the reader. Instead, this guide is an overview of the necessary changes to adapt the Live-to-VOD system for multi-channel support.
6 |
7 | **1. Recording Configuration and Amazon S3 Bucket Structure**
8 |
9 | - The existing recording configuration can be utilized to save recordings from multiple Amazon IVS channels to the same Amazon S3 bucket.
10 | - Amazon IVS automatically creates a dedicated path within the S3 bucket for each channel, ensuring that recordings from different channels are stored separately.
11 |
12 | **2. Modifications to the `SaveRecordingStartMeta` Lambda Function**
13 |
14 | - The `SaveRecordingStartMeta` Lambda function currently modifies and saves the latest `recording-started.json` file for a single Amazon IVS channel.
15 | - To support multiple channels, the function needs to be modified to handle recording start metadata from all the configured channels.
16 | - When a live stream starts for any of the channels, an `ObjectCreated` S3 event notification will be triggered, invoking the `SaveRecordingStartMeta` Lambda function for each of the channels that went live.
17 | - The Lambda function should save the recording metadata for each channel separately, using appropriate naming conventions to distinguish between the files for different channels. For instance, you may adopt the naming convention of appending the channel ID to the end of the file name (i.e. `recording-started-{CHANNEL_ID}.json`).
18 |
19 | **3. Modifications to the `GetLatestRecordingStartMeta` Lambda@Edge Function**
20 |
21 | - The `GetLatestRecordingStartMeta` Lambda@Edge function currently returns the recording metadata for the single Amazon IVS channel in use.
22 | - To accommodate multiple channels, the function should be updated to accept a parameter specifying the desired channel (i.e. the channel ID), rather than using the `channel-arn` custom header to identify the channel.
23 | - Based on the provided channel parameter, the function should retrieve and return the recording metadata specific to that particular channel. For instance, following the channel ID naming convention from step 2, we can build the respective channel ARN and use it to retrieve metadata about the active session.
24 |
25 | **4. CloudFront Distribution and Behavior Patterns**
26 |
27 | - The existing CloudFront distribution can be used to support multiple Amazon IVS channels without requiring separate distributions for each channel.
28 | - However, the current `/recording-started-latest.json` behavior pattern may need to be adjusted to handle the saved metadata file names for each channel.
29 | - The behavior pattern should be modified to match the naming convention used for the metadata files of each channel, ensuring that the correct file is served based on the requested channel. For instance, following the channel ID naming convention from step 2, the behavior pattern would need to be changed to `/recording-started-*.json`
30 |
31 |
32 |
33 | 
34 |
--------------------------------------------------------------------------------
/web-ui/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["react-app", "plugin:jsx-a11y/recommended"],
3 | "plugins": ["jsx-a11y"],
4 | "env": {
5 | "worker": true
6 | },
7 | "rules": {
8 | "no-unused-vars": "error",
9 | "react-hooks/exhaustive-deps": "error",
10 | "react/prop-types": "error",
11 | "react/require-default-props": [
12 | 2,
13 | {
14 | "functions": "defaultArguments"
15 | }
16 | ],
17 | "react/no-unused-prop-types": "error"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/web-ui/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # CDK stack output
4 | */cdk_output.json
5 |
6 | # dependencies
7 | /node_modules
8 | /.pnp
9 | .pnp.js
10 |
11 | # testing
12 | /coverage
13 |
14 | # production
15 | /build
16 |
17 | # misc
18 | .DS_Store
19 | .env
20 | .env.local
21 | .env.development.local
22 | .env.test.local
23 | .env.production.local
24 |
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 |
--------------------------------------------------------------------------------
/web-ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web-ui",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^18.3.1",
7 | "react-dom": "^18.3.1",
8 | "react-scripts": "^5.0.1",
9 | "react-transition-group": "^4.4.5",
10 | "swr": "^2.2.5",
11 | "web-vitals": "^2.1.4"
12 | },
13 | "scripts": {
14 | "start": "react-scripts start",
15 | "build": "react-scripts build",
16 | "test": "react-scripts test",
17 | "eject": "react-scripts eject"
18 | },
19 | "eslintConfig": {
20 | "extends": [
21 | "react-app",
22 | "react-app/jest"
23 | ]
24 | },
25 | "browserslist": {
26 | "production": [
27 | ">0.2%",
28 | "not dead",
29 | "not op_mini all"
30 | ],
31 | "development": [
32 | "last 1 chrome version",
33 | "last 1 firefox version",
34 | "last 1 safari version"
35 | ]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/web-ui/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/amazon-ivs-dvr-web-demo/a2682c616b8f35f729d78903123b62fb77f607d0/web-ui/public/favicon.ico
--------------------------------------------------------------------------------
/web-ui/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Amazon IVS DVR Demo
18 |
19 |
20 |
21 |
22 |
23 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/web-ui/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/amazon-ivs-dvr-web-demo/a2682c616b8f35f729d78903123b62fb77f607d0/web-ui/public/logo192.png
--------------------------------------------------------------------------------
/web-ui/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/amazon-ivs-dvr-web-demo/a2682c616b8f35f729d78903123b62fb77f607d0/web-ui/public/logo512.png
--------------------------------------------------------------------------------
/web-ui/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/web-ui/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/web-ui/src/App.css:
--------------------------------------------------------------------------------
1 | .app {
2 | align-items: center;
3 | background-color: var(--color-zinc-200);
4 | display: flex;
5 | justify-content: center;
6 | min-height: 100vh;
7 | }
8 |
9 | @media (prefers-color-scheme: dark) {
10 | .app {
11 | background-color: var(--color-zinc-800);
12 | }
13 | }
14 |
15 | @media (max-width: 875px) {
16 | .app {
17 | background-color: var(--color-black);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/web-ui/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import './App.css';
4 | import ControlsProvider from './contexts/Controls/provider';
5 | import MobileBreakpointProvider from './contexts/MobileBreakpoint/provider';
6 | import PlaybackProvider from './contexts/Playback/provider';
7 | import SPPlaybackProvider from './contexts/SinglePlayerPlayback/provider';
8 | import Player from './Player';
9 | import SinglePlayer from './SinglePlayer';
10 |
11 | const PLAYER_MODES = {
12 | SINGLE: 'SINGLE',
13 | DOUBLE: 'DOUBLE'
14 | };
15 | const PLAYER_MODE =
16 | window.location.pathname === '/alt'
17 | ? PLAYER_MODES.SINGLE
18 | : PLAYER_MODES.DOUBLE;
19 |
20 | const App = () => (
21 |