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

Support post-processing to produce arbitrary output lines for segment… #151

Merged
merged 1 commit into from
Apr 21, 2024
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
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,21 @@ Converts a text playlist into a structured JS object
#### return value
An instance of either `MasterPlaylist` or `MediaPlaylist` (See **Data format** below.)

### `HLS.stringify(obj)`
### `HLS.stringify(obj, processors)`
Converts a JS object into a plain text playlist

#### params
| Name | Type | Required | Default | Description |
| ------- | ------ | -------- | ------- | ------------- |
| obj | `MasterPlaylist` or `MediaPlaylist` (See **Data format** below.) | Yes | N/A | An object returned by `HLS.parse()` or a manually created object |
| postProcess | PostProcess | No | undefined | A function to be called for each segment or variant to manipulate the output. |

##### `PostProcess`
| Property | Type | Required | Default | Description |
| ---------------- | ------------- | -------- | ------- | ------------- |
| `segmentProcessor` | (lines: string[], start: number, end: number, segment: Segment, i: number) => undefined | No | undefined | A function to manipulate the segment output. |
| `variantProcessor` | (lines: string[], start: number, end: number, variant: Variant, i: number) => undefined | No | undefined | A function to manipulate the variant output. |


#### return value
A text data that conforms to [the HLS playlist spec](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.1)
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"type-check": "tsc --noEmit",
"audit": "npm audit --audit-level high",
"build": "rm -fR ./dist; tsc ; webpack --mode development ; webpack --mode production",
"test": "npm run lint && npm run build && npm run audit && ava --verbose"
"test": "npm run lint && npm run build && npm run audit && ava --verbose",
"test-offline": "npm run lint && npm run build && ava --verbose"
},
"repository": {
"type": "git",
Expand Down
34 changes: 26 additions & 8 deletions stringify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
Segment,
SessionData,
SpliceInfo,
Variant
Variant,
PostProcess,
} from './types';

const ALLOW_REDUNDANCY = [
Expand Down Expand Up @@ -57,6 +58,15 @@ class LineArray extends Array<string> {
}
return this.length;
}

override join(separator: string | undefined = ','): string {
for (let i = this.length - 1; i >= 0; i--) {
if (!this[i]) {
this.splice(i, 1);
}
}
return super.join(separator);
}
}

