Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ivs): support recording configuration for channel #31899

Merged
merged 12 commits into from
Nov 7, 2024
87 changes: 87 additions & 0 deletions packages/@aws-cdk/aws-ivs-alpha/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,90 @@ const myChannel = new ivs.Channel(this, 'Channel', {
authorized: true, // default value is false
});
```

## Recording Configurations

An Amazon IVS Recording Configuration stores settings that specify how a channel's live streams should be recorded.
You can configure video quality, thumbnail generation, and where recordings are stored in Amazon S3.

For more information about IVS recording, see [IVS Auto-Record to Amazon S3 | Low-Latency Streaming](https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/record-to-s3.html).

You can create a recording configuration:

```ts
// create an S3 bucket for storing recordings
const recordingBucket = new s3.Bucket(this, 'RecordingBucket');

// create a basic recording configuration
const recordingConfiguration = new ivs.RecordingConfiguration(this, 'RecordingConfiguration', {
bucket: recordingBucket,
});
```

### Renditions of a Recording

When you stream content to an Amazon IVS channel, auto-record-to-s3 uses the source video to generate multiple renditions.

For more information, see [Discovering the Renditions of a Recording](https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/record-to-s3.html#r2s3-recording-renditions).

```ts
declare const recordingBucket: s3.Bucket;

const recordingConfiguration= new ivs.RecordingConfiguration(this, 'RecordingConfiguration', {
bucket: recordingBucket,

// set rendition configuration
renditionConfiguration: ivs.RenditionConfiguration.custom([ivs.Resolution.HD, ivs.Resolution.SD]),
});
```

### Thumbnail Generation

You can enable or disable the recording of thumbnails for a live session and modify the interval at which thumbnails are generated for the live session.

Thumbnail intervals may range from 1 second to 60 seconds; by default, thumbnail recording is enabled, at an interval of 60 seconds.

For more information, see [Thumbnails](https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/record-to-s3.html#r2s3-thumbnails).

```ts
declare const recordingBucket: s3.Bucket;

const recordingConfiguration = new ivs.RecordingConfiguration(this, 'RecordingConfiguration', {
bucket: recordingBucket,

// set thumbnail settings
thumbnailConfiguration: ivs.ThumbnailConfiguration.interval(ivs.Resolution.HD, [ivs.Storage.LATEST, ivs.Storage.SEQUENTIAL], Duration.seconds(30)),
});
```

### Merge Fragmented Streams

The `recordingReconnectWindow` property allows you to specify a window of time (in seconds) during which, if your stream is interrupted and a new stream is started, Amazon IVS tries to record to the same S3 prefix as the previous stream.

In other words, if a broadcast disconnects and then reconnects within the specified interval, the multiple streams are considered a single broadcast and merged together.

For more information, see [Merge Fragmented Streams](https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/record-to-s3.html#r2s3-merge-fragmented-streams).

```ts
declare const recordingBucket: s3.Bucket;

const recordingConfiguration= new ivs.RecordingConfiguration(this, 'RecordingConfiguration', {
bucket: recordingBucket,

// set recording reconnect window
recordingReconnectWindow: Duration.seconds(60),
});
```

### Attaching Recording Configuration to a Channel

To enable recording for a channel, specify the recording configuration when creating the channel:

```ts
declare const recordingConfiguration: ivs.RecordingConfiguration;

