Skip to content

Commit 3678e69

Browse files
committed
Parse EXT-X-KEY
1 parent 771a47e commit 3678e69

File tree

5 files changed

+117
-3
lines changed

5 files changed

+117
-3
lines changed

apps/stitcher/src/parser/helpers.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,32 @@ function splitByCommaWithPreservingQuotes(str: string) {
5757

5858
return list;
5959
}
60+
61+
export function hexToByteSequence(str: string): Uint8Array {
62+
if (str.startsWith("0x") || str.startsWith("0X")) {
63+
str = str.slice(2);
64+
}
65+
const numArray = new Uint8Array(str.length / 2);
66+
for (let i = 0; i < str.length; i += 2) {
67+
numArray[i / 2] = Number.parseInt(str.slice(i, i + 2), 16);
68+
}
69+
return numArray;
70+
}
71+
72+
export function byteSequenceToHex(
73+
sequence: Uint8Array,
74+
start = 0,
75+
end = sequence.byteLength,
76+
) {
77+
if (end <= start) {
78+
throw new Error("End must be larger than start");
79+
}
80+
const list: string[] = [];
81+
for (let i = start; i < end; i++) {
82+
const chunk = sequence[i];
83+
if (chunk !== undefined) {
84+
list.push(`0${(chunk & 0xff).toString(16).toUpperCase()}`.slice(-2));
85+
}
86+
}
87+
return `0x${list.join("")}`;
88+
}

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

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { DateTime } from "luxon";
22
import { assert } from "shared/assert";
3-
import { mapAttributes, partOf } from "./helpers";
3+
import { hexToByteSequence, mapAttributes, partOf } from "./helpers";
44
import type {
55
DateRange,
6+
Key,
67
MediaInitializationSection,
78
PlaylistType,
89
Resolution,
@@ -42,7 +43,8 @@ export type Tag =
4243
| ["EXT-X-MEDIA", Media]
4344
| ["EXT-X-MAP", MediaInitializationSection]
4445
| ["EXT-X-DATERANGE", DateRange]
45-
| ["EXT-X-CUE-OUT", CueOut];
46+
| ["EXT-X-CUE-OUT", CueOut]
47+
| ["EXT-X-KEY", Key];
4648

4749
export interface ExtInf {
4850
duration: number;
@@ -311,11 +313,53 @@ function parseLine(line: string): Tag | null {
311313
];
312314
}
313315

316+
case "EXT-X-KEY": {
317+
assert(param, "EXT-X-KEY: no param");
318+
319+
const attrs: Partial<Key> = {};
320+
321+
mapAttributes(param, (key, value) => {
322+
switch (key) {
323+
case "METHOD":
324+
case "URI":
325+
case "KEYFORMAT":
326+
case "KEYFORMATVERSION":
327+
attrs.method = value;
328+
break;
329+
330+
case "IV":
331+
attrs.iv = parseIV(value);
332+
break;
333+
}
334+
});
335+
336+
assert(attrs.method, "EXT-X-KEY: no method");
337+
338+
return [
339+
name,
340+
{
341+
method: attrs.method,
342+
uri: attrs.uri,
343+
iv: attrs.iv,
344+
format: attrs.format,
345+
formatVersion: attrs.formatVersion,
346+
},
347+
];
348+
}
349+
314350
default:
315351
return null;
316352
}
317353
}
318354

