diff --git a/src/parse.ts b/src/parse.ts index d0f0de8..f88bacf 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -15,6 +15,7 @@ import { RenditionReport, ByteRange, Resolution, + CustomTags, } from './types'; export interface ParseParams { @@ -25,6 +26,7 @@ export interface ParseParams { compatibleVersion: number; isClosedCaptionsNone: boolean; hash: Record; + customTags: CustomTags; } export interface Tag { @@ -36,6 +38,7 @@ export interface Tag { export type TagName = // Basic | 'EXTM3U' + | 'EXT-X-CUSTOM-TAGS' | 'EXT-X-VERSION' // Segment | 'EXTINF' @@ -102,7 +105,8 @@ export type TagParams = | [ExtInf, null] | [ByteRange, null] | ['EVENT' | 'VOID', null] - | [unknown, null]; + | [unknown, null] + | [CustomTags, null]; function unquote(str) { return utils.trim(str, '"'); @@ -112,6 +116,7 @@ function getTagCategory(tagName: TagName | string): TagCategory { switch (tagName) { case 'EXTM3U': case 'EXT-X-VERSION': + case 'EXT-X-CUSTOM-TAGS': return 'Basic'; case 'EXTINF': case 'EXT-X-BYTERANGE': @@ -300,6 +305,8 @@ function parseTagParam(name: TagName, param: string): TagParams { case 'EXT-X-INDEPENDENT-SEGMENTS': case 'EXT-X-CUE-IN': return [null, null]; + case 'EXT-X-CUSTOM-TAGS': + return [parseCustomTag(param), null]; case 'EXT-X-VERSION': case 'EXT-X-TARGETDURATION': case 'EXT-X-MEDIA-SEQUENCE': @@ -490,6 +497,8 @@ function parseMasterPlaylist(lines, params) { for (const [index, { name, value, attributes }] of lines.entries()) { if (name === 'EXT-X-VERSION') { playlist.version = value; + } else if (name === 'EXT-X-CUSTOM-TAGS') { + playlist.customTags = params.value; } else if (name === 'EXT-X-STREAM-INF') { const uri = lines[index + 1]; if (typeof uri !== 'string' || uri.startsWith('#EXT')) { @@ -780,6 +789,8 @@ function parseMediaPlaylist(lines, params) { } else { utils.INVALIDPLAYLIST('A Playlist file MUST NOT contain more than one EXT-X-VERSION tag.'); } + } else if (name === 'EXT-X-CUSTOM-TAGS') { + playlist.customTags = params.value; } else if (name === 'EXT-X-TARGETDURATION') { playlist.targetDuration = params.targetDuration = value; } else if (name === 'EXT-X-MEDIA-SEQUENCE') { @@ -1118,6 +1129,17 @@ function CHECKTAGCATEGORY(category, params) { // category === 'Basic' or 'MediaorMasterPlaylist' or 'Unknown' } +function parseCustomTag(params: string): CustomTags { + const customTags = {}; + if (params) { + for (const pair of params.split(';')) { + const [k, v] = pair.split('='); + customTags[k] = JSON.parse(v); + } + } + return customTags; +} + function parseTag(line: string, params: ParseParams): Tag { const [name, param] = splitTag(line); const category = getTagCategory(name); @@ -1195,6 +1217,7 @@ export function parse(text) { compatibleVersion: 1, isClosedCaptionsNone: false, hash: {}, + customTags: {}, }; const lines = lexicalParse(text, params); diff --git a/src/stringify.ts b/src/stringify.ts index 50ed512..29b7eba 100644 --- a/src/stringify.ts +++ b/src/stringify.ts @@ -1,4 +1,4 @@ -import { Key } from './types'; +import { CustomTags, Key, MasterPlaylist, MediaPlaylist } from './types'; import * as utils from './utils'; const ALLOW_REDUNDANCY = [ @@ -428,7 +428,12 @@ function buildParts(lines, parts) { return hint; } -export function stringify(playlist) { +function buildCustomTags(lines: string[], customTags: CustomTags) { + const pairs = Object.entries(customTags).map(([k, v]) => `${k}=${v}`); + lines.push(`#EXT-X-CUSTOM-TAGS:${pairs.join(';')}`); +} + +export function stringify(playlist: MasterPlaylist | MediaPlaylist) { utils.PARAMCHECK(playlist); utils.ASSERT('Not a playlist', playlist.type === 'playlist'); const lines = new LineArray(playlist.uri); @@ -446,6 +451,9 @@ export function stringify(playlist) { }`, ); } + if (playlist.customTags) { + buildCustomTags(lines, playlist.customTags); + } if (playlist.isMasterPlaylist) { buildMasterPlaylist(lines, playlist); } else { diff --git a/src/types.ts b/src/types.ts index 7a34559..0812196 100644 --- a/src/types.ts +++ b/src/types.ts @@ -417,6 +417,8 @@ export interface PlaylistStart { precise: boolean; } +export type CustomTags = Record; + export interface PlaylistProperties extends Data { isMasterPlaylist: boolean; uri: string; @@ -424,10 +426,11 @@ export interface PlaylistProperties extends Data { independentSegments: boolean; start: PlaylistStart; source: string; + customTags: CustomTags; } export type PlaylistOptionalConstructorProperties = Partial< - Pick + Pick >; export type PlaylistRequiredConstructorProperties = Pick; export type PlaylistConstructorProperties = PlaylistOptionalConstructorProperties & @@ -440,6 +443,7 @@ export class Playlist extends Data implements PlaylistProperties { public independentSegments: boolean; public start: PlaylistStart; public source: string; + public customTags: CustomTags; constructor({ isMasterPlaylist, // required @@ -448,6 +452,7 @@ export class Playlist extends Data implements PlaylistProperties { independentSegments = false, start, source, + customTags = {}, }: PlaylistConstructorProperties) { super('playlist'); utils.PARAMCHECK(isMasterPlaylist); @@ -457,6 +462,7 @@ export class Playlist extends Data implements PlaylistProperties { this.independentSegments = independentSegments; this.start = start; this.source = source; + this.customTags = customTags; } }