npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

m3u8-composer

v0.1.3

Published

Flexible library for working with m3u8 and HLS playlists (parsing, validation and other stuff)

Downloads

1

Readme

M3U8 Composer

Flexible library for working with m3u8 and HLS playlists (parsing, validation and other stuff).

This library is:

  • Composable. You can build your own parser from small modules to selectively parse and/or validate only the necessary tags and attributes, increasing performance.

  • Tree-shakable. Take from the library only what you need for work to reduce bundle size.

  • Scalable. Convenient creation of non-standard tags and attributes.

  • Informative. Detailed logging system with descriptions for deep analysis.

Fast Example (Parsing and Validation)

const { interpreter } = require('m3u8-composer/interpreter/m3u8-to-schema');
const { createSchema } = require('m3u8-composer/schema/rfc8216-v13');

const data = `
#EXTM3U
#EXT-X-VERSION:1

# variants
#EXT-X-STREAM-INF:BANDWIDTH=493000,CODECS="mp4a.40.2,avc1.66.30",RESOLUTION=224x100,FRAME-RATE=24
low.m3u8

#EXT-X-STREAM-INF:BANDWIDTH=932000,CODECS="avc1.64002a",RESOLUTION=448x200,FRAME-RATE=24
medium.m3u8`;

const schema = createSchema();

interpreter(data, schema);


console.log(schema);

/*
{
  ...<system data>

  playlist: {
    '#EXTM3U': true,
    '#EXT-X-VERSION': '1'
  },
  mediaSegments: [],
  variantStreams: [
    {
      '#EXT-X-STREAM-INF': {
        BANDWIDTH: '493000',
        CODECS: [ 'mp4a.40.2', 'avc1.66.30' ],
        RESOLUTION: '224x100',
        'FRAME-RATE': '24'
      },
      URI: 'low.m3u8'
    },
    {
      '#EXT-X-STREAM-INF': {
        BANDWIDTH: '932000',
        CODECS:  [ 'avc1.64002a' ],
        RESOLUTION: '448x200',
        'FRAME-RATE': '24'
      },
      URI: 'medium.m3u8'
    }
  ],
  metadata: {
    multivariantPlaylist: true,
  },
  logs: {
    '0x1001': {
      value: true,
      row: 1,
      col: 0,
      isValidated: true,
    },
  },
  validate: [Function (anonymous)],
}
*/

Guideline


Supported Tags and Attributes

Click to open detailed list:

Installation

npm install m3u8-composer

Creating your own parser

Let's try to parse and validate Media Playlist according to the rfc8216 v13 specification rules.

For parsing we need a Schema. In the example above, we used ready-made Schema (it includes all tags and validators), but now let's build our own one to understand how it works:

const { interpreter } = require('m3u8-composer/interpreter/m3u8-to-schema');

// parser tags
const { ParserSchema } = require('m3u8-composer/tags/rfc8216/v13/schema');

const { EXTM3U } = require('m3u8-composer/tags/rfc8216/v13/EXTM3U');
const { EXT_X_MEDIA_SEQUENCE } = require('m3u8-composer/tags/rfc8216/v13/EXT-X-MEDIA-SEQUENCE');
const { EXT_X_PLAYLIST_TYPE } = require('m3u8-composer/tags/rfc8216/v13/EXT-X-PLAYLIST-TYPE');
const { EXT_X_MAP } = require('m3u8-composer/tags/rfc8216/v13/EXT-X-MAP');
const { URI } = require('m3u8-composer/tags/rfc8216/v13/EXT-X-MAP/attributes/URI');
const { BYTERANGE } = require('m3u8-composer/tags/rfc8216/v13/EXT-X-MAP/attributes/BYTERANGE');
const { EXT_X_BYTERANGE } = require('m3u8-composer/tags/rfc8216/v13/EXT-X-BYTERANGE');
const { EXTINF } = require('m3u8-composer/tags/rfc8216/v13/EXTINF');

// validations
const { ValidationSchema } = require('m3u8-composer/tags-validation/rfc8216/v13/schema');
const { EXT_X_PLAYLIST_TYPE_V13 } = require('m3u8-composer/tags-validation/rfc8216/v13/EXT-X-PLAYLIST-TYPE');


