Skip to content

Commit

Permalink
feat(ivs): support recording configuration for channel (aws#31899)
Browse files Browse the repository at this point in the history
### Issue # (if applicable)

Closes aws#31780.

### Reason for this change
To use recording configuration for IVS channel.



### Description of changes
* Add `RecordingConfiguration` Construct.
* Add `recordingConfiguration` property to the Channel.



### Description of how you validated changes
Add unit tests and integ test.



### Checklist
- [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md)

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
mazyu36 authored and Leonardo Gama committed Nov 13, 2024
1 parent 2f409d4 commit 3ff07f3
Show file tree
Hide file tree
Showing 19 changed files with 1,751 additions and 0 deletions.
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;
}

/**
* 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

0 comments on commit 3ff07f3

Please sign in to comment.