function buildDecimalFloatingNumber(num: number, fixed?: number) {
Expand All @@ -77,15 +87,19 @@ function getNumberOfDecimalPlaces(num: number) {
return str.length - index - 1;
}

function buildMasterPlaylist(lines: LineArray, playlist: MasterPlaylist) {
function buildMasterPlaylist(lines: LineArray, playlist: MasterPlaylist, postProcess: PostProcess | undefined) {
for (const sessionData of playlist.sessionDataList) {
lines.push(buildSessionData(sessionData));
}
for (const sessionKey of playlist.sessionKeyList) {
lines.push(buildKey(sessionKey, true));
}
for (const variant of playlist.variants) {
for (const [i, variant] of playlist.variants.entries()) {
const base = lines.length;
buildVariant(lines, variant);
if (postProcess?.variantProcessor) {
postProcess.variantProcessor(lines, base, lines.length - 1, variant, i);
}
}
}

Expand Down Expand Up @@ -231,7 +245,7 @@ function buildRendition(rendition: Rendition) {
return `#EXT-X-MEDIA:${attrs.join(',')}`;
}

function buildMediaPlaylist(lines: LineArray, playlist: MediaPlaylist) {
function buildMediaPlaylist(lines: LineArray, playlist: MediaPlaylist, postProcess: PostProcess | undefined) {
let lastKey = '';
let lastMap = '';
let unclosedCueIn = false;
Expand Down Expand Up @@ -272,14 +286,18 @@ function buildMediaPlaylist(lines: LineArray, playlist: MediaPlaylist) {
if (playlist.skip > 0) {
lines.push(`#EXT-X-SKIP:SKIPPED-SEGMENTS=${playlist.skip}`);
}
for (const segment of playlist.segments) {
for (const [i, segment] of playlist.segments.entries()) {
const base = lines.length;
let markerType = '';
[lastKey, lastMap, markerType] = buildSegment(lines, segment, lastKey, lastMap, playlist.version);
if (markerType === 'OUT') {
unclosedCueIn = true;
} else if (markerType === 'IN' && unclosedCueIn) {
unclosedCueIn = false;
}
if (postProcess?.segmentProcessor) {
postProcess.segmentProcessor(lines, base, lines.length - 1, segment, i);
}
}
if (playlist.playlistType === 'VOD' && unclosedCueIn) {
lines.push('#EXT-X-CUE-IN');
Expand Down Expand Up @@ -449,7 +467,7 @@ function buildParts(lines: LineArray, parts: PartialSegment[]) {
return hint;
}

function stringify(playlist: MasterPlaylist | MediaPlaylist): string {
function stringify(playlist: MasterPlaylist | MediaPlaylist, postProcess: PostProcess | undefined): string {
utils.PARAMCHECK(playlist);
utils.ASSERT('Not a playlist', playlist.type === 'playlist');
const lines = new LineArray(playlist.uri);
Expand All @@ -464,9 +482,9 @@ function stringify(playlist: MasterPlaylist | MediaPlaylist): string {
lines.push(`#EXT-X-START:TIME-OFFSET=${buildDecimalFloatingNumber(playlist.start.offset)}${playlist.start.precise ? ',PRECISE=YES' : ''}`);
}
if (playlist.isMasterPlaylist) {
buildMasterPlaylist(lines, playlist as MasterPlaylist);
buildMasterPlaylist(lines, playlist as MasterPlaylist, postProcess);
} else {
buildMediaPlaylist(lines, playlist as MediaPlaylist);
buildMediaPlaylist(lines, playlist as MediaPlaylist, postProcess);
}
// console.log('<<<');
// console.log(lines.join('\n'));
Expand Down
226 changes: 226 additions & 0 deletions test/spec/stringify.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,229 @@ for (const {name, m3u8, object} of fixtures) {
t.is(result, utils.stripCommentsAndEmptyLines(m3u8));
});
}

test('stringify.postProcess.segment.add', t => {
const obj = HLS.parse(`
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXTINF:6.006,
http://media.example.com/01.ts
#EXTINF:6.006,
http://media.example.com/02.ts
#EXTINF:6.006,
http://ads.example.com/ad-01.ts
#EXTINF:6.006,
http://ads.example.com/ad-02.ts
#EXTINF:6.006,
http://media.example.com/03.ts
#EXTINF:3.003,
http://media.example.com/04.ts
`);
const expected = `
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-MY-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z
#EXTINF:6.006,
http://media.example.com/01.ts
#EXT-X-MY-PROGRAM-DATE-TIME:2014-03-05T11:14:06.006Z
#EXTINF:6.006,
http://media.example.com/02.ts
#EXT-X-MY-PROGRAM-DATE-TIME:2014-03-05T11:14:12.012Z
#EXTINF:6.006,
http://ads.example.com/ad-01.ts
#EXT-X-MY-PROGRAM-DATE-TIME:2014-03-05T11:14:18.018Z
#EXTINF:6.006,
http://ads.example.com/ad-02.ts
#EXT-X-MY-PROGRAM-DATE-TIME:2014-03-05T11:14:24.024Z
#EXTINF:6.006,
http://media.example.com/03.ts
#EXT-X-MY-PROGRAM-DATE-TIME:2014-03-05T11:14:30.030Z
#EXTINF:3.003,
http://media.example.com/04.ts
`;
let time = new Date('2014-03-05T11:14:00.000Z').getTime();
const segmentProcessor = (lines, start, end, segment) => {
let hasPdt = false;
for (let i = start; i <= end; i++) {
if (lines[i].startsWith('#EXT-X-PROGRAM-DATE-TIME')) {
hasPdt = true;
break;
}
}
if (!hasPdt) {
lines.splice(start, 0, `#EXT-X-MY-PROGRAM-DATE-TIME:${new Date(Math.round(time)).toISOString()}`);
}
time += segment.duration * 1000;
};
const result = HLS.stringify(obj, {segmentProcessor});
t.is(result, utils.stripCommentsAndEmptyLines(expected));
});

test('stringify.postProcess.segment.delete', t => {
const obj = HLS.parse(`
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z
#EXTINF:6.006,
http://media.example.com/01.ts
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:06.006Z
#EXTINF:6.006,
http://media.example.com/02.ts
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:12.012Z
#EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2014-03-05T11:15:00.000Z",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF000014056FFFFFF000E011622DCAFF000052636200000000000A0008029896F50000008700000000
#EXTINF:6.006,
http://ads.example.com/ad-01.ts
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:18.018Z
#EXTINF:6.006,
http://ads.example.com/ad-02.ts
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:24.024Z
#EXT-X-DATERANGE:ID="splice-6FFFFFF0",DURATION=59.993,SCTE35-IN=0xFC002A0000000000FF00000F056FFFFFF000401162802E6100000000000A0008029896F50000008700000000
#EXTINF:6.006,
http://media.example.com/03.ts
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:30.030Z
#EXTINF:3.003,
http://media.example.com/04.ts
`);
const expected = `
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z
#EXTINF:6.006,
http://media.example.com/01.ts
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:06.006Z
#EXTINF:6.006,
http://media.example.com/02.ts
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:12.012Z
#EXTINF:6.006,
http://ads.example.com/ad-01.ts
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:18.018Z
#EXTINF:6.006,
http://ads.example.com/ad-02.ts
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:24.024Z
#EXTINF:6.006,
http://media.example.com/03.ts
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:30.030Z
#EXTINF:3.003,
http://media.example.com/04.ts
`;
const segmentProcessor = (lines, start, end) => {
for (let i = start; i <= end; i++) {
const line = lines[i];
if (line.startsWith('#EXT-X-DATERANGE')) {
lines[i] = '';
}
}
};
const result = HLS.stringify(obj, {segmentProcessor});
t.is(result, utils.stripCommentsAndEmptyLines(expected));
});

test('stringify.postProcess.segment.update', t => {
const obj = HLS.parse(`
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z
#EXTINF:6.006,
http://media.example.com/01.ts
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:06.006Z
#EXTINF:6.006,
http://media.example.com/02.ts
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:12.012Z
#EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2014-03-05T11:15:00.000Z",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF000014056FFFFFF000E011622DCAFF000052636200000000000A0008029896F50000008700000000
#EXTINF:6.006,
http://ads.example.com/ad-01.ts
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:18.018Z
#EXTINF:6.006,
http://ads.example.com/ad-02.ts
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:24.024Z
#EXT-X-DATERANGE:ID="splice-6FFFFFF0",DURATION=59.993,SCTE35-IN=0xFC002A0000000000FF00000F056FFFFFF000401162802E6100000000000A0008029896F50000008700000000
#EXTINF:6.006,
http://media.example.com/03.ts
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:30.030Z
#EXTINF:3.003,
http://media.example.com/04.ts
`);
const expected = `
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z
#EXTINF:6.006,
http://media.example.com/01.ts
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:06.006Z
#EXTINF:6.006,
http://media.example.com/02.ts
<b>#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:12.012Z
#EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2014-03-05T11:15:00.000Z",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF000014056FFFFFF000E011622DCAFF000052636200000000000A0008029896F50000008700000000
#EXTINF:6.006,
http://ads.example.com/ad-01.ts
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:18.018Z
#EXTINF:6.006,
http://ads.example.com/ad-02.ts</b>
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:24.024Z
#EXT-X-DATERANGE:ID="splice-6FFFFFF0",DURATION=59.993,SCTE35-IN=0xFC002A0000000000FF00000F056FFFFFF000401162802E6100000000000A0008029896F50000008700000000
#EXTINF:6.006,
http://media.example.com/03.ts
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:30.030Z
#EXTINF:3.003,
http://media.example.com/04.ts
`;
const segmentProcessor = (lines, start, end) => {
for (let i = start; i <= end; i++) {
const line = lines[i];
if (line.startsWith('#EXT-X-DATERANGE')) {
if (line.includes('PLANNED-DURATION')) {
lines[start] = `<b>${lines[start]}`;
} else if (start > 0) {
lines[start - 1] = `${lines[start - 1]}</b>`;
}
}
}
};
const result = HLS.stringify(obj, {segmentProcessor});
t.is(result, utils.stripCommentsAndEmptyLines(expected));
});

test('stringify.postProcess.variant.update', t => {
const obj = HLS.parse(`
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=1280000
http://example.com/low.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2560000
http://example.com/mid.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=7680000
http://example.com/hi.m3u8
`);
const expected = `
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=1280000,MY-RESOLUTION=1280x720
http://example.com/low.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2560000,MY-RESOLUTION=1920x1080
http://example.com/mid.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=7680000,MY-RESOLUTION=3840x2160
http://example.com/hi.m3u8
`;
const variantProcessor = (lines, start, end, {bandwidth}) => {
for (let i = start; i <= end; i++) {
const line = lines[i];
if (line.startsWith('#EXT-X-STREAM-INF')) {
let resolution = '640x360';
if (bandwidth >= 1000000 && bandwidth < 2000000) {
resolution = '1280x720';
} else if (bandwidth >= 2000000 && bandwidth < 3000000) {
resolution = '1920x1080';
} else if (bandwidth >= 3000000) {
resolution = '3840x2160';
}
lines[i] = `${line},MY-RESOLUTION=${resolution}`;
}
}
};
const result = HLS.stringify(obj, {variantProcessor});
t.is(result, utils.stripCommentsAndEmptyLines(expected));
});
5 changes: 5 additions & 0 deletions types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,3 +518,8 @@ export type TagParam =
| [ Date, null ];

export type UserAttribute = number | string | Uint8Array;

export type PostProcess = {
segmentProcessor: ((lines: string[], start: number, end: number, segment: Segment, i: number) => undefined) | undefined;
variantProcessor: ((lines: string[], start: number, end: number, variant: Variant, i: number) => undefined) | undefined;
};