// build schema

const schema = ValidationSchema(ParserSchema({
  ...EXTM3U(),
  ...EXT_X_MEDIA_SEQUENCE(),
  ...EXT_X_PLAYLIST_TYPE_V13(EXT_X_PLAYLIST_TYPE()), // we will validate only this tag
  ...EXT_X_MAP({
    ...URI(),
    ...BYTERANGE(),
  }),
  ...EXT_X_BYTERANGE(),
  ...EXTINF(),
}));

// playlist as a string

const data = `#EXTM3U
#EXT-X-TARGETDURATION:6
#EXT-X-VERSION:7
#EXT-X-MEDIA-SEQUENCE:1
#EXT-X-PLAYLIST-TYPE:BAD_VALUE
#EXT-X-MAP:URI="main.mp4",BYTERANGE="719@0"
#EXTINF:6.00000,
#EXT-X-BYTERANGE:1508000@719
main.mp4
#EXTINF:6.00000,
#EXT-X-BYTERANGE:1510244@1508719
main.mp4
#EXTINF:6.00000,test_name
#EXT-X-BYTERANGE:1504861@3018963
main.mp4`;

// interpretation..

interpreter(data, schema);

// after that, the results will appear in the Schema object

Schema

Playlist

A "playlist" object stores general tags, that don't belong to Media Segments or Variant Stream. Only those tags that are in the Schema are recorded:

console.log(schema.playlist);

/*
{
  '#EXTM3U': true,
  '#EXT-X-MEDIA-SEQUENCE': '1',
  '#EXT-X-PLAYLIST-TYPE': 'BAD_VALUE'
}
*/

Media Segments

Media Segments are written to the "mediaSegments" array of the Schema in the order in which they are declared in the playlist. All tags and their attributes from the Schema are added to each Media Segment according to the specification (with URI):

console.log(schema.mediaSegments);

/*
[                                                             
  {                                                                 
    '#EXTINF': { duration: '6.00000', title: '' },                  
    '#EXT-X-BYTERANGE': '1508000@719',                              
    '#EXT-X-MAP': { URI: 'main.mp4', BYTERANGE: '719@0' },          
    URI: 'main.mp4'                                                 
  },                                                                
  {                                                                 
    '#EXTINF': { duration: '6.00000', title: '' },                  
    '#EXT-X-BYTERANGE': '1510244@1508719',                          
    '#EXT-X-MAP': { URI: 'main.mp4', BYTERANGE: '719@0' },          
    URI: 'main.mp4'                                                 
  },                                                                
  {                                                                 
    '#EXTINF': { duration: '6.00000', title: 'test_name' },         
    '#EXT-X-BYTERANGE': '1504861@3018963',                          
    '#EXT-X-MAP': { URI: 'main.mp4', BYTERANGE: '719@0' },          
    URI: 'main.mp4'                                                 
  }                                                                 
]                                                                   
*/

Variant Streams

Variant Streams are written to the "variantStreams" array, but it is empty now because we didn't parse any Multivariant Playlist tags:

console.log(schema.variantStreams);

// []

Logs

A "logs" object contains records of all events that occurred during the interpretation process. Let's look at the result:

console.log(schema.logs);

/*
{
  '0x1100': {
    row: 0,
    col: 0,
    value: null,
    isValidated: true
  },
  '0x1181': {
    value: 'BAD_VALUE',
    row: 4,
    col: 0,
    isValidated: true
  }
}
*/

In this case we recorded two events (All event codes can be found here):

0x1100: indicates that the REQUIRED tag #EXT-X-TARGETDURATION is missing because we didn't add it to the Schema. That is, in this case it is our fault.

0x1181: Here we see that in the 4-th row (numbering starts from zero) of the playlist an invalid value "BAD_VALUE" was added in the #EXT-X-PLAYLIST-TYPE tag. According to the specification, this tag only accepts "EVENT" or "VOD".

Extended Logs

In the features folder you can find a function that adds an extended description of error codes in the logs object:

const { extendedLogsFeature } = require('m3u8-composer/features/extended-logs');

...

console.log(extendedLogsFeature(schema));

