Skip to content

Commit 99ec221

Browse files
authored
feat: HLS parser - renditions & variants (#146)
* Make uri optional * Fix EXT-X-MAP * Fixed tests
1 parent 2b59303 commit 99ec221

File tree

11 files changed

+253
-329
lines changed

11 files changed

+253
-329
lines changed

packages/stitcher/src/filters.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,14 @@ export function filterMasterPlaylist(master: MasterPlaylist, filter: Filter) {
8585
}
8686
if (filter.audioLanguage !== undefined) {
8787
const list = parseFilterToList(filter.audioLanguage);
88-
master.variants.filter((variant) => {
89-
variant.audio = variant.audio.filter(
90-
(audio) => !audio.language || list.includes(audio.language),
91-
);
88+
master.renditions = master.renditions.filter((rendition) => {
89+
if (rendition.type === "AUDIO") {
90+
if (rendition.language && list.includes(rendition.language)) {
91+
return true;
92+
}
93+
return false;
94+
}
95+
return true;
9296
});
9397
}
9498
}

packages/stitcher/src/parser/helpers.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,3 @@
1-
import type { Rendition, Variant } from "./types";
2-
3-
export function getRenditions(variants: Variant[]) {
4-
const group = new Set<Rendition>();
5-
variants.forEach((variant) => {
6-
variant.audio.forEach((rendition) => {
7-
group.add(rendition);
8-
});
9-
variant.subtitles.forEach((rendition) => {
10-
group.add(rendition);
11-
});
12-
});
13-
return Array.from(group.values());
14-
}
15-
161
export function mapAttributes(
172
param: string,
183
callback: (key: string, value: string) => void,
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export { parseMasterPlaylist, parseMediaPlaylist } from "./parse";
22
export { stringifyMasterPlaylist, stringifyMediaPlaylist } from "./stringify";
3-
export { getRenditions } from "./helpers";
43

54
export * from "./types";

packages/stitcher/src/parser/lexical-parse.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,12 @@ export interface StreamInf {
5454
subtitles?: string;
5555
}
5656

57-
export type MediaType = "AUDIO" | "SUBTITLES";
58-
5957
export interface Media {
60-
type: MediaType;
58+
type: "AUDIO" | "SUBTITLES";
6159
groupId: string;
6260
name: string;
6361
language?: string;
64-
uri: string;
62+
uri?: string;
6563
channels?: string;
6664
}
6765

@@ -187,7 +185,6 @@ function parseLine(line: string): Tag | null {
187185
assert(attrs.type, "EXT-X-MEDIA: no type");
188186
assert(attrs.groupId, "EXT-X-MEDIA: no groupId");
189187
assert(attrs.name, "EXT-X-MEDIA: no name");
190-
assert(attrs.uri, "EXT-X-MEDIA: no uri");
191188

192189
return [
193190
name,

packages/stitcher/src/parser/parse.ts

Lines changed: 71 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { assert } from "shared/assert";
22
import { lexicalParse } from "./lexical-parse";
3-
import type { Media, StreamInf, Tag } from "./lexical-parse";
3+
import type { Tag } from "./lexical-parse";
44
import type {
55
DateRange,
66
MasterPlaylist,
@@ -13,14 +13,52 @@ import type {
1313
} from "./types";
1414
import type { DateTime } from "luxon";
1515

16+
function formatMasterPlaylist(tags: Tag[]): MasterPlaylist {
17+
let independentSegments = false;
18+
const variants: Variant[] = [];
19+
const renditions: Rendition[] = [];
20+
21+
tags.forEach(([name, value], index) => {
22+
if (name === "EXT-X-INDEPENDENT-SEGMENTS") {
23+
independentSegments = true;
24+
}
25+
if (name === "EXT-X-MEDIA") {
26+
renditions.push({
27+
type: value.type,
28+
groupId: value.groupId,
29+
name: value.name,
30+
uri: value.uri,
31+
channels: value.channels,
32+
language: value.language,
33+
});
34+
}
35+
if (name === "EXT-X-STREAM-INF") {
36+
const uri = nextLiteral(tags, index);
37+
variants.push({
38+
uri,
39+
bandwidth: value.bandwidth,
40+
resolution: value.resolution,
41+
codecs: value.codecs,
42+
audio: value.audio,
43+
subtitles: value.subtitles,
44+
});
45+
}
46+
});
47+
48+
return {
49+
independentSegments,
50+
variants,
51+
renditions,
52+
};
53+
}
54+
1655
function formatMediaPlaylist(tags: Tag[]): MediaPlaylist {
1756
let targetDuration: number | undefined;
1857
let endlist = false;
1958
let playlistType: PlaylistType | undefined;
2059
let independentSegments = false;
2160
let mediaSequenceBase: number | undefined;
2261
let discontinuitySequenceBase: number | undefined;
23-
let map: MediaInitializationSection | undefined;
2462
const dateRanges: DateRange[] = [];
2563

2664
tags.forEach(([name, value]) => {
@@ -33,9 +71,6 @@ function formatMediaPlaylist(tags: Tag[]): MediaPlaylist {
3371
if (name === "EXT-X-PLAYLIST-TYPE") {
3472
playlistType = value;
3573
}
36-
if (name === "EXT-X-MAP") {
37-
map = value;
38-
}
3974
if (name === "EXT-X-INDEPENDENT-SEGMENTS") {
4075
independentSegments = true;
4176
}
@@ -53,7 +88,12 @@ function formatMediaPlaylist(tags: Tag[]): MediaPlaylist {
5388
const segments: Segment[] = [];
5489
let segmentStart = -1;
5590

56-
tags.forEach(([name], index) => {
91+
let map: MediaInitializationSection | undefined;
92+
tags.forEach(([name, value], index) => {
93+
if (name === "EXT-X-MAP") {
94+
map = value;
95+
}
96+
5797
if (isSegmentTag(name)) {
5898
segmentStart = index - 1;
5999
}
@@ -86,6 +126,31 @@ function formatMediaPlaylist(tags: Tag[]): MediaPlaylist {
86126
};
87127
}
88128

129+
function nextLiteral(tags: Tag[], index: number) {
130+
if (!tags[index + 1]) {
131+
throw new Error("Expecting next tag to be found");
132+
}
133+
const tag = tags[index + 1];
134+
if (!tag) {
135+
throw new Error(`Expected valid tag on ${index + 1}`);
136+
}
137+
const [name, value] = tag;
138+
if (name !== "LITERAL") {
139+
throw new Error("Expecting next tag to be a literal");
140+
}
141+
return value;
142+
}
143+
144+
function isSegmentTag(name: Tag[0]) {
145+
switch (name) {
146+
case "EXTINF":
147+
case "EXT-X-DISCONTINUITY":
148+
case "EXT-X-PROGRAM-DATE-TIME":
149+
return true;
150+
}
151+
return false;
152+
}
153+
89154
function parseSegment(
90155
tags: Tag[],
91156
uri: string,
@@ -118,120 +183,6 @@ function parseSegment(
118183
};
119184
}
120185

121-
function createRendition(media: Media, renditions: Map<string, Rendition>) {
122-
let rendition = renditions.get(media.uri);
123-
if (rendition) {
124-
return rendition;
125-
}
126-
127-
rendition = {
128-
type: media.type,
129-
groupId: media.groupId,
130-
name: media.name,
131-
language: media.language,
132-
uri: media.uri,
133-
channels: media.channels,
134-
};
135-
136-
renditions.set(media.uri, rendition);
137-
138-
return rendition;
139-
}
140-
141-
function addRendition(
142-
variant: Variant,
143-
media: Media,
144-
renditions: Map<string, Rendition>,
145-
) {
146-
const rendition = createRendition(media, renditions);
147-
148-
if (media.type === "AUDIO") {
149-
variant.audio.push(rendition);
150-
}
151-
152-
if (media.type === "SUBTITLES") {
153-
variant.subtitles.push(rendition);
154-
}
155-
}
156-
157-
function parseVariant(
158-
tags: Tag[],
159-
streamInf: StreamInf,
160-
uri: string,
161-
renditions: Map<string, Rendition>,
162-
) {
163-
const variant: Variant = {
164-
uri,
165-
bandwidth: streamInf.bandwidth,
166-
resolution: streamInf.resolution,
167-
codecs: streamInf.codecs,
168-
audio: [],
169-
subtitles: [],
170-
};
171-
172-
for (const [name, value] of tags) {
173-
if (name === "EXT-X-MEDIA") {
174-
if (
175-
streamInf.audio === value.groupId ||
176-
streamInf.subtitles === value.groupId
177-
) {
178-
addRendition(variant, value, renditions);
179-
}
180-
}
181-
}
182-
183-
return variant;
184-
}
185-
186-
function formatMasterPlaylist(tags: Tag[]): MasterPlaylist {
187-
const variants: Variant[] = [];
188-
let independentSegments = false;
189-
190-
const renditions = new Map<string, Rendition>();
191-
192-
tags.forEach(([name, value], index) => {
193-
if (name === "EXT-X-STREAM-INF") {
194-
const uri = nextLiteral(tags, index);
195-
const variant = parseVariant(tags, value, uri, renditions);
196-
variants.push(variant);
197-
}
198-
if (name === "EXT-X-INDEPENDENT-SEGMENTS") {
199-
independentSegments = true;
200-
}
201-
});
202-
203-
return {
204-
independentSegments,
205-
variants,
206-
};
207-
}
208-
209-
function nextLiteral(tags: Tag[], index: number) {
210-
if (!tags[index + 1]) {
211-
throw new Error("Expecting next tag to be found");
212-
}
213-
const tag = tags[index + 1];
214-
if (!tag) {
215-
throw new Error(`Expected valid tag on ${index + 1}`);
216-
}
217-
const [name, value] = tag;
218-
if (name !== "LITERAL") {
219-
throw new Error("Expecting next tag to be a literal");
220-
}
221-
return value;
222-
}
223-
224-
function isSegmentTag(name: Tag[0]) {
225-
switch (name) {
226-
case "EXTINF":
227-
case "EXT-X-DISCONTINUITY":
228-
case "EXT-X-MAP":
229-
case "EXT-X-PROGRAM-DATE-TIME":
230-
return true;
231-
}
232-
return false;
233-
}
234-
235186
export function parseMasterPlaylist(text: string) {
236187
const tags = lexicalParse(text);
237188
return formatMasterPlaylist(tags);

0 commit comments

Comments
 (0)