Skip to content

Commit 0164a93

Browse files
drewhan90andreido
authored andcommitted
feat: redux - stream manager, channel and shared redux slices (#40)
1 parent f29840a commit 0164a93

File tree

155 files changed

+18394
-24120
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

155 files changed

+18394
-24120
lines changed

README.md

+29-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ Deploying the CDK stack will:
4242

4343
## Features
4444

45+
<<<<<<< HEAD
46+
=======
4547

48+
>>>>>>> fork/main
4649
### User Registration, Login and Password Reset using Amazon Cognito
4750

4851
New users can create an account from the `/register` route. Returning users can login to their account from the `/login` route. They can also reset their password at `/reset`.
@@ -268,6 +271,31 @@ The `cdk/cdk.json` file provides two configuration objects: one for the `dev` st
268271
"productLinkRegionCode": "20"
269272
```
270273

274+
- The `multitrackInputConfiguration` object configures [Amazon IVS multitrack video](https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/multitrack-video.html) streaming for a channel.
275+
276+
It's important to note that to enable multitrack, the ivsChannelType must be set to "STANDARD".
277+
278+
When `enabled` is set to true, it allows the channel to accept multiple video tracks from a single streaming source.
279+
280+
The `maximumResolution` field sets the highest input resolution allowed, with options being "SD" (480p or less), "HD" (720p or less), or "FULL_HD" (1080p or less).
281+
282+
The `policy` field determines whether multitrack input is optional ("ALLOW") or mandatory ("REQUIRE") for broadcasters connecting to the channel.
283+
284+
This configuration optimizes streaming efficiency, potentially reducing costs and improving viewer experience by enabling adaptive bitrate streaming without server-side transcoding. Remember, multitrack functionality is only available for Standard channels, so ensuring the correct channel type is crucial for this feature to work.
285+
286+
For more information on the feature, please refer to the [CDK README.md](<(./cdk/README.md#Amazon-IVS-multitrack-video)>) file.
287+
288+
Example:
289+
290+
```json
291+
"ivsChannelType": "STANDARD",
292+
"multitrackInputConfiguration": {
293+
"enabled": true,
294+
"maximumResolution": "FULL_HD",
295+
"policy": "ALLOW"
296+
}
297+
```
298+
271299
## Guides
272300

273301

@@ -330,7 +358,7 @@ To set your Product Advertising API credentials you must:
330358

331359
After deployment, through [Amazon OneLink](https://affiliate-program.amazon.com/resource-center/onelink-launch), you can best earn money via product affiliate links by redirecting international traffic to the appropriate Amazon store for their location, increasing the likelihood that they will make a purchase. To get started:
332360

333-
1. Sign up for Amazon Associates: To use Amazon OneLink, you need to be an [Amazon Associate](https://affiliate-program.amazon.com/). If you're not already signed up, go to the Amazon Associates website and create an account.
361+
1. Sign up for Amazon Associates: To use Amazon OneLink, you need to be an [Amazon Associate](https://associates.amazon.ca/). If you're not already signed up, go to the Amazon Associates website and create an account.
334362

335363
2. Enable OneLink: Once you've signed up for Amazon Associates, navigate to the 'Manage Tracking IDs' section located at the top right-hand corner
336364
of the Amazon Associates portal.

cdk/Makefile

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ PUBLISH ?= false
1010
STACK ?= UGC-$(STAGE)
1111
COGNITO_CLEANUP_SCHEDULE ?= rate(48 hours)
1212
STAGE_CLEANUP_SCHEDULE ?= rate(24 hours)
13+
## Overide STAGE_CLEANUP_SCHEDULE for prod stage
14+
ifeq ($(STAGE),prod)
15+
STAGE_CLEANUP_SCHEDULE = rate(10 minutes)
16+
endif
1317
CDK_OPTIONS = $(if $(AWS_PROFILE),$(AWS_PROFILE_FLAG)) -c stage=$(STAGE) -c publish=$(PUBLISH) -c stackName=$(STACK) -c cognitoCleanupScheduleExp="$(strip $(COGNITO_CLEANUP_SCHEDULE))" -c stageCleanupScheduleExp="$(strip $(STAGE_CLEANUP_SCHEDULE))"
1418
FE_DEPLOYMENT_STACK = UGC-Frontend-Deployment-$(STAGE)
1519
SEED_COUNT ?= 50

cdk/README.md

+24
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,27 @@ The cleanup lambda function follows these steps:
128128

129129
### Amazon FIFO SQS
130130
A FIFO queue using content body for message deduplication and a 3-minute delayed delivery, allowing the host sufficient time to rejoin if they choose to.
131+
132+
### Amazon IVS multitrack video
133+
134+
Amazon Interactive Video Service (IVS) support low-latency streaming approach called multitrack video. This technology empowers broadcasting software like OBS Studio to:
135+
- Encode and stream multiple video qualities directly from their GPU-powered computer.
136+
- Automatically configure encoder settings for the best possible stream.
137+
- Deliver a high quality Adaptive Bitrate (ABR) viewing experience.
138+
139+
Unlike single-track video streaming, which necessitates server-side transcoding, multitrack video achieves these capabilities without such requirements. Multitrack video enhances streaming efficiency and quality for content creators and viewers alike.
140+
141+
Please refer to the [Amazon IVS Multitrack Video](https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/multitrack-video.html) documentation for more information.
142+
143+
#### Required CDK configuration to enable channel multitrack input
144+
145+
Please make sure the correct configurations are set in the [cdk.json](./cdk.json) file to enable multitrack. If any of these configurations are incorrectly set, the stack deployment process will throw an error.
146+
147+
Refer to the [Amazon IVS Multitrack Video: Setup Guide](https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/multitrack-video-setup.html) for more information on how to setup the feature.
148+
149+
- ivsChannelType = "STANDARD"
150+
- multitrackInputConfiguration.enabled = true
151+
- multitrackInputConfiguration.maximumResolution = "SD" | "HD" | "FULL_HD"
152+
- multitrackInputConfiguration.policy = "ALLOW" | "REQUIRE"
153+
154+
Please note that `ContainerFormat` will be automatically set as "FRAGMENTED_MP4" when multitrack is enabled.

cdk/api/channel/authRouter/createResources.ts

+20-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import {
22
ChannelType,
33
CreateChannelCommand,
4+
MultitrackInputConfiguration,
45
TranscodePreset
56
} from '@aws-sdk/client-ivs';
67
import { CreateRoomCommand } from '@aws-sdk/client-ivschat';
78
import { FastifyReply, FastifyRequest } from 'fastify';
89

9-
import { generateDeterministicId, getUser } from '../helpers';
10+
import {
11+
ChannelConfiguration,
12+
generateDeterministicId,
13+
getMultitrackChannelInputFields,
14+
getUser
15+
} from '../helpers';
1016
import {
1117
CHANNELS_TABLE_STAGE_FIELDS,
1218
UNEXPECTED_EXCEPTION
@@ -43,12 +49,15 @@ const handler = async (
4349
// Create IVS channel
4450
const cleanedUserName = username.replace(/[^a-zA-Z0-9-_]/g, '');
4551
const channelName = `${cleanedUserName}s-channel`;
52+
const multitrackChannelInputFields = getMultitrackChannelInputFields();
53+
4654
const createChannelCommand = new CreateChannelCommand({
4755
name: channelName,
4856
type: process.env.IVS_CHANNEL_TYPE as ChannelType,
4957
preset: process.env
5058
.IVS_ADVANCED_CHANNEL_TRANSCODE_PRESET as TranscodePreset,
51-
tags: { project: process.env.PROJECT_TAG as string }
59+
tags: { project: process.env.PROJECT_TAG as string },
60+
...multitrackChannelInputFields
5261
});
5362
const { channel, streamKey } = await ivsClient.send(createChannelCommand);
5463

@@ -65,6 +74,11 @@ const handler = async (
6574
const streamKeyArn = streamKey?.arn;
6675
const playbackUrl = channel?.playbackUrl;
6776
const chatRoomArn = chatRoom.arn;
77+
const channelConfiguration: ChannelConfiguration = {
78+
type: channel?.type as ChannelType,
79+
multitrackInputConfiguration:
80+
channel?.multitrackInputConfiguration as MultitrackInputConfiguration
81+
};
6882

6983
let trackingId;
7084

@@ -109,6 +123,10 @@ const handler = async (
109123
{
110124
key: CHANNELS_TABLE_STAGE_FIELDS.STAGE_CREATION_DATE,
111125
value: null
126+
},
127+
{
128+
key: 'channelConfiguration',
129+
value: channelConfiguration
112130
}
113131
],
114132
primaryKey: { key: 'id', value: sub },

cdk/api/channel/authRouter/getUser.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ interface GetUserResponseBody extends ResponseBody {
2222
streamKeyValue?: string;
2323
username?: string;
2424
trackingId?: string;
25+
stageId?: string;
2526
}
2627

2728
const handler = async (request: FastifyRequest, reply: FastifyReply) => {
@@ -31,6 +32,8 @@ const handler = async (request: FastifyRequest, reply: FastifyReply) => {
3132
try {
3233
// Get user from channelsTable
3334
const { Item = {} } = await getUser(sub);
35+
const data = unmarshall(Item);
36+
let channelId;
3437
const {
3538
avatar,
3639
channelArn,
@@ -40,9 +43,10 @@ const handler = async (request: FastifyRequest, reply: FastifyReply) => {
4043
playbackUrl,
4144
streamKeyValue,
4245
username,
43-
trackingId
44-
} = unmarshall(Item);
45-
let channelId;
46+
trackingId,
47+
stageId,
48+
channelConfiguration
49+
} = data;
4650

4751
if (!channelArn) {
4852
throw new Error('No IVS resources have been created for this user.');
@@ -64,6 +68,8 @@ const handler = async (request: FastifyRequest, reply: FastifyReply) => {
6468
responseBody.channelAssetUrls = getChannelAssetUrls(channelAssets);
6569
responseBody.trackingId = trackingId;
6670
responseBody.channelId = channelId;
71+
responseBody.stageId = stageId;
72+
responseBody.channelConfiguration = channelConfiguration;
6773
} catch (error) {
6874
console.error(error);
6975

cdk/api/channel/authRouter/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import addToFollowingList from './addToFollowingList';
1717
import removeFromFollowingList from './removeFromFollowingList';
1818
import getFollowingChannels from './getFollowingChannels';
1919
import getLiveStatus from './getLiveStatus';
20+
import updateChannelConfig from './updateChannelConfiguration';
2021

2122
declare module '@fastify/request-context' {
2223
interface RequestContextData {
@@ -42,6 +43,7 @@ const router: FastifyPluginAsync = async (resource) => {
4243
generateImagePresignedPost
4344
);
4445

46+
resource.put('/config/update', updateChannelConfig);
4547
resource.put('/username/update', changeUsername);
4648
resource.put('/preferences/update', changeUserPreferences);
4749
resource.put('/followingList/add', addToFollowingList);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import {
2+
ChannelType,
3+
UpdateChannelCommand,
4+
TranscodePreset,
5+
MultitrackInputConfiguration
6+
} from '@aws-sdk/client-ivs';
7+
import { FastifyReply, FastifyRequest } from 'fastify';
8+
import { unmarshall } from '@aws-sdk/util-dynamodb';
9+
10+
import {
11+
areObjectsSame,
12+
ChannelConfiguration,
13+
getMultitrackChannelInputFields,
14+
getUser
15+
} from '../helpers';
16+
import { UNEXPECTED_EXCEPTION } from '../../shared/constants';
17+
import { ivsClient, updateDynamoItemAttributes } from '../../shared/helpers';
18+
import { UserContext } from '../../shared/authorizer';
19+
20+
const cdkMultitrackInputConfiguration: MultitrackInputConfiguration =
21+
JSON.parse(process.env.CHANNEL_MULTITRACK_INPUT_CONFIGURATION || '{}');
22+
23+
const handler = async (request: FastifyRequest, reply: FastifyReply) => {
24+
const { sub } = request.requestContext.get('user') as UserContext;
25+
26+
try {
27+
const { Item = {} } = await getUser(sub);
28+
const userData = unmarshall(Item);
29+
const cdkChannelConfiguration: ChannelConfiguration = {
30+
type: process.env.IVS_CHANNEL_TYPE as ChannelType,
31+
multitrackInputConfiguration: cdkMultitrackInputConfiguration
32+
};
33+
if (
34+
userData &&
35+
areObjectsSame(userData.channelConfiguration, cdkChannelConfiguration)
36+
) {
37+
reply.statusCode = 204;
38+
39+
return reply.send({
40+
message: 'Channel configuration is already up to date'
41+
});
42+
}
43+
44+
const { containerFormat, multitrackInputConfiguration } =
45+
getMultitrackChannelInputFields();
46+
47+
const updateChannelCommand = new UpdateChannelCommand({
48+
arn: userData.channelArn,
49+
type: process.env.IVS_CHANNEL_TYPE as ChannelType,
50+
preset: process.env
51+
.IVS_ADVANCED_CHANNEL_TRANSCODE_PRESET as TranscodePreset,
52+
containerFormat,
53+
multitrackInputConfiguration
54+
});
55+
const { channel } = await ivsClient.send(updateChannelCommand);
56+
57+
// Update the "channelConfiguration" field in the channel record
58+
await updateDynamoItemAttributes({
59+
attributes: [
60+
{
61+
key: 'channelConfiguration',
62+
value: {
63+
type: channel?.type,
64+
multitrackInputConfiguration: channel?.multitrackInputConfiguration
65+
}
66+
}
67+
],
68+
primaryKey: { key: 'id', value: sub },
69+
tableName: process.env.CHANNELS_TABLE_NAME as string
70+
});
71+
} catch (error) {
72+
console.error(error);
73+
74+
reply.statusCode = 500;
75+
76+
return reply.send({ __type: UNEXPECTED_EXCEPTION });
77+
}
78+
79+
reply.statusCode = 201;
80+
81+
return reply.send({});
82+
};
83+
84+
export default handler;

cdk/api/channel/helpers.ts

+63-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ import {
2424
USER_NOT_FOUND_EXCEPTION
2525
} from '../shared/constants';
2626
import { dynamoDbClient, ivsChatClient, s3Client } from '../shared/helpers';
27+
import {
28+
ChannelType,
29+
ContainerFormat,
30+
MultitrackInputConfiguration
31+
} from '@aws-sdk/client-ivs';
32+
33+
type ChannelTypeValues = (typeof ChannelType)[keyof typeof ChannelType];
34+
35+
export interface ChannelConfiguration {
36+
type: ChannelTypeValues;
37+
multitrackInputConfiguration: MultitrackInputConfiguration;
38+
}
2739

2840
export const getUser = (sub: string) => {
2941
const getItemCommand = new GetItemCommand({
@@ -147,7 +159,7 @@ export const createChatRoomToken = async (
147159
color?: string;
148160
displayName?: string;
149161
},
150-
capabilities?: (ChatTokenCapability | string)[]
162+
capabilities?: ChatTokenCapability[] | undefined
151163
) => {
152164
let chatRoomArn, bannedUserSubs;
153165
const { Items } = await getUserByUsername(chatRoomOwnerUsername);
@@ -342,3 +354,53 @@ export const processAssetPreference = (
342354
}
343355
});
344356
};
357+
358+
export const getMultitrackChannelInputFields = () => {
359+
const multitrackInputConfiguration: MultitrackInputConfiguration = JSON.parse(
360+
process.env.CHANNEL_MULTITRACK_INPUT_CONFIGURATION || '{}'
361+
);
362+
363+
if (!multitrackInputConfiguration.enabled) {
364+
return {};
365+
}
366+
367+
return {
368+
containerFormat: ContainerFormat.FragmentedMP4,
369+
multitrackInputConfiguration
370+
};
371+
};
372+
373+
/**
374+
* Compares two values to determine if they are the same.
375+
* For objects, it checks if they have the same keys and values.
376+
* For non-objects, it performs a strict equality check.
377+
*
378+
* @param {*} obj1 - The first value to compare.
379+
* @param {*} obj2 - The second value to compare.
380+
* @returns {boolean} - Returns true if the values are the same; otherwise, false.
381+
*/
382+
export const areObjectsSame = <T extends Record<string, any>>(
383+
obj1: T,
384+
obj2: T
385+
): boolean => {
386+
// Handle null/undefined and non-object cases
387+
if (obj1 === obj2) return true;
388+
if (
389+
obj1 == null ||
390+
obj2 == null ||
391+
typeof obj1 !== 'object' ||
392+
typeof obj2 !== 'object'
393+
)
394+
return false;
395+
396+
const keys1 = Object.keys(obj1);
397+
const keys2 = Object.keys(obj2);
398+
399+
// Check if the number of keys is the same
400+
if (keys1.length !== keys2.length) return false;
401+
402+
// Check if all keys and values are the same
403+
return keys1.every(
404+
(key) => keys2.includes(key) && areObjectsSame(obj1[key], obj2[key])
405+
);
406+
};

cdk/api/channel/unauthRouter/createChatToken.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const handler = async (
5656

5757
try {
5858
let token, sessionExpirationTime, tokenExpirationTime;
59-
const capabilities: ChatTokenCapabilityType[] = [];
59+
const capabilities: ChatTokenCapability[] | undefined = [];
6060

6161
if (!viewerSub) {
6262
({ token, sessionExpirationTime, tokenExpirationTime } =

0 commit comments

Comments
 (0)