diff --git a/parse.ts b/parse.ts index d8e54d9..0b909b8 100644 --- a/parse.ts +++ b/parse.ts @@ -49,6 +49,7 @@ function getTagCategory(tagName: string): TagCategory { case 'EXT-X-SCTE35': case 'EXT-X-PART': case 'EXT-X-PRELOAD-HINT': + case 'EXT-X-GAP': return 'Segment'; case 'EXT-X-TARGETDURATION': case 'EXT-X-MEDIA-SEQUENCE': @@ -214,6 +215,7 @@ function parseTagParam(name: string, param): TagParam { case 'EXT-X-I-FRAMES-ONLY': case 'EXT-X-INDEPENDENT-SEGMENTS': case 'EXT-X-CUE-IN': + case 'EXT-X-GAP': return [null, null]; case 'EXT-X-VERSION': case 'EXT-X-TARGETDURATION': @@ -508,6 +510,11 @@ function parseSegment(lines: Line[], uri: string, start: number, end: number, me utils.INVALIDPLAYLIST('EXT-X-DISCONTINUITY must appear before the first EXT-X-PART tag of the Parent Segment.'); } segment.discontinuity = true; + } else if (name === 'EXT-X-GAP') { + if (params.compatibleVersion < 8) { + params.compatibleVersion = 8; + } + segment.gap = true; } else if (name === 'EXT-X-KEY') { if (segment.parts.length > 0) { utils.INVALIDPLAYLIST('EXT-X-KEY must appear before the first EXT-X-PART tag of the Parent Segment.'); @@ -604,6 +611,10 @@ function parseSegment(lines: Line[], uri: string, start: number, end: number, me independent: attributes['INDEPENDENT'], gap: attributes['GAP'] }); + if (segment.gap && !partialSegment.gap) { + // https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-6.2.1 + utils.INVALIDPLAYLIST('Partial segments must have GAP=YES if they are in a gap (EXT-X-GAP)'); + } segment.parts.push(partialSegment); } } diff --git a/test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.4.4.7_EXT-X-GAP.spec.js b/test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.4.4.7_EXT-X-GAP.spec.js new file mode 100644 index 0000000..e91af63 --- /dev/null +++ b/test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.4.4.7_EXT-X-GAP.spec.js @@ -0,0 +1,40 @@ +const test = require("ava"); +const utils = require("../../../../helpers/utils"); + +// https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-4.4.4.7 + +test('#EXT-X-TAG_01', t => { + utils.parseFail(t, ` + #EXTM3U + #EXT-X-VERSION:8 + #EXT-X-GAP + 1.ts + `); + utils.parsePass(t, ` + #EXTM3U + #EXT-X-VERSION:8 + #EXT-X-TARGETDURATION:5 + #EXT-X-GAP + #EXTINF:4, + 1.ts + `); +}); + +test('#EXT-X-TAG_02', t => { + utils.parseFail(t, ` + #EXTM3U + #EXT-X-VERSION:8 + #EXT-X-TARGETDURATION:5 + #EXT-X-GAP + #EXT-X-PART:DURATION=2,URI="1.ts" + #EXT-X-ENDLIST + `); + utils.parsePass(t, ` + #EXTM3U + #EXT-X-VERSION:8 + #EXT-X-TARGETDURATION:5 + #EXT-X-GAP + #EXT-X-PART:DURATION=2,URI="1.ts",GAP=YES + #EXT-X-ENDLIST + `); +}) \ No newline at end of file diff --git a/test/spec/7_Protocol-version-compatibility/7_EXT-X-VERSION.spec.js b/test/spec/7_Protocol-version-compatibility/7_EXT-X-VERSION.spec.js index 64fe071..1ea5032 100644 --- a/test/spec/7_Protocol-version-compatibility/7_EXT-X-VERSION.spec.js +++ b/test/spec/7_Protocol-version-compatibility/7_EXT-X-VERSION.spec.js @@ -274,3 +274,16 @@ test('#EXT-X-VERSION_11', t => { http://example.com `); }); + +// A Media Playlist MUST indicate a EXT-X-VERSION of 8 or higher if it +// contains: +// - the "EXT-X-GAP" tag. +test('#EXT-X-VERSION_12', t => { + utils.parseFail(t, ` + #EXTM3U + #EXT-X-VERSION:1 + #EXTINF:5 + #EXT-X-GAP + http://example.com + `); +}); diff --git a/types.ts b/types.ts index a3867dd..8c7ce9b 100644 --- a/types.ts +++ b/types.ts @@ -367,6 +367,7 @@ class Segment extends Data { dateRange: DateRange; markers: SpliceInfo[]; parts: PartialSegment[]; + gap?: boolean; constructor({ uri, @@ -383,7 +384,8 @@ class Segment extends Data { programDateTime, dateRange, markers = [], - parts = [] + parts = [], + gap }: any) { super('segment'); // utils.PARAMCHECK(uri, mediaSequenceNumber, discontinuitySequence); @@ -402,6 +404,7 @@ class Segment extends Data { this.dateRange = dateRange; this.markers = markers; this.parts = parts; + this.gap = gap; } }