355+
function parseIV(value: string) {
356+
const iv = hexToByteSequence(value);
357+
if (iv.length !== 16) {
358+
throw new Error("IV must be a 128-bit unsigned integer");
359+
}
360+
return iv;
361+
}
362+
319363
function splitLine(line: string): [string, string | null] {
320364
const index = line.indexOf(":");
321365
if (index === -1) {

apps/stitcher/src/parser/parse.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { lexicalParse } from "./lexical-parse";
33
import type { Tag } from "./lexical-parse";
44
import type {
55
DateRange,
6+
Key,
67
MasterPlaylist,
78
MediaInitializationSection,
89
MediaPlaylist,
@@ -90,12 +91,18 @@ function formatMediaPlaylist(tags: Tag[]): MediaPlaylist {
9091
let segmentStart = -1;
9192

9293
let map: MediaInitializationSection | undefined;
94+
let key: Key | undefined;
9395
tags.forEach(([name, value], index) => {
9496
if (name === "EXT-X-MAP") {
9597
// TODO: We might be better off passing on segments to |parseSegment| and look up
9698
// the last valid map.
9799
map = value;
98100
}
101+
if (name === "EXT-X-KEY") {
102+
// TODO: We might be better off passing on segments to |parseSegment| and look up
103+
// the last valid key.
104+
key = value;
105+
}
99106

100107
// TODO: When we have EXT-X-KEY support, we're better off passing a full list of segments
101108
// to |parseSegment|, maybe?
@@ -114,7 +121,7 @@ function formatMediaPlaylist(tags: Tag[]): MediaPlaylist {
114121
const segmentTags = tags.slice(segmentStart, index + 1);
115122
const uri = nextLiteral(segmentTags, segmentTags.length - 2);
116123

117-
const segment = parseSegment(segmentTags, uri, map);
124+
const segment = parseSegment(segmentTags, uri, map, key);
118125
segments.push(segment);
119126

120127
segmentStart = -1;
@@ -164,6 +171,7 @@ function isSegmentTag(name: Tag[0]) {
164171
case "EXT-X-MAP":
165172
case "EXT-X-CUE-OUT":
166173
case "EXT-X-CUE-IN":
174+
case "EXT-X-KEY":
167175
return true;
168176
}
169177
return false;
@@ -173,6 +181,7 @@ function parseSegment(
173181
tags: Tag[],
174182
uri: string,
175183
map?: MediaInitializationSection,
184+
key?: Key,
176185
): Segment {
177186
let duration: number | undefined;
178187
let discontinuity: boolean | undefined;
@@ -204,6 +213,7 @@ function parseSegment(
204213
duration,
205214
discontinuity,
206215
map,
216+
key,
207217
programDateTime,
208218
spliceInfo,
209219
};

apps/stitcher/src/parser/stringify.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { byteSequenceToHex } from "./helpers";
12
import type {
3+
Key,
24
MasterPlaylist,
35
MediaInitializationSection,
46
MediaPlaylist,
@@ -99,6 +101,7 @@ export function stringifyMediaPlaylist(playlist: MediaPlaylist) {
99101
}
100102

101103
let lastMap: MediaInitializationSection | undefined;
104+
let lastKey: Key | undefined;
102105

103106
playlist.segments.forEach((segment) => {
104107
// See https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-16#section-4.4.4.5
@@ -111,6 +114,25 @@ export function stringifyMediaPlaylist(playlist: MediaPlaylist) {
111114
}
112115
lastMap = segment.map;
113116
}
117+
if (segment.key !== lastKey) {
118+
if (segment.key) {
119+
const attrs = [`METHOD=${segment.key.method}`];
120+
if (segment.key.uri) {
121+
attrs.push(`URI="${segment.key.uri}"`);
122+
}
123+
if (segment.key.iv) {
124+
attrs.push(`IV=${byteSequenceToHex(segment.key.iv)}`);
125+
}
126+
if (segment.key.format) {
127+
attrs.push(`KEYFORMAT="${segment.key.format}"`);
128+
}
129+
if (segment.key.formatVersion) {
130+
attrs.push(`KEYFORMATVERSIONS="${segment.key.formatVersion}"`);
131+
}
132+
lines.push(`#EXT-X-KEY:${attrs.join(",")}`);
133+
}
134+
lastKey = segment.key;
135+
}
114136

115137
if (segment.discontinuity) {
116138
lines.push(`#EXT-X-DISCONTINUITY`);

apps/stitcher/src/parser/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export interface Segment {
3838
duration: number;
3939
discontinuity?: boolean;
4040
map?: MediaInitializationSection;
41+
key?: Key;
4142
programDateTime?: DateTime;
4243
spliceInfo?: SpliceInfo;
4344
}
@@ -49,6 +50,14 @@ export interface SpliceInfo {
4950

5051
export type PlaylistType = "EVENT" | "VOD";
5152

53+
export interface Key {
54+
method: string;
55+
uri?: string;
56+
iv?: Uint8Array;
57+
format?: string;
58+
formatVersion?: string;
59+
}
60+
5261
export interface MediaPlaylist {
5362
independentSegments?: boolean;
5463
targetDuration: number;

0 commit comments

Comments
 (0)