diff --git a/README.md b/README.md index edb23fa..9797245 100644 --- a/README.md +++ b/README.md @@ -95,13 +95,13 @@ Currently, the Chaos Stream Proxy supports 4 types of corruptions for HLS and MP To specify the configurations for a particular corruption, you will need to add a stringified JSON object as a query parameter to the proxied URL. Each corruption has a unique configuration JSON object template. Each object can be used to target one specific segment for corruption. -e.i. `https:///api/v2/manifests/hls/proxy-master.m3u8?url=?some_corruption=[{i:0},{i:1},{i:2}, ... ,{i:N}]` +e.i. `https:///api/v2/manifests/hls/proxy-master.m3u8?url=&some_corruption=[{i:0},{i:1},{i:2}, ... ,{i:N}]` Across all corruptions, there are 3 ways to target a segment in a playlist for corruption. 1. `i`: The segment's list index in any Media Playlist, with HLS segments starting at 0 and MPEG-DASH segments starting at 1. For a Media Playlist with 12 segments, `i`=11, would target the last segment for HLS and `i`=12, would target the last segment for MPEG-DASH. 2. `sq`: The segment's Media Sequence Number for HLS, or the "$Number$" or "$Time$" part of a segment URL for DASH. For an HLS Media Playlist with 12 segments, and where `#EXT-X-MEDIA-SEQUENCE` is 100, `sq`=111 would target the last segment. When corrupting a live HLS stream it is recommended to target with `rsq`. -3. `rsq`: A relative sequence number, counted from where the live stream is currently at when requesting manifest. (**HLS SUPPORTED ONLY IN STATEFUL MODE**) +3. `rsq`: A relative sequence number, counted from where the live stream is currently at when requesting manifest. Can also use a negative integer, which enables counting backwards from the end of the manifest instead. (**HLS SUPPORTED ONLY IN STATEFUL MODE**) Below are configuration JSON object templates for the currently supported corruptions. A query should have its value be an array consisting of any one of these 3 types of items: diff --git a/src/manifests/handlers/hls/media.ts b/src/manifests/handlers/hls/media.ts index a92bae4..2799211 100644 --- a/src/manifests/handlers/hls/media.ts +++ b/src/manifests/handlers/hls/media.ts @@ -78,12 +78,12 @@ export default async function hlsMediaHandler( } } } - const [error, allMutations, levelMutations] = configUtils.getAllManifestConfigs( mediaSequence, false, - mediaSequenceOffset + mediaSequenceOffset, + mediaM3U.items.PlaylistItem.length ); if (error) { return generateErrorResponse(error); diff --git a/src/manifests/utils/configs.test.ts b/src/manifests/utils/configs.test.ts index 2621aec..d145658 100644 --- a/src/manifests/utils/configs.test.ts +++ b/src/manifests/utils/configs.test.ts @@ -193,7 +193,44 @@ describe('configs', () => { // Act const [err, actual] = configs.getAllManifestConfigs(0, false, 100); + // Assert + expect(err).toBeNull(); + expect(actual.get(115)).toEqual( + new Map([ + [ + 'statusCode', + { + fields: { code: 400 }, + sq: 115 + } + ] + ]) + ); + expect(actual.get(15)).toEqual( + new Map([ + [ + 'throttle', + { + fields: { rate: 1000 }, + sq: 15 + } + ] + ]) + ); + }); + it('should handle media sequence offsets with negative rsq value', () => { + // Arrange + const configs = statefulConfig.corruptorConfigUtils( + new URLSearchParams( + 'statusCode=[{rsq:-1,code:400}]&throttle=[{sq:15,rate:1000}]' + ) + ); + + configs.register(statusCodeConfig).register(throttleConfig); + + // Act + const [err, actual] = configs.getAllManifestConfigs(0, false, 100, 15); // Assert expect(err).toBeNull(); expect(actual.get(115)).toEqual( diff --git a/src/manifests/utils/configs.ts b/src/manifests/utils/configs.ts index d3fd050..bf678bb 100644 --- a/src/manifests/utils/configs.ts +++ b/src/manifests/utils/configs.ts @@ -54,7 +54,8 @@ export interface CorruptorConfigUtils { getAllManifestConfigs: ( mseq?: number, isDash?: boolean, - mseqOffset?: number + mseqOffset?: number, + playlistSize?: number ) => [ ServiceError | null, IndexedCorruptorConfigMap | null, @@ -131,7 +132,12 @@ export const corruptorConfigUtils = function ( } return this; }, - getAllManifestConfigs(mseq = 0, isDash = false, mseqOffset = 0) { + getAllManifestConfigs( + mseq = 0, + isDash = false, + mseqOffset = 0, + playlistSize = 0 + ) { const outputMap = new CorruptorIndexMap(); const levelMap = new CorruptorLevelMap(); const configs = ( @@ -171,7 +177,11 @@ export const corruptorConfigUtils = function ( // Replace relative sequence numbers with absolute ones params = params.map((param) => { if (param.rsq) { - param.sq = Number(param.rsq) + mseqOffset; + const rsq = Number(param.rsq); + param['sq'] = + rsq < 0 && playlistSize > 0 + ? mseqOffset + playlistSize + rsq + 1 + : Number(param.rsq) + mseqOffset; delete param.rsq; } return param; @@ -182,6 +192,7 @@ export const corruptorConfigUtils = function ( if (error) { return [error, null]; } + configList.forEach((item) => { if (item.i != undefined) { outputMap.deepSet(item.i, config.name, item, false); diff --git a/src/manifests/utils/hlsManifestUtils.ts b/src/manifests/utils/hlsManifestUtils.ts index badc639..ff7e0de 100644 --- a/src/manifests/utils/hlsManifestUtils.ts +++ b/src/manifests/utils/hlsManifestUtils.ts @@ -1,9 +1,5 @@ import { M3U, Manifest } from '../../shared/types'; -import { - newState, - proxyPathBuilder, - segmentUrlParamString -} from '../../shared/utils'; +import { proxyPathBuilder, segmentUrlParamString } from '../../shared/utils'; import { CorruptorConfigMap, IndexedCorruptorConfigMap } from './configs'; import clone from 'clone'; @@ -143,15 +139,13 @@ export default function (): HLSManifestTools { configsMap: IndexedCorruptorConfigMap ) { const m3u: M3U = clone(originalM3U); + const playlistSize = m3u.items.PlaylistItem.length; // configs for each index - const corruptions = this.utils.mergeMap( - m3u.items.PlaylistItem.length, - configsMap - ); + const corruptions = this.utils.mergeMap(playlistSize, configsMap); // Attach corruptions to manifest - for (let i = 0; i < m3u.items.PlaylistItem.length; i++) { + for (let i = 0; i < playlistSize; i++) { const item = m3u.items.PlaylistItem[i]; const corruption = corruptions[i]; let sourceSegURL: string = item.get('uri');