Skip to content

Commit 6dd73bc

Browse files
authored
Support post-processing to produce arbitrary output lines for segment/variant (#151)
1 parent 6b038fb commit 6dd73bc

File tree

5 files changed

+268
-10
lines changed

5 files changed

+268
-10
lines changed

README.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,21 @@ Converts a text playlist into a structured JS object
6161
#### return value
6262
An instance of either `MasterPlaylist` or `MediaPlaylist` (See **Data format** below.)
6363

64-
### `HLS.stringify(obj)`
64+
### `HLS.stringify(obj, processors)`
6565
Converts a JS object into a plain text playlist
6666

6767
#### params
6868
| Name | Type | Required | Default | Description |
6969
| ------- | ------ | -------- | ------- | ------------- |
7070
| obj | `MasterPlaylist` or `MediaPlaylist` (See **Data format** below.) | Yes | N/A | An object returned by `HLS.parse()` or a manually created object |
71+
| postProcess | PostProcess | No | undefined | A function to be called for each segment or variant to manipulate the output. |
72+
73+
##### `PostProcess`
74+
| Property | Type | Required | Default | Description |
75+
| ---------------- | ------------- | -------- | ------- | ------------- |
76+
| `segmentProcessor` | (lines: string[], start: number, end: number, segment: Segment, i: number) => undefined | No | undefined | A function to manipulate the segment output. |
77+
| `variantProcessor` | (lines: string[], start: number, end: number, variant: Variant, i: number) => undefined | No | undefined | A function to manipulate the variant output. |
78+
7179

7280
#### return value
7381
A text data that conforms to [the HLS playlist spec](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.1)

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"type-check": "tsc --noEmit",
1111
"audit": "npm audit --audit-level high",
1212
"build": "rm -fR ./dist; tsc ; webpack --mode development ; webpack --mode production",
13-
"test": "npm run lint && npm run build && npm run audit && ava --verbose"
13+
"test": "npm run lint && npm run build && npm run audit && ava --verbose",
14+
"test-offline": "npm run lint && npm run build && ava --verbose"
1415
},
1516
"repository": {
1617
"type": "git",

stringify.ts

+26-8
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
Segment,
1111
SessionData,
1212
SpliceInfo,
13-
Variant
13+
Variant,
14+
PostProcess,
1415
} from './types';
1516

1617
const ALLOW_REDUNDANCY = [
@@ -57,6 +58,15 @@ class LineArray extends Array<string> {
5758
}
5859
return this.length;
5960
}
61+
62+
override join(separator: string | undefined = ','): string {
63+
for (let i = this.length - 1; i >= 0; i--) {
64+
if (!this[i]) {
65+
this.splice(i, 1);
66+
}
67+
}
68+
return super.join(separator);
69+
}
6070
}
6171

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

