UNPKG

hls-parser

Version:

A simple library to read/write HLS playlists

535 lines (534 loc) 18.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const utils = __importStar(require("./utils")); const ALLOW_REDUNDANCY = [ '#EXTINF', '#EXT-X-BYTERANGE', '#EXT-X-DISCONTINUITY', '#EXT-X-STREAM-INF', '#EXT-X-CUE-OUT', '#EXT-X-CUE-IN', '#EXT-X-KEY', '#EXT-X-MAP' ]; const SKIP_IF_REDUNDANT = [ '#EXT-X-MEDIA' ]; class LineArray extends Array { baseUri; constructor(baseUri) { super(); this.baseUri = baseUri; } push(...elems) { // redundancy check for (const elem of elems) { if (!elem.startsWith('#')) { super.push(elem); continue; } if (ALLOW_REDUNDANCY.some(item => elem.startsWith(item))) { super.push(elem); continue; } if (this.includes(elem)) { if (SKIP_IF_REDUNDANT.some(item => elem.startsWith(item))) { continue; } utils.INVALIDPLAYLIST(`Redundant item (${elem})`); } super.push(elem); } return this.length; } join(separator = ',') { for (let i = this.length - 1; i >= 0; i--) { if (!this[i]) { this.splice(i, 1); } } return super.join(separator); } } function buildDecimalFloatingNumber(num, fixed) { let roundFactor = 1000; if (fixed) { roundFactor = 10 ** fixed; } const rounded = Math.round(num * roundFactor) / roundFactor; return fixed ? rounded.toFixed(fixed) : rounded; } function getNumberOfDecimalPlaces(num) { const str = num.toString(10); const index = str.indexOf('.'); if (index === -1) { return 0; } return str.length - index - 1; } function buildMasterPlaylist(lines, playlist, postProcess) { if (playlist.contentSteering) { lines.push(buildContentSteeringServer(playlist.contentSteering)); } for (const sessionData of playlist.sessionDataList) { lines.push(buildSessionData(sessionData)); } for (const sessionKey of playlist.sessionKeyList) { lines.push(buildKey(sessionKey, true)); } for (const [i, variant] of playlist.variants.entries()) { const base = lines.length; buildVariant(lines, variant); if (postProcess?.variantProcessor) { postProcess.variantProcessor(lines, base, lines.length - 1, variant, i); } } } function buildContentSteeringServer(contentSteering) { const attrs = [ `SERVER-URI="${contentSteering.serverUri}"`, `PATHWAY-ID="${contentSteering.pathwayId}"` ]; return `#EXT-X-CONTENT-STEERING:${attrs.join(',')}`; } function buildSessionData(sessionData) { const attrs = [`DATA-ID="${sessionData.id}"`]; if (sessionData.language) { attrs.push(`LANGUAGE="${sessionData.language}"`); } if (sessionData.value) { attrs.push(`VALUE="${sessionData.value}"`); } else if (sessionData.uri) { attrs.push(`URI="${sessionData.uri}"`); } return `#EXT-X-SESSION-DATA:${attrs.join(',')}`; } function buildKey(key, isSessionKey) { const name = isSessionKey ? '#EXT-X-SESSION-KEY' : '#EXT-X-KEY'; const attrs = [`METHOD=${key.method}`]; if (key.uri) { attrs.push(`URI="${key.uri}"`); } if (key.iv) { if (key.iv.byteLength !== 16) { utils.INVALIDPLAYLIST('IV must be a 128-bit unsigned integer'); } attrs.push(`IV=${utils.byteSequenceToHex(key.iv)}`); } if (key.format) { attrs.push(`KEYFORMAT="${key.format}"`); } if (key.formatVersion) { attrs.push(`KEYFORMATVERSIONS="${key.formatVersion}"`); } return `${name}:${attrs.join(',')}`; } function buildVariant(lines, variant) { const name = variant.isIFrameOnly ? '#EXT-X-I-FRAME-STREAM-INF' : '#EXT-X-STREAM-INF'; const attrs = [`BANDWIDTH=${variant.bandwidth}`]; if (variant.averageBandwidth) { attrs.push(`AVERAGE-BANDWIDTH=${variant.averageBandwidth}`); } if (variant.isIFrameOnly) { attrs.push(`URI="${variant.uri}"`); } if (variant.codecs) { attrs.push(`CODECS="${variant.codecs}"`); } if (variant.resolution) { attrs.push(`RESOLUTION=${variant.resolution.width}x${variant.resolution.height}`); } if (variant.frameRate) { attrs.push(`FRAME-RATE=${buildDecimalFloatingNumber(variant.frameRate, 3)}`); } if (variant.hdcpLevel) { attrs.push(`HDCP-LEVEL=${variant.hdcpLevel}`); } if (variant.audio.length > 0) { attrs.push(`AUDIO="${variant.audio[0].groupId}"`); for (const rendition of variant.audio) { lines.push(buildRendition(rendition)); } } if (variant.video.length > 0) { attrs.push(`VIDEO="${variant.video[0].groupId}"`); for (const rendition of variant.video) { lines.push(buildRendition(rendition)); } } if (variant.subtitles.length > 0) { attrs.push(`SUBTITLES="${variant.subtitles[0].groupId}"`); for (const rendition of variant.subtitles) { lines.push(buildRendition(rendition)); } } if (utils.getOptions().allowClosedCaptionsNone && variant.closedCaptions.length === 0) { attrs.push(`CLOSED-CAPTIONS=NONE`); } else if (variant.closedCaptions.length > 0) { attrs.push(`CLOSED-CAPTIONS="${variant.closedCaptions[0].groupId}"`); for (const rendition of variant.closedCaptions) { lines.push((buildRendition(rendition))); } } if (variant.score) { attrs.push(`SCORE=${variant.score}`); } if (variant.allowedCpc) { const list = []; for (const { format, cpcList } of variant.allowedCpc) { list.push(`${format}:${cpcList.join('/')}`); } attrs.push(`ALLOWED-CPC="${list.join(',')}"`); } if (variant.videoRange) { attrs.push(`VIDEO-RANGE=${variant.videoRange}`); } if (variant.stableVariantId) { attrs.push(`STABLE-VARIANT-ID="${variant.stableVariantId}"`); } if (variant.pathwayId) { attrs.push(`PATHWAY-ID="${variant.pathwayId}"`); } if (variant.programId) { attrs.push(`PROGRAM-ID=${variant.programId}`); } lines.push(`${name}:${attrs.join(',')}`); if (!variant.isIFrameOnly) { lines.push(`${variant.uri}`); } } function buildRendition(rendition) { const attrs = [ `TYPE=${rendition.type}`, `GROUP-ID="${rendition.groupId}"`, `NAME="${rendition.name}"` ]; if (rendition.isDefault !== undefined) { attrs.push(`DEFAULT=${rendition.isDefault ? 'YES' : 'NO'}`); } if (rendition.autoselect !== undefined) { attrs.push(`AUTOSELECT=${rendition.autoselect ? 'YES' : 'NO'}`); } if (rendition.forced !== undefined) { attrs.push(`FORCED=${rendition.forced ? 'YES' : 'NO'}`); } if (rendition.language) { attrs.push(`LANGUAGE="${rendition.language}"`); } if (rendition.assocLanguage) { attrs.push(`ASSOC-LANGUAGE="${rendition.assocLanguage}"`); } if (rendition.instreamId) { attrs.push(`INSTREAM-ID="${rendition.instreamId}"`); } if (rendition.characteristics) { attrs.push(`CHARACTERISTICS="${rendition.characteristics}"`); } if (rendition.channels) { attrs.push(`CHANNELS="${rendition.channels}"`); } if (rendition.uri) { attrs.push(`URI="${rendition.uri}"`); } return `#EXT-X-MEDIA:${attrs.join(',')}`; } function buildMediaPlaylist(lines, playlist, postProcess) { let lastKey = ''; let lastMap = ''; let unclosedCueIn = false; if (playlist.targetDuration) { lines.push(`#EXT-X-TARGETDURATION:${playlist.targetDuration}`); } if (playlist.lowLatencyCompatibility) { const { canBlockReload, canSkipUntil, holdBack, partHoldBack } = playlist.lowLatencyCompatibility; const params = []; params.push(`CAN-BLOCK-RELOAD=${canBlockReload ? 'YES' : 'NO'}`); if (canSkipUntil !== undefined) { params.push(`CAN-SKIP-UNTIL=${canSkipUntil}`); } if (holdBack !== undefined) { params.push(`HOLD-BACK=${holdBack}`); } if (partHoldBack !== undefined) { params.push(`PART-HOLD-BACK=${partHoldBack}`); } lines.push(`#EXT-X-SERVER-CONTROL:${params.join(',')}`); } if (playlist.partTargetDuration) { lines.push(`#EXT-X-PART-INF:PART-TARGET=${playlist.partTargetDuration}`); } if (playlist.mediaSequenceBase) { lines.push(`#EXT-X-MEDIA-SEQUENCE:${playlist.mediaSequenceBase}`); } if (playlist.discontinuitySequenceBase) { lines.push(`#EXT-X-DISCONTINUITY-SEQUENCE:${playlist.discontinuitySequenceBase}`); } if (playlist.playlistType) { lines.push(`#EXT-X-PLAYLIST-TYPE:${playlist.playlistType}`); } if (playlist.isIFrame) { lines.push(`#EXT-X-I-FRAMES-ONLY`); } if (playlist.skip > 0) { lines.push(`#EXT-X-SKIP:SKIPPED-SEGMENTS=${playlist.skip}`); } for (const [i, segment] of playlist.segments.entries()) { const base = lines.length; let markerType = ''; [lastKey, lastMap, markerType] = buildSegment(lines, segment, lastKey, lastMap, playlist.version); if (markerType === 'OUT') { unclosedCueIn = true; } else if (markerType === 'IN' && unclosedCueIn) { unclosedCueIn = false; } if (postProcess?.segmentProcessor) { postProcess.segmentProcessor(lines, base, lines.length - 1, segment, i); } } if (playlist.playlistType === 'VOD' && unclosedCueIn) { lines.push('#EXT-X-CUE-IN'); } if (playlist.prefetchSegments.length > 2) { utils.INVALIDPLAYLIST('The server must deliver no more than two prefetch segments'); } for (const segment of playlist.prefetchSegments) { if (segment.discontinuity) { lines.push(`#EXT-X-PREFETCH-DISCONTINUITY`); } lines.push(`#EXT-X-PREFETCH:${segment.uri}`); } if (playlist.endlist) { lines.push(`#EXT-X-ENDLIST`); } for (const report of playlist.renditionReports) { const params = []; params.push(`URI="${report.uri}"`, `LAST-MSN=${report.lastMSN}`); if (report.lastPart !== undefined) { params.push(`LAST-PART=${report.lastPart}`); } lines.push(`#EXT-X-RENDITION-REPORT:${params.join(',')}`); } } function buildSegment(lines, segment, lastKey, lastMap, version = 1) { let hint = false; let markerType = ''; if (segment.discontinuity) { lines.push(`#EXT-X-DISCONTINUITY`); } if (segment.gap) { lines.push(`#EXT-X-GAP`); } if (segment.key) { const line = buildKey(segment.key); if (line !== lastKey) { lines.push(line); lastKey = line; } } if (segment.map) { const line = buildMap(segment.map); if (line !== lastMap) { lines.push(line); lastMap = line; } } if (segment.programDateTime) { lines.push(`#EXT-X-PROGRAM-DATE-TIME:${utils.formatDate(segment.programDateTime)}`); } if (segment.dateRange) { lines.push(buildDateRange(segment.dateRange)); } if (segment.markers.length > 0) { markerType = buildMarkers(lines, segment.markers); } if (segment.parts.length > 0) { hint = buildParts(lines, segment.parts); } if (hint) { return [lastKey, lastMap]; } if (typeof segment.duration === 'number' && !Number.isNaN(segment.duration)) { const duration = version < 3 ? Math.round(segment.duration) : buildDecimalFloatingNumber(segment.duration, getNumberOfDecimalPlaces(segment.duration)); lines.push(`#EXTINF:${duration},${unescape(encodeURIComponent(segment.title || ''))}`); } if (segment.byterange) { lines.push(`#EXT-X-BYTERANGE:${buildByteRange(segment.byterange)}`); } Array.prototype.push.call(lines, `${segment.uri}`); // URIs could be redundant when EXT-X-BYTERANGE is used return [lastKey, lastMap, markerType]; } function buildMap(map) { const attrs = [`URI="${map.uri}"`]; if (map.byterange) { attrs.push(`BYTERANGE="${buildByteRange(map.byterange)}"`); } return `#EXT-X-MAP:${attrs.join(',')}`; } function buildByteRange({ offset, length }) { return `${length}@${offset}`; } function buildDateRange(dateRange) { const attrs = [ `ID="${dateRange.id}"` ]; if (dateRange.start) { attrs.push(`START-DATE="${utils.formatDate(dateRange.start)}"`); } if (dateRange.cue) { attrs.push(`CUE="${dateRange.cue}"`); } if (dateRange.end) { attrs.push(`END-DATE="${utils.formatDate(dateRange.end)}"`); } if (dateRange.duration) { attrs.push(`DURATION=${dateRange.duration}`); } if (dateRange.plannedDuration) { attrs.push(`PLANNED-DURATION=${dateRange.plannedDuration}`); } if (dateRange.classId) { attrs.push(`CLASS="${dateRange.classId}"`); } if (dateRange.endOnNext) { attrs.push(`END-ON-NEXT=YES`); } for (const key of Object.keys(dateRange.attributes)) { if (key.startsWith('X-')) { if (typeof dateRange.attributes[key] === 'number') { attrs.push(`${key}=${dateRange.attributes[key]}`); } else { attrs.push(`${key}="${dateRange.attributes[key]}"`); } } else if (key.startsWith('SCTE35-')) { attrs.push(`${key}=${utils.byteSequenceToHex(dateRange.attributes[key])}`); } } return `#EXT-X-DATERANGE:${attrs.join(',')}`; } function buildMarkers(lines, markers) { let type = ''; for (const marker of markers) { if (marker.type === 'OUT') { type = 'OUT'; lines.push(`#EXT-X-CUE-OUT:DURATION=${marker.duration}`); } else if (marker.type === 'IN') { type = 'IN'; lines.push('#EXT-X-CUE-IN'); } else if (marker.type === 'RAW') { const value = marker.value ? `:${marker.value}` : ''; lines.push(`#${marker.tagName}${value}`); } } return type; } function buildParts(lines, parts) { let hint = false; for (const part of parts) { if (part.hint) { const params = []; params.push('TYPE=PART', `URI="${part.uri}"`); if (part.byterange) { const { offset, length } = part.byterange; params.push(`BYTERANGE-START=${offset}`); if (length) { params.push(`BYTERANGE-LENGTH=${length}`); } } lines.push(`#EXT-X-PRELOAD-HINT:${params.join(',')}`); hint = true; } else { const params = []; params.push(`DURATION=${part.duration}`, `URI="${part.uri}"`); if (part.byterange) { params.push(`BYTERANGE=${buildByteRange(part.byterange)}`); } if (part.independent) { params.push('INDEPENDENT=YES'); } if (part.gap) { params.push('GAP=YES'); } lines.push(`#EXT-X-PART:${params.join(',')}`); } } return hint; } function buildDefines(define) { const attrs = []; for (const attr in define) { attrs.push(`${attr}="${define[attr]}"`); } return `#EXT-X-DEFINE:${attrs.join(',')}`; } function stringify(playlist, postProcess) { utils.PARAMCHECK(playlist); utils.ASSERT('Not a playlist', playlist.type === 'playlist'); const lines = new LineArray(playlist.uri); lines.push('#EXTM3U'); if (playlist.version) { lines.push(`#EXT-X-VERSION:${playlist.version}`); } if (playlist.independentSegments) { lines.push('#EXT-X-INDEPENDENT-SEGMENTS'); } if (playlist.start) { lines.push(`#EXT-X-START:TIME-OFFSET=${buildDecimalFloatingNumber(playlist.start.offset)}${playlist.start.precise ? ',PRECISE=YES' : ''}`); } if (playlist.defines) { for (const session of playlist.defines) { lines.push(buildDefines(session)); } } if (playlist.isMasterPlaylist) { buildMasterPlaylist(lines, playlist, postProcess); } else { buildMediaPlaylist(lines, playlist, postProcess); } // console.log('<<<'); // console.log(lines.join('\n')); // console.log('>>>'); return lines.join('\n'); } exports.default = stringify;