UNPKG

m3u8parse

Version:

Structural parsing of Apple HTTP Live Streaming .m3u8 format

176 lines (175 loc) 6.15 kB
import { AttrList } from './attrlist.js'; import { isStringish } from './playlist-base.js'; import { MediaPlaylist } from './playlist.js'; const stringifyAttrs = function (attrs) { if (attrs === undefined || typeof attrs !== 'object') { return undefined; } if (!(attrs instanceof AttrList)) { attrs = new AttrList(attrs); } return attrs.size > 0 ? attrs.toString() : undefined; }; const streamInfAttrs = function (obj, version) { const attrs = new AttrList(obj); if (version >= 6) { attrs.delete('program-id'); } return attrs.toString(); }; export class PlaylistWriter { constructor(playlist) { this.playlist = playlist; } compile(pusher) { this._push = pusher; try { const { playlist } = this; this._writeShared(playlist); if (playlist.master) { this._writeMain(playlist); } else { this._writeMedia(playlist); } this._push(''); } finally { this._push = PlaylistWriter.prototype._push; } } toString() { const list = []; this.compile((...lines) => list.push(...lines)); return list.join('\n'); } _writeShared(playlist) { this._push('#EXTM3U'); if (playlist.version > 1) { this._ext('VERSION', playlist.version); } for (const key of playlist.defines) { this._ext('DEFINE', stringifyAttrs(key)); } this._ext('START', stringifyAttrs(playlist.start)); this._ext('INDEPENDENT-SEGMENTS', !!playlist.independent_segments); if (playlist.vendor) { // Add vendor extensions for (const [ext, value] of playlist.vendor) { let line = ext; if (isStringish(value)) { line += ':' + value; } this._push(line); } } } _writeMain(playlist) { for (const key of playlist.session_keys) { this._ext('SESSION-KEY', stringifyAttrs(key)); } for (const list of playlist.data.values()) { for (const data of list) { this._ext('SESSION-DATA', stringifyAttrs(data)); } } for (const list of playlist.groups.values()) { for (const group of list) { this._ext('MEDIA', stringifyAttrs(group)); } } for (const iframe of playlist.iframes) { this._ext('I-FRAME-STREAM-INF', streamInfAttrs(iframe)); } for (const variant of playlist.variants) { if (variant.info) { this._ext('STREAM-INF', streamInfAttrs(variant.info)); } this._push(variant.uri); } } _writeMedia(playlist) { this._ext('TARGETDURATION', playlist.target_duration); this._ext('PLAYLIST-TYPE', playlist.type); this._ext('SERVER-CONTROL', stringifyAttrs(playlist.server_control)); this._ext('PART-INF', stringifyAttrs(playlist.part_info)); const mediaSequence = parseInt(playlist.media_sequence, 10) || 0; if (mediaSequence !== 0) { this._ext('MEDIA-SEQUENCE', mediaSequence); } if (playlist.type !== MediaPlaylist.Type.VOD && playlist.type !== MediaPlaylist.Type.EVENT) { this._ext('DISCONTINUITY-SEQUENCE', parseInt(playlist.discontinuity_sequence, 10)); } if (playlist.version >= 4) { this._ext('I-FRAMES-ONLY', !!playlist.i_frames_only); } const meta = playlist.meta || {}; this._ext('SKIP', stringifyAttrs(meta.skip)); for (const segment of playlist.segments) { this._writeSegment(segment); } for (const [ext, entry] of MediaPlaylist._metas.entries()) { for (const key of meta[entry] || []) { this._ext(ext, stringifyAttrs(key)); } } this._ext('ENDLIST', !!playlist.ended); } _writeSegment(segment) { this._ext('DISCONTINUITY', !!segment.discontinuity); if (segment.program_time) { const program_time = segment.program_time.toISOString ? segment.program_time.toISOString() : segment.program_time; this._ext('PROGRAM-DATE-TIME', program_time); } if (segment.keys) { for (const key of segment.keys) { this._ext('KEY', stringifyAttrs(key)); } } if (segment.bitrate !== undefined) { this._ext('BITRATE', segment.bitrate); } this._ext('MAP', stringifyAttrs(segment.map)); if (segment.byterange?.length || segment.byterange?.length === 0) { let range = '' + segment.byterange.length; if (segment.byterange.offset || segment.byterange.offset === 0) { range += '@' + segment.byterange.offset; } this._ext('BYTERANGE', range); } if (segment.vendor) { // Add vendor extensions for (const [ext, value] of segment.vendor) { let line = ext; if (isStringish(value)) { line += ':' + value; } this._push(line); } } for (const part of segment.parts ?? []) { this._ext('PART', stringifyAttrs(part)); } if (segment.uri && segment.duration !== undefined) { this._push(`#EXTINF:${parseFloat(segment.duration.toFixed(5))},${segment.title}`); this._ext('GAP', !!segment.gap); this._push(segment.uri); } } _push(...lines) { throw new Error('No compiler'); } _ext(ext, value) { if (value === undefined || value === false || (typeof value === 'number' && isNaN(value))) { return; } if (value === true) { this._push('#EXT-X-' + ext); } else { this._push(`#EXT-X-${ext}:${value}`); } } }