const channel = new ivs.Channel(this, 'Channel', {
// set recording configuration
recordingConfiguration: recordingConfiguration,
});
```
9 changes: 9 additions & 0 deletions packages/@aws-cdk/aws-ivs-alpha/lib/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Lazy, Names } from 'aws-cdk-lib/core';
import { Construct } from 'constructs';
import { CfnChannel } from 'aws-cdk-lib/aws-ivs';
import { StreamKey } from './stream-key';
import { IRecordingConfiguration } from './recording-configuration';

/**
* Represents an IVS Channel
Expand Down Expand Up @@ -153,6 +154,13 @@ export interface ChannelProps {
* @default - Preset.HIGHER_BANDWIDTH_DELIVERY if channelType is ADVANCED_SD or ADVANCED_HD, none otherwise
*/
readonly preset?: Preset;

/**
* A recording configuration for the channel.
*
* @default - recording is disabled
*/
readonly recordingConfiguration?: IRecordingConfiguration;
}

/**
Expand Down Expand Up @@ -223,6 +231,7 @@ export class Channel extends ChannelBase {
name: this.physicalName,
type: props.type,
preset,
recordingConfigurationArn: props.recordingConfiguration?.recordingConfigurationArn,
});

this.channelArn = resource.attrArn;
Expand Down
4 changes: 4 additions & 0 deletions packages/@aws-cdk/aws-ivs-alpha/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
export * from './channel';
export * from './playback-key-pair';
export * from './recording-configuration';
export * from './rendition-configuration';
export * from './stream-key';
export * from './thumbnail-configuration';
export * from './util';

// AWS::IVS CloudFormation Resources:
210 changes: 210 additions & 0 deletions packages/@aws-cdk/aws-ivs-alpha/lib/recording-configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { CfnRecordingConfiguration } from 'aws-cdk-lib/aws-ivs';
import { IBucket } from 'aws-cdk-lib/aws-s3';
import { Duration, Fn, IResource, Resource, Stack, Token } from 'aws-cdk-lib/core';
import { Construct } from 'constructs';
import { RenditionConfiguration } from './rendition-configuration';
import { ThumbnailConfiguration } from './thumbnail-configuration';

/**
* Properties of the IVS Recording configuration
*/
export interface RecordingConfigurationProps {
/**
* S3 bucket where recorded videos will be stored.
*/
readonly bucket: IBucket;

/**
* The name of the Recording configuration.
* The value does not need to be unique.
*
* @default - auto generate
*/
readonly recordingConfigurationName?: string;

/**
* If a broadcast disconnects and then reconnects within the specified interval,
* the multiple streams will be considered a single broadcast and merged together.
*
* `recordingReconnectWindow` must be between 0 and 300 seconds
*
* @default - 0 seconds (means disabled)
*/
readonly recordingReconnectWindow?: Duration;

/**
* A rendition configuration describes which renditions should be recorded for a stream.
*
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ivs-recordingconfiguration-renditionconfiguration.html
*
* @default - no rendition configuration
*/
readonly renditionConfiguration?: RenditionConfiguration;

/**
* A thumbnail configuration enables/disables the recording of thumbnails for a live session and controls the interval at which thumbnails are generated for the live session.
*
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ivs-recordingconfiguration-thumbnailconfiguration.html
*
* @default - no thumbnail configuration
*/
readonly thumbnailConfiguration?:ThumbnailConfiguration;
}
Comment on lines +35 to +52
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for making the requested changes @mazyu36! I notice now though that the properties that are contained within the renditionConfiguration and thumbnailConfiguration no longer have defaults because the new default is that the configuration is not provided. Is this intentional or should we still set a default for these properties somewhere?

Copy link
Contributor Author

@mazyu36 mazyu36 Nov 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@paulhcsun
Thanks.
The lack of default values is intentional. Is it correct to understand that you're suggesting we should document somewhere what the default values are when @param is omitted? If so, I plan to add this information to the @param description.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added an explanation regarding the default value. Please let me know if it doesn’t match your intention.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes the documentation is appreciated! But also I wanted to check if this default is something that gets set by the service/cloudformation fills it in, since we're not explicitly setting it as the default anywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks.
Those default values are provided by CFn.
So I've not explicitly set.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, I'm good to approve then. Thank you for working on this @mazyu36!!


/**
* Represents the IVS Recording configuration.
*/
export interface IRecordingConfiguration extends IResource {
/**
* The ID of the Recording configuration.
* @attribute
*/
readonly recordingConfigurationId: string;

/**
* The ARN of the Recording configuration.
* @attribute
*/
readonly recordingConfigurationArn: string;
}

/**
* The IVS Recording configuration
*
* @resource AWS::IVS::RecordingConfiguration
*/
export class RecordingConfiguration extends Resource implements IRecordingConfiguration {
/**
* Imports an IVS Recording Configuration from attributes.
*/
public static fromRecordingConfigurationId(scope: Construct, id: string,
recordingConfigurationId: string): IRecordingConfiguration {

class Import extends Resource implements IRecordingConfiguration {
public readonly recordingConfigurationId = recordingConfigurationId;
public readonly recordingConfigurationArn = Stack.of(this).formatArn({
resource: 'recording-configuration',
service: 'ivs',
resourceName: recordingConfigurationId,
});
}

return new Import(scope, id);
}

/**
* Imports an IVS Recording Configuration from its ARN
*/
public static fromArn(scope: Construct, id: string, recordingConfigurationArn: string): IRecordingConfiguration {
const resourceParts = Fn.split('/', recordingConfigurationArn);

if (!resourceParts || resourceParts.length < 2) {
throw new Error(`Unexpected ARN format: ${recordingConfigurationArn}`);
}

const recordingConfigurationId = Fn.select(1, resourceParts);

class Import extends Resource implements IRecordingConfiguration {
public readonly recordingConfigurationId = recordingConfigurationId;
public readonly recordingConfigurationArn = recordingConfigurationArn;
}

return new Import(scope, id);
}

/**
* The ID of the Recording configuration.
* @attribute
*/
readonly recordingConfigurationId: string;

/**
* The ARN of the Recording configuration.
* @attribute
*/
readonly recordingConfigurationArn: string;

private readonly props: RecordingConfigurationProps;

public constructor(scope: Construct, id: string, props: RecordingConfigurationProps) {
super(scope, id, {
physicalName: props.recordingConfigurationName,
});

this.props = props;

this.validateRecordingConfigurationName();
this.validateRecordingReconnectWindowSeconds();

const resource = new CfnRecordingConfiguration(this, 'Resource', {
destinationConfiguration: {
s3: {
bucketName: this.props.bucket.bucketName,
},
},
name: this.props.recordingConfigurationName,
recordingReconnectWindowSeconds: this.props.recordingReconnectWindow?.toSeconds(),
renditionConfiguration: this._renderRenditionConfiguration(),
thumbnailConfiguration: this._renderThumbnailConfiguration(),
});

this.recordingConfigurationId = resource.ref;
this.recordingConfigurationArn = resource.attrArn;
}

private _renderRenditionConfiguration(): CfnRecordingConfiguration.RenditionConfigurationProperty | undefined {
if (!this.props.renditionConfiguration) {
return;
}

return {
renditions: this.props.renditionConfiguration.renditions,
renditionSelection: this.props.renditionConfiguration.renditionSelection,
};
};

private _renderThumbnailConfiguration(): CfnRecordingConfiguration.ThumbnailConfigurationProperty | undefined {
if (!this.props.thumbnailConfiguration) {
return;
}

return {
recordingMode: this.props.thumbnailConfiguration.recordingMode,
resolution: this.props.thumbnailConfiguration.resolution,
storage: this.props.thumbnailConfiguration.storage,
targetIntervalSeconds: this.props.thumbnailConfiguration.targetInterval?.toSeconds(),
};
};

private validateRecordingConfigurationName(): undefined {
const recordingConfigurationName = this.props.recordingConfigurationName;

if (recordingConfigurationName == undefined || Token.isUnresolved(recordingConfigurationName)) {
return;
}

if (!/^[a-zA-Z0-9-_]*$/.test(recordingConfigurationName)) {
throw new Error(`\`recordingConfigurationName\` must consist only of alphanumeric characters, hyphens or underbars, got: ${recordingConfigurationName}.`);
}