/*
{
  logs: {
    '0x1100': {
      relatesTo: '#EXT-X-TARGETDURATION',
      keyword: 'REQUIRED',
      message: 'The EXT-X-TARGETDURATION tag is REQUIRED',
      origin: 'rfc8216 v13',
      row: 0,
      col: 0,
      value: null,
      isValidated: true
    },
    '0x1181': {
      relatesTo: '#EXT-X-PLAYLIST-TYPE',
      keyword: 'MUST NOT',
      message: 'where type-enum is either EVENT or VOD',
      origin: 'rfc8216 v13',
      value: 'BAD_VALUE',
      row: 4,
      col: 0,
      isValidated: true
    }
}
*/

Be careful: error descriptions increase the size of your application bundle. It is not recommended to use this in production builds.

Custom tag and attributes creation

Let's create a custom tag #EXT-X-IMAGE-STREAM-INF as an example for Trick Play support, with additional URI and BANDWIDTH attributes:

const { interpreter } = require('m3u8-composer/interpreter/m3u8-to-schema');
const { ParserSchema } = require('m3u8-composer/tags/rfc8216/v13/schema');

const { tagAttributeList } = require('m3u8-composer/parser/node/tag-attribute-list');
const { attribute } = require('m3u8-composer/parser/node/attribute');

const EXT_X_IMAGE_STREAM_INF = (attributes) => {
  return tagAttributeList('#EXT-X-IMAGE-STREAM-INF', attributes);
};

const URI = () => {
  return attribute('URI');
};

const BANDWIDTH = () => {
  return attribute('BANDWIDTH');
};

// now let's build the Schema

const schema = ParserSchema({
  ...EXT_X_IMAGE_STREAM_INF({
    ...URI(),
    ...BANDWIDTH(),
  })
});

// ok! Now we can parse a playlist with the tag "#EXT-X-IMAGE-STREAM-INF"

const data = `#EXT-X-IMAGE-STREAM-INF:URI="test.m3u8",BANDWIDTH=29729`;

interpreter(data, schema);

console.log(schema.playlist);

/*
{
  '#EXT-X-IMAGE-STREAM-INF': {
    URI: '"test.m3u8"',
    BANDWIDTH: '29729'
  }
}
*/

We used node entities (tagAttributeList and attribute) from the system parser. This means that we set a specific algorithms for parsing tags and attributes for the system parser. Here you will find a list of all algorithms with descriptions.

Now let's look at the result of the URI attribute:

{ URI: '"test.m3u8"' }

Let's say we want to write values as quoted-string from the rfc8216 v13. Let's use ready-made tools for this and rewrite the URI attribute code:

const { quotedString } = require('m3u8-composer/tags/rfc8216/v13/common/quoted-string');

const URI = () => {
  const attributeNode = attribute('URI');

  attributeNode['URI'].parse = (value) => {
    return quotedString(value);
  };

  return attributeNode;
};

That is, we have overridden the parse method of the URI attribute, and the result will now look like:

{ URI: 'test.m3u8' }

Any tag or attribute must have a parse method, it is called by the parser. The method takes the original value as input and returns the processed one.

Custom validation

Let's now create our own validator for the URI attribute from the example above. For example, if the value does not contain the substring ".m3u8", then we will display a warning in the console:

const URI_VALIDATION = (tag) => {
    tag['URI'].validate = (schema, data, dataAll) => {
      if (data.value.indexOf('.m3u8') === -1) {
        console.warn('unsupported playlist!');
      }
    };

    return tag;
};

Here we have overridden the validate method. Every tag and attribute also has this method, it is empty by default. It takes 3 arguments:

|Argument|Type|Description| |---|---|---| |schema|object|Entire Schema object| |data|object|Raw data from the parser { value: string, row: number, col: number, isValidated: boolean, ...attributes }, where "attributes" are nested objects in the same format| |dataAll|array / object|Set of "data", described above|

Now let's add the validator to the Schema as a decorator, wrapping a URI:

const schema = ParserSchema({
  ...EXT_X_IMAGE_STREAM_INF({
    ...URI_VALIDATION(URI()), // validate only the URI attribute
    ...BANDWIDTH(),
  })
});

interpreter(data, schema);

