@nolawnchairs/m3u8
v1.6.0
Published
Library for parsing, manipulating and producing ad-hoc m3u8 files for HLS
Readme
Lib M3U8
A library to parse and create variants of M3u8 manifest files.
Installation
npm install @nolawnchairs/m3u8
Note: This is a private NPM library used internally. No guarantees are made about the stability or quality of the library.
Usage
Instantiate either a master or media manifest file with the string contents of a valid m3u8 file.
Loading a master (variant manifest)
const contents = await fs.readFile('master.m3u8', { encoding: 'utf-8' })
const masterM3u8 = new MasterM3u8(contents)Obtaining variants
Access the variants from a master manifest. The variants accessor returns a string array containing the sources for all variants referenced in the manifest.
const variants = masterM3u8.variantsLoading a variant
const contents = await fs.readFile(variants[0], { encoding: 'utf-8' })
const variantM3u8 = new MediaM3u8(contents)Generating slices
Any MediaM3u8 variant can be sliced into smaller parts, which can be marshaled into either EVENT or VOD formats.
Create a new instance of M3u8Slicer, providing the source MediaM3u8 instance, a TargetResolver and an optional timing ratio.
The TargetResolver is a class that takes two functional operators in its constructor that will be used to resolve the actual URL to the encryption key, and that of the actual media source, respectively.
const resolver = new TargetResolver(
value => value.replace('encryption.key', '/path/to/this/encryption.key'),
value => `https://example.com/${value}`
)
const slicer = new M3u8Slicer(variantM3u8, resolver)Create a live slice, which will produce a manifest with the #EXT-X-PLAYLIST-TYPE as EVENT, and without the #EXT-X-ENDLIST line at the end. The method takes three parameters:
- The numeric value to set to the
#EXT-X-MEDIA-SEQUENCEtag - The starting segment index of the slice
- The number of segments to include
// The first 5 segments
const slice1 = slicer.toLiveSlice(0, 0, 5)
// The next 5 segments
const slice2 = slicer.toLiveSlice(1, 4, 5)Create a VOD slice, which will produce a manifest with the #EXT-X-PLAYLIST-TYPE as VOD, and includes the #EXT-X-ENDLIST line at the end. The method takes two parameters:
- The starting segment index of the slice
- The number of segments to include
The VOD slice will always include the
#EXT-X-MEDIA-SEQUENCEtag with the value of0.
// Make a slice from the 10th segment with a length of 10 segments
const vodSlice = slicer.toVodSlice(9, 10)Create a live transition slice, which will produce a manifest with the #EXT-X-PLAYLIST-TYPE as EVENT, and without the #EXT-X-ENDLIST line at the end, but will include part of a different manifest using the same indexing. The method takes three parameters:
- The numeric value to set to the
#EXT-X-MEDIA-SEQUENCEtag - The starting segment index of the slice
- The
M3u8Slicerinstance of the slice to which the transition will be made
Note: This method assumes a slice of length
3, since this is a library specific to the Montage platform
const nextSlicer = new M3u8Slicer(otherM3u8, resolver)
const transition = slice.toLiveTransitionSlice(0, 0, nextSlicer)The M3u8Slicer class can create a slice representation of the entire manifest with the TargetResolver applied to all segments. The method takes no parameters.
const slicer = new M3u8Slicer(variantM3u8, resolver)
const slice = slicer.toResolvedSlice()The M3u8Slicer class can also be used to create a slice representation of the entire manifest with all properties left intact. The method takes no parameters.
const clone = slicer.toCloneSlice()Note this method will NOT run the
TargetResolveron the slice.
Slices
The M3u8Slice objects include the following properties and methods:
Properties:
meta-IM3u8Line[]- The metadata for this created manifest slicesegments-IM3u8MediaSegment[]- The media segmentsoffsetMillis-number- The start offset, in milliseconds, that represents the time offset since the start of the media, based on the#EXT-X-TARGETDURATIONtag's valueoffsetSeconds-number- The start offset, in seconds, floored to an integermediaExhausted-boolean- Whether the slice was created at the end of a manifest file
Methods:
marshal- The method that converts the slice to an m3u8-formatted stringappendDiscontinuity- appends another slice to this slice, adding an#EXT-X-DISCONTINUITYtag to the beginning of the appended slice.insertMeta- inserts a meta element into the manifestmodifyMeta- modifies the value to a specific meta element from the manifestomitMeta- removes a specific meta element from the manifestmodifyEachSegment- apply a modifier function to each segment in the manifestmodifySegmentMeta- apply a modifier function to each segment's metadataomitSegmentMeta- remove a specific element from each segment's metadata
Note: appending a discontinuity will mutate the slice to which it's applied. The
insert.modifyandomitmethods will immutably create a newM3u8Sliceinstance.
const slicer1 = new M3u8Slicer(firstM3u8)
const slice1 = slicer.toLiveSlice(0, 0, 10)
const slicer2 = new M3u8Slicer(secondM3u8)
const slice2 = slicer.toLiveSlice(0, 0, 5)
// Append the 5 segments from the second slice into the first
slice1.appendDiscontinuity(slice2)
// Marshal the result
const composite = slice1.marshal()Note: Appending a discontinuity will work for both live and VOD slices.
Modifying slices
The M3u8Slice class also includes methods that can be used to generate slices with modified metadata and segments.
These methods all return a new M3u8Slice instance, and do not mutate the original slice.
Inserting meta
const slice = slicer.toCloneSlice()
const modified = slice.insertMeta(M3u8Tag.EXT_X_PROGRAM_DATE_TIME, new Date().toISOString())
const marshaled = modified.marshal()Modifying meta
const slice = slicer.toCloneSlice()
const modified = slice.modifyMeta(M3u8Tag.EXT_X_PROGRAM_DATE_TIME, new Date().toISOString())
const marshaled = modified.marshal()Omitting meta
const slice = slicer.toCloneSlice()
const modified = slice.omitMeta(M3u8Tag.EXT_X_PROGRAM_DATE_TIME)
const marshaled = modified.marshal()Since all slice modifier methods return a new M3u8Slice instance, they can be chained together.
const slice = slicer.toCloneSlice()
.insertMeta(M3u8Tag.EXT_X_PROGRAM_DATE_TIME, new Date().toISOString())
.modifyMeta(M3u8Tag.EXT_X_PROGRAM_DATE_TIME, new Date().toISOString())
.omitMeta(M3u8Tag.EXT_X_PROGRAM_DATE_TIME)Modifying each segment
const slice = slicer.toCloneSlice()
.modifyEachSegment(({ source, ...rest }) => ({
...rest,
source: `https://example.com/${source}`
}))Modifying segment meta
const slice = slicer.toCloneSlice()
const modified = slice.modifySegmentMeta(M3u8Tag.EXT_X_KEY,
({ value }) => value.replace('encryption.key', 'https://example.com/keys/encryption.key'))Omitting segment meta
const slice = slicer.toCloneSlice().omitSegmentMeta(M3u8Tag.EXT_X_KEY)Full Example
const contents = await fs.readFile('master.m3u8', { encoding: 'utf-8' })
const masterM3u8 = new MasterM3u8(contents)
const resolver = new TargetResolver(
value => value.replace('encryption.key', '/path/to/this/encryption.key'),
value => `https://example.com/${value}`
)
for (const variant of masterM3u8.variants) {
const variantContents = await fs.readFile(variant, { encoding: 'utf-8' })
const variantM3u8 = new MediaM3u8(contents)
const slicer = new M3u8Slicer(variantM3u8, resolver)
const results = []
let i = 0
for (const segment of variantM3u8.segments) {
const slice = slicer.toLiveSlice(i, i, 3)
results.push(slice.marshal())
}
}