if (recordingConfigurationName.length > 128) {
throw new Error(`\`recordingConfigurationName\` must be less than or equal to 128 characters, got: ${recordingConfigurationName.length} characters.`);
}
};

private validateRecordingReconnectWindowSeconds(): undefined {
const recordingReconnectWindow = this.props.recordingReconnectWindow;

if (recordingReconnectWindow === undefined || Token.isUnresolved(recordingReconnectWindow)) {
return;
}

if (0 < recordingReconnectWindow.toMilliseconds() && recordingReconnectWindow.toMilliseconds() < Duration.seconds(1).toMilliseconds()) {
throw new Error(`\`recordingReconnectWindow\` must be between 0 and 300 seconds, got ${recordingReconnectWindow.toMilliseconds()} milliseconds.`);
}

if (recordingReconnectWindow.toSeconds() > 300) {
throw new Error(`\`recordingReconnectWindow\` must be between 0 and 300 seconds, got ${recordingReconnectWindow.toSeconds()} seconds.`);
}
};
}
55 changes: 55 additions & 0 deletions packages/@aws-cdk/aws-ivs-alpha/lib/rendition-configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Resolution } from './util';

/**
* Rendition selection mode.
*/
export enum RenditionSelection {
/**
* Record all available renditions.
*/
ALL = 'ALL',

/**
* Does not record any video. This option is useful if you just want to record thumbnails.
*/
NONE = 'NONE',

/**
* Select a subset of video renditions to record.
*/
CUSTOM = 'CUSTOM',
}

/**
* Rendition configuration for IVS Recording configuration
*/
export class RenditionConfiguration {
/**
* Record all available renditions.
*/
public static all(): RenditionConfiguration {
return new RenditionConfiguration(RenditionSelection.ALL);
}

/**
* Does not record any video.
*/
public static none(): RenditionConfiguration {
return new RenditionConfiguration(RenditionSelection.NONE);
}

/**
* Record a subset of video renditions.
*
* @param renditions A list of which renditions are recorded for a stream.
*/
public static custom(renditions: Resolution[]): RenditionConfiguration {
return new RenditionConfiguration(RenditionSelection.CUSTOM, renditions);
}

/**
* @param renditionSelection The set of renditions are recorded for a stream.
* @param renditions A list of which renditions are recorded for a stream. If you do not specify this property, no resolution is selected.
*/
private constructor(public readonly renditionSelection: RenditionSelection, public readonly renditions?: Resolution[]) { }
}
Loading