...

You can also reuse default AttributeValue validators like decimal-integer and all the others:

import { decimalIntegerV13 } from 'm3u8-composer/tags-validation/rfc8216/v13/common/decimal-integer';

const URI_VALIDATION = (tag) => {
    tag['URI'].validate = (schema, data) => {
      decimalIntegerV13(schema, data);
    };

    return tag;
};

In this case we will get 0x0000 error in the schema.logs because URI value is a string.

Schema tag modifying

A schema is a javascript object, so:

// you can delete a tag by key
delete(schema['#EXTINF']);

// you can remove the tag attribute
delete(schema['#EXT-X-PART']['DURATION']);

// you can override validation for a specific tag
schema['#EXTM3U'].validate = () => {};

// and so on..

Parser algorithms

tag

||| |---|---| |Description:|Parses tags without a value or with a single value after the : character| |Examples:|#EXT3MU#EXT-X-VERSION:9| |Results:| { '#EXT3MU': true }{ '#EXT-X-VERSION': '9' } |

For tags without a value the result is true if the tag is in the playlist. If the tag is not in the playlist, the value is not recorded

If a tag appears twice in a playlist, the value of the second will overwrite the value of the first

tagAttributeList

||| |---|---| |Description:|Parses tags with attributes| |Example:|#EXT-X-MAP:URI="main.mp4",BYTERANGE="560@0"| |Result:| { '#EXT-X-MAP': { URI: '"main.mp4"', BYTERANGE: '"560@0"' } } |

The result is an object whose keys are AttributeName and whose values are AttributeValue.

If a tag appears twice in a playlist, the value of the second will overwrite the value of the first. If the tag is not in the playlist, the value is not recorded.

tagAttributeListMultiple

||| |---|---| |Description:|Same as tagAttributeList, but the result is an array | |Example:|#EXT-X-DEFINE:NAME="test",VALUE="value"#EXT-X-DEFINE:NAME="test2",VALUE="value2"| |Result:| { '#EXT-X-DEFINE': [{ NAME: '"test"', VALUE: '"value"' }, { NAME: '"test2"', VALUE: '"value2"' }] } |

the values are simply added to the array. If the tag is not in the playlist, the value is not recorded.

attribute

||| |---|---| |Описание:|Attribute| |Пример:|#EXT-X-START:TIME-OFFSET=25| |Результат:| { '#EXT-X-START': { TIME-OFFSET: 25 } } |

The attribute in the example above is TIME-OFFSET. If a tag in the playlist does not have an attribute, the attribute value is not recorded.

Media Type

Each tag additionally has a mediaType which defines the algorithm of parsing according to URI logic from the specification (Media Segments / Variant Streams):

|Name|Description| |---|---| |none-media-type-id|Media type missing (by default)| |media-segment-apply-next-media-type-id|The tag is applied to the next URI, the result is written to mediaSegments| |media-segment-apply-all-media-type-id|The tag applies to all URIs, the result is written to mediaSegments| |variant-stream-apply-next-media-type-id|The tag is applied to the next URI, the result is written to variantStreams|

Here's an example of how to pass this ID to a tag:

const { MEDIA_SEGMENT_APPLY_NEXT_MEDIA_TYPE_ID } = require('m3u8-composer/parser/const');
const { tag } = require('m3u8-composer/parser/node/tag');
const { EXT_X_BYTERANGE_ID } = require('m3u8-composer/tags/const');

const EXT_X_BYTERANGE = () => {
    return tag(
        EXT_X_BYTERANGE_ID,
        undefined,
        MEDIA_SEGMENT_APPLY_NEXT_MEDIA_TYPE_ID,
    );
};

Features

You can perform additional actions on the final playlist:

|name|description| |---|---| |extendedLogsFeature|Adds additional information to event codes in schema.logs|

[WIP] Additional features will be added in the future, such as Delta Updates, Variable Substitution.

Playlist creation from the schema

[WIP] In the future, it will be possible to create a playlist from a schema.

Tags versioning

[WIP] All tags and their validations will be versioned and placed in the corresponding folders, as is now done for rfc8216 v13. In the future, it is planned to collect a huge database of tags, including non-standard tags.