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: corruption to delay HLS media manifest responses #45

Merged
merged 1 commit into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ Delay Corruption:
```typescript
{
i?: number | "*", // index of target segment in playlist. If "*", then target all segments. (Starts on 0 for HLS / 1 for MPEG-DASH)
l?: number | "*", // index of ABR rung to delay media playlist. (Starts on 1 and only applicable to HLS)
sq?: number | "*", // media sequence number of target segment in playlist. If "*", then target all segments
rsq?: number, // relative sequence number from where a livestream is currently at
ms?: number, // time to delay in milliseconds
Expand Down Expand Up @@ -191,7 +192,13 @@ https://chaos-proxy.prod.eyevinn.technology/api/v2/manifests/hls/proxy-master.m3
7. LIVE: With response of status code 404 on segment with sequence number 105:

```
https://chaos-proxy.prod.eyevinn.technology/api/v2/manifests/hls/proxy-master.m3u8?url=https://cph-p2p-msl.akamaized.net/hls/live/2000341/test/master.m3u8&statusCode=[{sq:105,code:400}]
https://chaos-proxy.prod.eyevinn.technology/api/v2/manifests/hls/proxy-master.m3u8?url=https://demo.vc.eyevinn.technology/channels/demo/master.m3u8&statusCode=[{sq:105,code:400}]
```

8. LIVE: Delay response of media manifest ladder 1 and 2 with 500 ms

```
https://chaos-proxy.prod.eyevinn.technology/api/v2/manifests/hls/proxy-master.m3u8?url=https://demo.vc.eyevinn.technology/channels/demo/master.m3u8&delay=[{l:1,ms:500},{l:2,ms:500}]
```

### Example corruptions on MPEG-DASH Streams
Expand Down
18 changes: 15 additions & 3 deletions src/manifests/handlers/hls/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import path from 'path';
import hlsManifestUtils from '../../utils/hlsManifestUtils';
import { corruptorConfigUtils } from '../../utils/configs';

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

export default async function hlsMediaHandler(
event: ALBEvent
): Promise<ALBResult> {
Expand Down Expand Up @@ -56,9 +58,8 @@ export default async function hlsMediaHandler(
.register(timeoutSCC)
.register(throttleSCC);

const [error, allMutations] = configUtils.getAllManifestConfigs(
mediaM3U.get('mediaSequence')
);
const [error, allMutations, levelMutations] =
configUtils.getAllManifestConfigs(mediaM3U.get('mediaSequence'));
if (error) {
return generateErrorResponse(error);
}
Expand All @@ -70,6 +71,17 @@ export default async function hlsMediaHandler(
allMutations
);

if (levelMutations) {
// apply media manifest Delay
const level = reqQueryParams.get('level')
? Number(reqQueryParams.get('level'))
: undefined;
if (level && levelMutations.get(level)) {
const delay = Number(levelMutations.get(level).get('delay').fields?.ms);
console.log(`Applying ${delay}ms delay to ${query.url}`);
await sleep(delay);
}
}
return {
statusCode: 200,
headers: {
Expand Down
52 changes: 47 additions & 5 deletions src/manifests/utils/configs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import {
corruptorConfigUtils,
SegmentCorruptorQueryConfig,
CorruptorConfig,
CorruptorIndexMap
CorruptorIndexMap,
CorruptorLevelMap
} from './configs';

describe('configs', () => {
Expand Down Expand Up @@ -89,18 +90,32 @@ describe('configs', () => {
it('should handle matching config with url query params', () => {
// Arrange
const configs = corruptorConfigUtils(
new URLSearchParams('test1=[{i:0,ms:150}]&test2=[{i:1,ms:250}]')
new URLSearchParams(
'test1=[{i:0,ms:150}]&test2=[{i:1,ms:250}]&test3=[{l:1,ms:400}]'
)
);
const config1: SegmentCorruptorQueryConfig = {
name: 'test1',
getManifestConfigs: () => [null, [{ i: 0, fields: { ms: 150 } }]],
getSegmentConfigs: () => [null, { fields: null }]
};
const config2: SegmentCorruptorQueryConfig = {
name: 'test2',
getManifestConfigs: () => [null, [{ i: 1, fields: { ms: 250 } }]],
getSegmentConfigs: () => [null, { fields: null }]
};
const config3: SegmentCorruptorQueryConfig = {
name: 'test3',
getManifestConfigs: () => [null, [{ l: 1, fields: { ms: 400 } }]],
getSegmentConfigs: () => [null, { fields: null }]
};

// Act
configs.register(config1);
const [err, actual] = configs.getAllManifestConfigs(0);
const expected = new CorruptorIndexMap([
configs.register(config2);
configs.register(config3);
const [err, actualIndex, actualLevel] = configs.getAllManifestConfigs(0);
const expectedIndex = new CorruptorIndexMap([
[
0,
new Map([
Expand All @@ -112,11 +127,38 @@ describe('configs', () => {
}
]
])
],
[
1,
new Map([
[
'test2',
{
fields: { ms: 250 },
i: 1
}
]
])
]
]);
const expectedLevel = new CorruptorLevelMap([
[
1,
new Map([
[
'test3',
{
fields: { ms: 400 },
l: 1
}
]
])
]
]);
// Assert
expect(err).toBeNull();
expect(actual).toEqual(expected);
expect(actualIndex).toEqual(expectedIndex);
expect(actualLevel).toEqual(expectedLevel);
});
});

Expand Down
34 changes: 31 additions & 3 deletions src/manifests/utils/configs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { ServiceError, TargetIndex } from '../../shared/types';
import { ServiceError, TargetIndex, TargetLevel } from '../../shared/types';

// export type SegmentCorruptorConfigItem = {
// index: TargetIndex;
// level: TargetLevel;
// seq: TargetIndex;
// name: string;
// queryValue: string;
Expand All @@ -20,6 +21,7 @@ export interface SegmentCorruptorQueryConfig {
// TODO sequence might not be relevant as a generic property
export interface CorruptorConfig {
i?: TargetIndex;
l?: TargetLevel;
sq?: TargetIndex;
br?: TargetIndex;
/**
Expand All @@ -37,6 +39,7 @@ export interface CorruptorConfig {
}

export type IndexedCorruptorConfigMap = Map<TargetIndex, CorruptorConfigMap>;
export type LevelCorruptorConfigMap = Map<TargetLevel, CorruptorConfigMap>;

export type CorruptorConfigMap = Map<string, CorruptorConfig>;

Expand All @@ -50,7 +53,11 @@ export interface CorruptorConfigUtils {
getAllManifestConfigs: (
mseq?: number,
isDash?: boolean
) => [ServiceError | null, IndexedCorruptorConfigMap | null];
) => [
ServiceError | null,
IndexedCorruptorConfigMap | null,
LevelCorruptorConfigMap | null
];

getAllSegmentConfigs: () => [ServiceError | null, CorruptorConfigMap | null];

Expand Down Expand Up @@ -81,6 +88,23 @@ export class CorruptorIndexMap extends Map<TargetIndex, CorruptorConfigMap> {
}
}

export class CorruptorLevelMap extends Map<TargetLevel, CorruptorConfigMap> {
deepSet(
level: TargetLevel,
configName: string,
value: CorruptorConfig,
overwrite = true
) {
if (!this.has(level)) {
this.set(level, new Map());
}
const indexMap = this.get(level);
if (overwrite || !indexMap.has(configName)) {
indexMap.set(configName, value);
}
}
}

export const corruptorConfigUtils = function (
urlSearchParams: URLSearchParams
): CorruptorConfigUtils {
Expand All @@ -107,6 +131,7 @@ export const corruptorConfigUtils = function (
},
getAllManifestConfigs(mseq = 0, isDash = false) {
const outputMap = new CorruptorIndexMap();
const levelMap = new CorruptorLevelMap();
const configs = (
(this.registered || []) as SegmentCorruptorQueryConfig[]
).filter(({ name }) => urlSearchParams.get(name));
Expand Down Expand Up @@ -141,9 +166,12 @@ export const corruptorConfigUtils = function (
outputMap.deepSet(item.sq - mseq, config.name, item, false);
}
}
if (item.l != undefined) {
levelMap.deepSet(item.l, config.name, item, false);
}
});
}
return [null, outputMap];
return [null, outputMap, levelMap];
},
getAllSegmentConfigs() {
const outputMap = new Map();
Expand Down
6 changes: 3 additions & 3 deletions src/manifests/utils/corruptions/delay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ describe('manifest.utils.corruptions.delay', () => {
const expected = [
{
message:
"Incorrect delay query format. Either 'i' or 'sq' is required in a single query object.",
"Incorrect delay query format. Either 'i', 'l' or 'sq' is required in a single query object.",
status: 400
},
null
Expand Down Expand Up @@ -131,7 +131,7 @@ describe('manifest.utils.corruptions.delay', () => {
const expected = [
{
message:
'Incorrect delay query format. Expected format: [{i?:number, sq?:number, br?:number, ms:number}, ...n] where i and sq are mutually exclusive.',
'Incorrect delay query format. Expected format: [{i?:number, l?:number, sq?:number, br?:number, ms:number}, ...n] where i and sq are mutually exclusive.',
status: 400
},
null
Expand All @@ -151,7 +151,7 @@ describe('manifest.utils.corruptions.delay', () => {
const expected = [
{
message:
'Incorrect delay query format. Expected format: [{i?:number, sq?:number, br?:number, ms:number}, ...n] where i and sq are mutually exclusive.',
'Incorrect delay query format. Expected format: [{i?:number, l?:number, sq?:number, br?:number, ms:number}, ...n] where i and sq are mutually exclusive.',
status: 400
},
null
Expand Down
26 changes: 21 additions & 5 deletions src/manifests/utils/corruptions/delay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface DelayConfig extends CorruptorConfig {

// TODO: Move to a constants file and group with and
const delayExpectedQueryFormatMsg =
'Incorrect delay query format. Expected format: [{i?:number, sq?:number, br?:number, ms:number}, ...n] where i and sq are mutually exclusive.';
'Incorrect delay query format. Expected format: [{i?:number, l?:number, sq?:number, br?:number, ms:number}, ...n] where i and sq are mutually exclusive.';

function getManifestConfigError(value: { [key: string]: unknown }): string {
const o = value as DelayConfig;
Expand All @@ -17,13 +17,14 @@ function getManifestConfigError(value: { [key: string]: unknown }): string {
return delayExpectedQueryFormatMsg;
}

if (o.i === undefined && o.sq === undefined) {
return "Incorrect delay query format. Either 'i' or 'sq' is required in a single query object.";
if (o.i === undefined && o.sq === undefined && o.l === undefined) {
return "Incorrect delay query format. Either 'i', 'l' or 'sq' is required in a single query object.";
}

if (
!(o.i === '*' || typeof o.i === 'number') &&
!(o.sq === '*' || typeof o.sq === 'number')
!(o.sq === '*' || typeof o.sq === 'number') &&
!(typeof o.l === 'number')
) {
return delayExpectedQueryFormatMsg;
}
Expand All @@ -40,6 +41,10 @@ function getManifestConfigError(value: { [key: string]: unknown }): string {
return 'Incorrect delay query format. Field i must be 0 or positive.';
}

if (Number(o.l) < 0) {
return 'Incorrect delay query format. Field l must be 0 or positive.';
}

return '';
}
function isValidSegmentConfig(value: { [key: string]: unknown }): boolean {
Expand Down Expand Up @@ -74,6 +79,7 @@ const delayConfig: SegmentCorruptorQueryConfig = {

const configIndexMap = new Map();
const configSqMap = new Map();
const configLevelMap = new Map();

for (let i = 0; i < configs.length; i++) {
const config = configs[i];
Expand Down Expand Up @@ -102,6 +108,12 @@ const delayConfig: SegmentCorruptorQueryConfig = {
configIndexMap.set(config.i, corruptorConfig);
}

// Level numeric
if (typeof config.l === 'number' && !configLevelMap.has(config.l)) {
corruptorConfig.l = config.l;
configLevelMap.set(config.l, corruptorConfig);
}

// Sequence default
if (config.sq === '*') {
// If default is already set, we skip
Expand All @@ -117,7 +129,6 @@ const delayConfig: SegmentCorruptorQueryConfig = {
configSqMap.set(config.sq, corruptorConfig);
}
}

const corruptorConfigs: CorruptorConfig[] = [];

for (const value of configIndexMap.values()) {
Expand All @@ -127,6 +138,10 @@ const delayConfig: SegmentCorruptorQueryConfig = {
for (const value of configSqMap.values()) {
corruptorConfigs.push(value);
}

for (const value of configLevelMap.values()) {
corruptorConfigs.push(value);
}
return [null, corruptorConfigs];
},
getSegmentConfigs(
Expand All @@ -148,6 +163,7 @@ const delayConfig: SegmentCorruptorQueryConfig = {
null,
{
i: config.i,
l: config.l,
sq: config.sq,
fields: {
ms: config.ms
Expand Down
6 changes: 3 additions & 3 deletions src/manifests/utils/hlsManifestUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ describe('hlsManifestTools', () => {
#EXT-X-VERSION:3
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-STREAM-INF:BANDWIDTH=4255267,AVERAGE-BANDWIDTH=4255267,CODECS="avc1.4d4032,mp4a.40.2",RESOLUTION=2560x1440,FRAME-RATE=25,AUDIO="audio",SUBTITLES="subs"
proxy-media.m3u8?url=https%3A%2F%2Fmock.mock.com%2Fstream%2Fhls%2Fmanifest_1.m3u8&statusCode=%5B%7Bi%3A0%2Ccode%3A404%7D%2C%7Bi%3A2%2Ccode%3A401%7D%5D&timeout=%5B%7Bi%3A3%7D%5D&delay=%5B%7Bi%3A2%2Cms%3A2000%7D%5D&bitrate=4255267
proxy-media.m3u8?url=https%3A%2F%2Fmock.mock.com%2Fstream%2Fhls%2Fmanifest_1.m3u8&statusCode=%5B%7Bi%3A0%2Ccode%3A404%7D%2C%7Bi%3A2%2Ccode%3A401%7D%5D&timeout=%5B%7Bi%3A3%7D%5D&delay=%5B%7Bi%3A2%2Cms%3A2000%7D%5D&bitrate=4255267&level=1
#EXT-X-STREAM-INF:BANDWIDTH=3062896,AVERAGE-BANDWIDTH=3062896,CODECS="avc1.4d4028,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=25,AUDIO="audio",SUBTITLES="subs"
proxy-media.m3u8?url=https%3A%2F%2Fmock.mock.com%2Fstream%2Fhls%2Fmanifest_2.m3u8&statusCode=%5B%7Bi%3A0%2Ccode%3A404%7D%2C%7Bi%3A2%2Ccode%3A401%7D%5D&timeout=%5B%7Bi%3A3%7D%5D&delay=%5B%7Bi%3A2%2Cms%3A2000%7D%5D&bitrate=3062896
proxy-media.m3u8?url=https%3A%2F%2Fmock.mock.com%2Fstream%2Fhls%2Fmanifest_2.m3u8&statusCode=%5B%7Bi%3A0%2Ccode%3A404%7D%2C%7Bi%3A2%2Ccode%3A401%7D%5D&timeout=%5B%7Bi%3A3%7D%5D&delay=%5B%7Bi%3A2%2Cms%3A2000%7D%5D&bitrate=3062896&level=2
#EXT-X-STREAM-INF:BANDWIDTH=2316761,AVERAGE-BANDWIDTH=2316761,CODECS="avc1.4d4028,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=25,AUDIO="audio",SUBTITLES="subs"
proxy-media.m3u8?url=https%3A%2F%2Fmock.mock.com%2Fstream%2Fhls%2Fmanifest_3.m3u8&statusCode=%5B%7Bi%3A0%2Ccode%3A404%7D%2C%7Bi%3A2%2Ccode%3A401%7D%5D&timeout=%5B%7Bi%3A3%7D%5D&delay=%5B%7Bi%3A2%2Cms%3A2000%7D%5D&bitrate=2316761
proxy-media.m3u8?url=https%3A%2F%2Fmock.mock.com%2Fstream%2Fhls%2Fmanifest_3.m3u8&statusCode=%5B%7Bi%3A0%2Ccode%3A404%7D%2C%7Bi%3A2%2Ccode%3A401%7D%5D&timeout=%5B%7Bi%3A3%7D%5D&delay=%5B%7Bi%3A2%2Cms%3A2000%7D%5D&bitrate=2316761&level=3

#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",LANGUAGE="en",NAME="English stereo",CHANNELS="2",DEFAULT=YES,AUTOSELECT=YES,URI="proxy-media.m3u8?url=https%3A%2F%2Fmock.mock.com%2Fstream%2Fhls%2Fmanifest_audio-en.m3u8&statusCode=%5B%7Bi%3A0%2Ccode%3A404%7D%2C%7Bi%3A2%2Ccode%3A401%7D%5D&timeout=%5B%7Bi%3A3%7D%5D&delay=%5B%7Bi%3A2%2Cms%3A2000%7D%5D"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",LANGUAGE="sv",NAME="Swedish stereo",CHANNELS="2",DEFAULT=NO,AUTOSELECT=YES,URI="proxy-media.m3u8?url=https%3A%2F%2Fmock.mock.com%2Fstream%2Fhls%2Fmanifest_audio-sv.m3u8&statusCode=%5B%7Bi%3A0%2Ccode%3A404%7D%2C%7Bi%3A2%2Ccode%3A401%7D%5D&timeout=%5B%7Bi%3A3%7D%5D&delay=%5B%7Bi%3A2%2Cms%3A2000%7D%5D"
Expand Down
3 changes: 3 additions & 0 deletions src/manifests/utils/hlsManifestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export default function (): HLSManifestTools {
const m3u: M3U = clone(originalM3U);

// [Video]
let abrLevel = 1;
m3u.items.StreamItem = m3u.items.StreamItem.map((streamItem) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const bitRate = (streamItem as any)?.attributes?.attributes?.bandwidth;
Expand All @@ -91,6 +92,8 @@ export default function (): HLSManifestTools {
const urlQuery = new URLSearchParams(originalUrlQuery);
if (bitRate) {
urlQuery.set('bitrate', bitRate);
urlQuery.set('level', abrLevel.toString());
abrLevel++;
}
streamItem.set(
'uri',
Expand Down
2 changes: 2 additions & 0 deletions src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export type ServiceError = {

export type TargetIndex = number | '*';

export type TargetLevel = number;

/**
* Cherrypicking explicitly what we need to type from
* https://github.com/Eyevinn/node-m3u8/blob/master/m3u/Item.js
Expand Down
Loading