80-
function buildMasterPlaylist(lines: LineArray, playlist: MasterPlaylist) {
90+
function buildMasterPlaylist(lines: LineArray, playlist: MasterPlaylist, postProcess: PostProcess | undefined) {
8191
for (const sessionData of playlist.sessionDataList) {
8292
lines.push(buildSessionData(sessionData));
8393
}
8494
for (const sessionKey of playlist.sessionKeyList) {
8595
lines.push(buildKey(sessionKey, true));
8696
}
87-
for (const variant of playlist.variants) {
97+
for (const [i, variant] of playlist.variants.entries()) {
98+
const base = lines.length;
8899
buildVariant(lines, variant);
100+
if (postProcess?.variantProcessor) {
101+
postProcess.variantProcessor(lines, base, lines.length - 1, variant, i);
102+
}
89103
}
90104
}
91105

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

234-
function buildMediaPlaylist(lines: LineArray, playlist: MediaPlaylist) {
248+
function buildMediaPlaylist(lines: LineArray, playlist: MediaPlaylist, postProcess: PostProcess | undefined) {
235249
let lastKey = '';
236250
let lastMap = '';
237251
let unclosedCueIn = false;
@@ -272,14 +286,18 @@ function buildMediaPlaylist(lines: LineArray, playlist: MediaPlaylist) {
272286
if (playlist.skip > 0) {
273287
lines.push(`#EXT-X-SKIP:SKIPPED-SEGMENTS=${playlist.skip}`);
274288
}
275-
for (const segment of playlist.segments) {
289+
for (const [i, segment] of playlist.segments.entries()) {
290+
const base = lines.length;
276291
let markerType = '';
277292
[lastKey, lastMap, markerType] = buildSegment(lines, segment, lastKey, lastMap, playlist.version);
278293
if (markerType === 'OUT') {
279294
unclosedCueIn = true;
280295
} else if (markerType === 'IN' && unclosedCueIn) {
281296
unclosedCueIn = false;
282297
}
298+
if (postProcess?.segmentProcessor) {
299+
postProcess.segmentProcessor(lines, base, lines.length - 1, segment, i);
300+
}
283301
}
284302
if (playlist.playlistType === 'VOD' && unclosedCueIn) {
285303
lines.push('#EXT-X-CUE-IN');
@@ -449,7 +467,7 @@ function buildParts(lines: LineArray, parts: PartialSegment[]) {
449467
return hint;
450468
}
451469

452-
function stringify(playlist: MasterPlaylist | MediaPlaylist): string {
470+
function stringify(playlist: MasterPlaylist | MediaPlaylist, postProcess: PostProcess | undefined): string {
453471
utils.PARAMCHECK(playlist);
454472
utils.ASSERT('Not a playlist', playlist.type === 'playlist');
455473
const lines = new LineArray(playlist.uri);
@@ -464,9 +482,9 @@ function stringify(playlist: MasterPlaylist | MediaPlaylist): string {
464482
lines.push(`#EXT-X-START:TIME-OFFSET=${buildDecimalFloatingNumber(playlist.start.offset)}${playlist.start.precise ? ',PRECISE=YES' : ''}`);
465483
}
466484
if (playlist.isMasterPlaylist) {
467-
buildMasterPlaylist(lines, playlist as MasterPlaylist);
485+
buildMasterPlaylist(lines, playlist as MasterPlaylist, postProcess);
468486
} else {
469-
buildMediaPlaylist(lines, playlist as MediaPlaylist);
487+
buildMediaPlaylist(lines, playlist as MediaPlaylist, postProcess);
470488
}
471489
// console.log('<<<');
472490
// console.log(lines.join('\n'));

test/spec/stringify.spec.js

+226
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,229 @@ for (const {name, m3u8, object} of fixtures) {
1111
t.is(result, utils.stripCommentsAndEmptyLines(m3u8));
1212
});
1313
}
14+
15+
test('stringify.postProcess.segment.add', t => {
16+
const obj = HLS.parse(`
17+
#EXTM3U
18+
#EXT-X-VERSION:3
19+
#EXT-X-TARGETDURATION:6
20+
#EXTINF:6.006,
21+
http://media.example.com/01.ts
22+
#EXTINF:6.006,
23+
http://media.example.com/02.ts
24+
#EXTINF:6.006,
25+
http://ads.example.com/ad-01.ts
26+
#EXTINF:6.006,
27+
http://ads.example.com/ad-02.ts
28+
#EXTINF:6.006,
29+
http://media.example.com/03.ts
30+
#EXTINF:3.003,
31+
http://media.example.com/04.ts
32+
`);
33+
const expected = `
34+
#EXTM3U
35+
#EXT-X-VERSION:3
36+
#EXT-X-TARGETDURATION:6
37+
#EXT-X-MY-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z
38+
#EXTINF:6.006,
39+
http://media.example.com/01.ts
40+
#EXT-X-MY-PROGRAM-DATE-TIME:2014-03-05T11:14:06.006Z
41+
#EXTINF:6.006,
42+
http://media.example.com/02.ts
43+
#EXT-X-MY-PROGRAM-DATE-TIME:2014-03-05T11:14:12.012Z
44+
#EXTINF:6.006,
45+
http://ads.example.com/ad-01.ts
46+
#EXT-X-MY-PROGRAM-DATE-TIME:2014-03-05T11:14:18.018Z
47+
#EXTINF:6.006,
48+
http://ads.example.com/ad-02.ts
49+
#EXT-X-MY-PROGRAM-DATE-TIME:2014-03-05T11:14:24.024Z
50+
#EXTINF:6.006,
51+
http://media.example.com/03.ts
52+
#EXT-X-MY-PROGRAM-DATE-TIME:2014-03-05T11:14:30.030Z
53+
#EXTINF:3.003,
54+
http://media.example.com/04.ts
55+
`;
56+
let time = new Date('2014-03-05T11:14:00.000Z').getTime();
57+
const segmentProcessor = (lines, start, end, segment) => {
58+
let hasPdt = false;
59+
for (let i = start; i <= end; i++) {
60+
if (lines[i].startsWith('#EXT-X-PROGRAM-DATE-TIME')) {
61+
hasPdt = true;
62+
break;
63+
}
64+
}
65+
if (!hasPdt) {
66+
lines.splice(start, 0, `#EXT-X-MY-PROGRAM-DATE-TIME:${new Date(Math.round(time)).toISOString()}`);
67+
}
68+
time += segment.duration * 1000;
69+
};
70+
const result = HLS.stringify(obj, {segmentProcessor});
71+
t.is(result, utils.stripCommentsAndEmptyLines(expected));
72+
});
73+
74+
test('stringify.postProcess.segment.delete', t => {
75+
const obj = HLS.parse(`
76+
#EXTM3U
77+
#EXT-X-VERSION:3
78+
#EXT-X-TARGETDURATION:6
79+
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z
80+
#EXTINF:6.006,
81+
http://media.example.com/01.ts
82+
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:06.006Z
83+
#EXTINF:6.006,
84+
http://media.example.com/02.ts
85+
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:12.012Z
86+
#EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2014-03-05T11:15:00.000Z",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF000014056FFFFFF000E011622DCAFF000052636200000000000A0008029896F50000008700000000
87+
#EXTINF:6.006,
88+
http://ads.example.com/ad-01.ts
89+
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:18.018Z
90+
#EXTINF:6.006,
91+
http://ads.example.com/ad-02.ts
92+
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:24.024Z
93+
#EXT-X-DATERANGE:ID="splice-6FFFFFF0",DURATION=59.993,SCTE35-IN=0xFC002A0000000000FF00000F056FFFFFF000401162802E6100000000000A0008029896F50000008700000000
94+
#EXTINF:6.006,
95+
http://media.example.com/03.ts
96+
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:30.030Z
97+
#EXTINF:3.003,
98+
http://media.example.com/04.ts
99+
`);
100+
const expected = `
101+
#EXTM3U
102+
#EXT-X-VERSION:3
103+
#EXT-X-TARGETDURATION:6
104+
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z
105+
#EXTINF:6.006,
106+
http://media.example.com/01.ts
107+
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:06.006Z
108+
#EXTINF:6.006,
109+
http://media.example.com/02.ts
110+
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:12.012Z
111+
#EXTINF:6.006,
112+
http://ads.example.com/ad-01.ts
113+
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:18.018Z
114+
#EXTINF:6.006,
115+
http://ads.example.com/ad-02.ts
116+
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:24.024Z
117+
#EXTINF:6.006,
118+
http://media.example.com/03.ts
119+
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:30.030Z
120+
#EXTINF:3.003,
121+
http://media.example.com/04.ts
122+
`;
123+
const segmentProcessor = (lines, start, end) => {
124+
for (let i = start; i <= end; i++) {
125+
const line = lines[i];
126+
if (line.startsWith('#EXT-X-DATERANGE')) {
127+
lines[i] = '';
128+
}
129+
}
130+
};
131+
const result = HLS.stringify(obj, {segmentProcessor});
132+
t.is(result, utils.stripCommentsAndEmptyLines(expected));
133+
});
134+
135+
test('stringify.postProcess.segment.update', t => {
136+
const obj = HLS.parse(`
137+
#EXTM3U
138+
#EXT-X-VERSION:3
139+
#EXT-X-TARGETDURATION:6
140+
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z
141+
#EXTINF:6.006,
142+
http://media.example.com/01.ts
143+
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:06.006Z
144+
#EXTINF:6.006,
145+
http://media.example.com/02.ts
146+
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:12.012Z
147+
#EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2014-03-05T11:15:00.000Z",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF000014056FFFFFF000E011622DCAFF000052636200000000000A0008029896F50000008700000000
148+
#EXTINF:6.006,
149+
http://ads.example.com/ad-01.ts
150+
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:18.018Z
151+
#EXTINF:6.006,
152+
http://ads.example.com/ad-02.ts
153+
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:24.024Z
154+
#EXT-X-DATERANGE:ID="splice-6FFFFFF0",DURATION=59.993,SCTE35-IN=0xFC002A0000000000FF00000F056FFFFFF000401162802E6100000000000A0008029896F50000008700000000
155+
#EXTINF:6.006,
156+
http://media.example.com/03.ts
157+
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:30.030Z
158+
#EXTINF:3.003,
159+
http://media.example.com/04.ts
160+
`);
161+
const expected = `
162+
#EXTM3U
163+
#EXT-X-VERSION:3
164+
#EXT-X-TARGETDURATION:6
165+
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z
166+
#EXTINF:6.006,
167+
http://media.example.com/01.ts
168+
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:06.006Z
169+
#EXTINF:6.006,
170+
http://media.example.com/02.ts
171+
<b>#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:12.012Z
172+
#EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2014-03-05T11:15:00.000Z",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF000014056FFFFFF000E011622DCAFF000052636200000000000A0008029896F50000008700000000
173+
#EXTINF:6.006,
174+
http://ads.example.com/ad-01.ts
175+
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:18.018Z
176+
#EXTINF:6.006,
177+
http://ads.example.com/ad-02.ts</b>
178+
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:24.024Z
179+
#EXT-X-DATERANGE:ID="splice-6FFFFFF0",DURATION=59.993,SCTE35-IN=0xFC002A0000000000FF00000F056FFFFFF000401162802E6100000000000A0008029896F50000008700000000
180+
#EXTINF:6.006,
181+
http://media.example.com/03.ts
182+
#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:30.030Z
183+
#EXTINF:3.003,
184+
http://media.example.com/04.ts
185+
`;
186+
const segmentProcessor = (lines, start, end) => {
187+
for (let i = start; i <= end; i++) {
188+
const line = lines[i];
189+
if (line.startsWith('#EXT-X-DATERANGE')) {
190+
if (line.includes('PLANNED-DURATION')) {
191+
lines[start] = `<b>${lines[start]}`;
192+
} else if (start > 0) {
193+
lines[start - 1] = `${lines[start - 1]}</b>`;
194+
}
195+
}
196+
}
197+
};
198+
const result = HLS.stringify(obj, {segmentProcessor});
199+
t.is(result, utils.stripCommentsAndEmptyLines(expected));
200+
});
201+
202+
test('stringify.postProcess.variant.update', t => {
203+
const obj = HLS.parse(`
204+
#EXTM3U
205+
#EXT-X-STREAM-INF:BANDWIDTH=1280000
206+
http://example.com/low.m3u8
207+
#EXT-X-STREAM-INF:BANDWIDTH=2560000
208+
http://example.com/mid.m3u8
209+
#EXT-X-STREAM-INF:BANDWIDTH=7680000
210+
http://example.com/hi.m3u8
211+
`);
212+
const expected = `
213+
#EXTM3U
214+
#EXT-X-STREAM-INF:BANDWIDTH=1280000,MY-RESOLUTION=1280x720
215+
http://example.com/low.m3u8
216+
#EXT-X-STREAM-INF:BANDWIDTH=2560000,MY-RESOLUTION=1920x1080
217+
http://example.com/mid.m3u8
218+
#EXT-X-STREAM-INF:BANDWIDTH=7680000,MY-RESOLUTION=3840x2160
219+
http://example.com/hi.m3u8
220+
`;
221+
const variantProcessor = (lines, start, end, {bandwidth}) => {
222+
for (let i = start; i <= end; i++) {
223+
const line = lines[i];
224+
if (line.startsWith('#EXT-X-STREAM-INF')) {
225+
let resolution = '640x360';
226+
if (bandwidth >= 1000000 && bandwidth < 2000000) {
227+
resolution = '1280x720';
228+
} else if (bandwidth >= 2000000 && bandwidth < 3000000) {
229+
resolution = '1920x1080';
230+
} else if (bandwidth >= 3000000) {
231+
resolution = '3840x2160';
232+
}
233+
lines[i] = `${line},MY-RESOLUTION=${resolution}`;
234+
}
235+
}
236+
};
237+
const result = HLS.stringify(obj, {variantProcessor});
238+
t.is(result, utils.stripCommentsAndEmptyLines(expected));
239+
});

types.ts

+5
Original file line numberDiff line numberDiff line change
@@ -518,3 +518,8 @@ export type TagParam =
518518
| [ Date, null ];
519519

520520
export type UserAttribute = number | string | Uint8Array;
521+
522+
export type PostProcess = {
523+
segmentProcessor: ((lines: string[], start: number, end: number, segment: Segment, i: number) => undefined) | undefined;
524+
variantProcessor: ((lines: string[], start: number, end: number, variant: Variant, i: number) => undefined) | undefined;
525+
};

0 commit comments

Comments
 (0)