├── .editorconfig
├── .github
└── workflows
│ ├── ci.yml
│ └── github-release.yml
├── .gitignore
├── .npmignore
├── .nvmrc
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── bin
└── parse.js
├── index.html
├── package-lock.json
├── package.json
├── scripts
├── karma.conf.js
├── netlify.js
└── rollup.config.js
├── src
├── errors.js
├── index.js
├── inheritAttributes.js
├── parseAttributes.js
├── parseUTCTimingScheme.js
├── playlist-merge.js
├── segment
│ ├── durationTimeParser.js
│ ├── segmentBase.js
│ ├── segmentList.js
│ ├── segmentTemplate.js
│ ├── timelineTimeParser.js
│ └── urlType.js
├── stringToMpdXml.js
├── toM3u8.js
├── toPlaylists.js
└── utils
│ ├── list.js
│ ├── object.js
│ ├── string.js
│ ├── time.js
│ └── xml.js
└── test
├── index.test.js
├── inheritAttributes.test.js
├── manifests
├── 608-captions.js
├── 608-captions.mpd
├── 708-captions.js
├── 708-captions.mpd
├── audio-only.js
├── audio-only.mpd
├── location.js
├── location.mpd
├── locations.js
├── locations.mpd
├── maat_vtt_segmentTemplate.js
├── maat_vtt_segmentTemplate.mpd
├── multiperiod-dynamic.js
├── multiperiod-dynamic.mpd
├── multiperiod-segment-list.js
├── multiperiod-segment-list.mpd
├── multiperiod-segment-template.js
├── multiperiod-segment-template.mpd
├── multiperiod-startnumber-removed-periods.js
├── multiperiod-startnumber-removed-periods.mpd
├── multiperiod-startnumber.js
├── multiperiod-startnumber.mpd
├── multiperiod.js
├── multiperiod.mpd
├── segmentBase.js
├── segmentBase.mpd
├── segmentList.js
├── segmentList.mpd
├── vtt_codecs.js
├── vtt_codecs.mpd
├── webmsegments.js
└── webmsegments.mpd
├── parseAttributes.test.js
├── playlist-merge.test.js
├── segment
├── durationTimeParser.test.js
├── segmentBase.test.js
├── segmentList.test.js
├── segmentTemplate.test.js
└── urlType.test.js
├── stringToMpdXml.test.js
├── toM3u8.test.js
├── toPlaylists.test.js
└── utils.test.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_style = space
8 | indent_size = 2
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | should-skip:
7 | continue-on-error: true
8 | runs-on: ubuntu-latest
9 | # Map a step output to a job output
10 | outputs:
11 | should-skip-job: ${{steps.skip-check.outputs.should_skip}}
12 | steps:
13 | - id: skip-check
14 | uses: fkirc/skip-duplicate-actions@v5.3.0
15 | with:
16 | github_token: ${{github.token}}
17 |
18 | ci:
19 | needs: should-skip
20 | if: ${{needs.should-skip.outputs.should-skip-job != 'true' || github.ref == 'refs/heads/main'}}
21 | strategy:
22 | fail-fast: false
23 | matrix:
24 | os: [ubuntu-latest]
25 | test-type: ['unit', 'coverage']
26 | env:
27 | BROWSER_STACK_USERNAME: ${{secrets.BROWSER_STACK_USERNAME}}
28 | BROWSER_STACK_ACCESS_KEY: ${{secrets.BROWSER_STACK_ACCESS_KEY}}
29 | CI_TEST_TYPE: ${{matrix.test-type}}
30 | runs-on: ${{matrix.os}}
31 | steps:
32 | - name: checkout code
33 | uses: actions/checkout@v3
34 |
35 | - name: read node version from .nvmrc
36 | run: echo "NVMRC=$(cat .nvmrc)" >> $GITHUB_OUTPUT
37 | shell: bash
38 | id: nvm
39 |
40 | - name: update apt cache on linux w/o browserstack
41 | run: sudo apt-get update
42 |
43 | - name: install ffmpeg/pulseaudio for firefox on linux w/o browserstack
44 | run: sudo apt-get install ffmpeg pulseaudio
45 |
46 | - name: start pulseaudio for firefox on linux w/o browserstack
47 | run: pulseaudio -D
48 |
49 | - name: setup node
50 | uses: actions/setup-node@v3
51 | with:
52 | node-version: '${{steps.nvm.outputs.NVMRC}}'
53 | cache: npm
54 |
55 | # turn off the default setup-node problem watchers...
56 | - run: echo "::remove-matcher owner=eslint-compact::"
57 | - run: echo "::remove-matcher owner=eslint-stylish::"
58 | - run: echo "::remove-matcher owner=tsc::"
59 |
60 | - name: npm install
61 | run: npm i --prefer-offline --no-audit
62 |
63 | - name: run npm test
64 | uses: coactions/setup-xvfb@v1
65 | with:
66 | run: npm run test
67 |
68 | - name: coverage
69 | uses: codecov/codecov-action@v3
70 | with:
71 | token: ${{secrets.CODECOV_TOKEN}}
72 | files: './test/dist/coverage/coverage-final.json'
73 | fail_ci_if_error: true
74 | if: ${{startsWith(env.CI_TEST_TYPE, 'coverage')}}
75 |
--------------------------------------------------------------------------------
/.github/workflows/github-release.yml:
--------------------------------------------------------------------------------
1 | name: github-release
2 | on:
3 | push:
4 | tags:
5 | # match semver versions
6 | - "v[0-9]+.[0-9]+.[0-9]+"
7 | # match semver pre-releases
8 | - "v[0-9]+.[0-9]+.[0-9]+-*"
9 | jobs:
10 | github-release:
11 | env:
12 | # Set this if you want a netlify example
13 | NETLIFY_BASE: ''
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v3
18 | # We neeed to fetch the entire history as conventional-changelog needs
19 | # access to any number of git commits to build the changelog.
20 | with:
21 | fetch-depth: 0
22 |
23 | - name: read node version from .nvmrc
24 | run: echo "NVMRC=$(cat .nvmrc)" >> $GITHUB_OUTPUT
25 | shell: bash
26 | id: nvm
27 |
28 | - name: setup node
29 | uses: actions/setup-node@v3
30 | with:
31 | node-version: '${{steps.nvm.outputs.NVMRC}}'
32 | cache: npm
33 |
34 | - name: npm install
35 | run: npm i --prefer-offline --no-audit
36 |
37 | - name: build
38 | run: npm run build-prod || npm run build --if-present
39 |
40 | - name: Check if this is a pre-release
41 | run: echo "IS_PRE_RELEASE=$(npx -p not-prerelease is-prerelease && echo "true" || echo "false")" >> $GITHUB_OUTPUT
42 | id: pre-release-check
43 |
44 | - name: truncate CHANGELOG.md so that we can get the current versions changes only
45 | run: truncate -s 0 CHANGELOG.md
46 |
47 | - name: Generate current release changelog
48 | run: npx -p @videojs/update-changelog vjs-update-changelog --run-on-prerelease
49 |
50 | - name: get dashed package version for netlify
51 | run: echo "VERSION=$(node -e "process.stdout.write(require('./package.json').version.split('.').join('-'))")" >> $GITHUB_OUTPUT
52 | id: get-version
53 | shell: bash
54 | if: env.NETLIFY_BASE != ''
55 |
56 | - name: add netlify preview to release notes
57 | run: |
58 | echo "" >> CHANGELOG.md
59 | echo "[netlify preview for this version](https://v${{steps.get-version.outputs.VERSION}}--${{env.NETLIFY_BASE}})" >> CHANGELOG.md
60 | if: env.NETLIFY_BASE != ''
61 |
62 | - name: Create Github release
63 | uses: softprops/action-gh-release@v1
64 | with:
65 | body_path: CHANGELOG.md
66 | token: ${{github.token}}
67 | prerelease: ${{steps.pre-release-check.outputs.IS_PRE_RELEASE}}
68 | files: |
69 | dist/**/*.js
70 | dist/**/*.css
71 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS
2 | Thumbs.db
3 | ehthumbs.db
4 | Desktop.ini
5 | .DS_Store
6 | ._*
7 |
8 | # Editors
9 | *~
10 | *.swp
11 | *.tmproj
12 | *.tmproject
13 | *.sublime-*
14 | .idea/
15 | .project/
16 | .settings/
17 | .vscode/
18 |
19 | # Logs
20 | logs
21 | *.log
22 | npm-debug.log*
23 |
24 | # Dependency directories
25 | bower_components/
26 | node_modules/
27 |
28 | # Build-related directories
29 | dist/
30 | docs/api/
31 | test/dist/
32 | deploy/
33 | .eslintcache
34 | .yo-rc.json
35 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Intentionally left blank, so that npm does not ignore anything by default,
2 | # but relies on the package.json "files" array to explicitly define what ends
3 | # up in the package.
4 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 10
2 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # CONTRIBUTING
2 |
3 | We welcome contributions from everyone!
4 |
5 | ## Getting Started
6 |
7 | Make sure you have NodeJS 4.0 or higher and npm installed.
8 |
9 | 1. Fork this repository and clone your fork
10 | 1. Install dependencies: `npm install`
11 | 1. Run a development server: `npm start`
12 |
13 | ### Making Changes
14 |
15 | Refer to the [video.js plugin conventions][conventions] for more detail on best practices and tooling for video.js plugin authorship.
16 |
17 | When you've made your changes, push your commit(s) to your fork and issue a pull request against the original repository.
18 |
19 | #### Updating test expecations
20 |
21 | You can regenerate the JS manifest files by running `mpd-to-m3u8-json` binary (or via the web page) but you'll need to update the `pssh` properties to be converted into a `new Uint8Array`.
22 |
23 | ### Running Tests
24 |
25 | Testing is a crucial part of any software project. For all but the most trivial changes (typos, etc) test cases are expected. Tests are run in actual browsers using [Karma][karma].
26 |
27 | - In all available and supported browsers: `npm test`
28 | - In a specific browser: `npm run test:chrome`, `npm run test:firefox`, etc.
29 | - While development server is running (`npm start`), navigate to [`http://localhost:9999/test/`][local]
30 |
31 |
32 | [karma]: http://karma-runner.github.io/
33 | [local]: http://localhost:9999/test/
34 | [conventions]: https://github.com/videojs/generator-videojs-plugin/blob/master/docs/conventions.md
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright Brightcove, Inc
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # mpd-parser
2 |
3 | [](https://travis-ci.org/videojs/mpd-parser)
4 | [](https://greenkeeper.io/)
5 | [](http://slack.videojs.com)
6 |
7 | [](https://nodei.co/npm/mpd-parser/)
8 |
9 | mpd parser
10 |
11 | ## Table of Contents
12 |
13 |
14 |
15 |
16 |
17 | - [Installation](#installation)
18 | - [Usage](#usage)
19 | - [Parsed Output](#parsed-output)
20 | - [Including the Parser](#including-the-parser)
21 | - [`
147 |
151 | ```
152 |
153 | ### Browserify
154 |
155 | When using with Browserify, install mpd-parser via npm and `require` the parser as you would any other module.
156 |
157 | ```js
158 | var mpdParser = require('mpd-parser');
159 |
160 | var parsedManifest = mpdParser.parse(manifest, { manifestUri });
161 | ```
162 |
163 | With ES6:
164 | ```js
165 | import { parse } from 'mpd-parser';
166 |
167 | const parsedManifest = parse(manifest, { manifestUri });
168 | ```
169 |
170 | ### RequireJS/AMD
171 |
172 | When using with RequireJS (or another AMD library), get the script in whatever way you prefer and `require` the parser as you normally would:
173 |
174 | ```js
175 | require(['mpd-parser'], function(mpdParser) {
176 | var parsedManifest = mpdParser.parse(manifest, { manifestUri });
177 | });
178 | ```
179 |
180 | ## License
181 |
182 | Apache-2.0. Copyright (c) Brightcove, Inc
183 |
--------------------------------------------------------------------------------
/bin/parse.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | /* eslint-disable no-console */
3 | const fs = require('fs');
4 | const path = require('path');
5 | const {parse} = require('../dist/mpd-parser.cjs.js');
6 |
7 | const file = path.resolve(process.cwd(), process.argv[2]);
8 | const result = parse(fs.readFileSync(file, 'utf8'), {
9 | manifestUri: ''
10 | });
11 |
12 | console.log(JSON.stringify(result, null, 2));
13 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | mpd-parser Demo
6 |
7 |
8 | Open dev tools to try it out
9 |
13 |
14 |
21 |
22 |
23 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mpd-parser",
3 | "version": "1.3.1",
4 | "description": "mpd parser",
5 | "main": "dist/mpd-parser.cjs.js",
6 | "module": "dist/mpd-parser.es.js",
7 | "repository": {
8 | "type": "git",
9 | "url": "git@github.com:videojs/mpd-parser.git"
10 | },
11 | "bin": {
12 | "mpd-to-m3u8-json": "bin/parse.js"
13 | },
14 | "scripts": {
15 | "prenetlify": "npm run build",
16 | "netlify": "node scripts/netlify.js",
17 | "build-test": "cross-env-shell TEST_BUNDLE_ONLY=1 'npm run build'",
18 | "build-prod": "cross-env-shell NO_TEST_BUNDLE=1 'npm run build'",
19 | "build": "npm-run-all -s clean -p build:*",
20 | "build:js": "rollup -c scripts/rollup.config.js",
21 | "clean": "shx rm -rf ./dist ./test/dist && shx mkdir -p ./dist ./test/dist",
22 | "lint": "vjsstandard",
23 | "prepublishOnly": "npm-run-all build-prod && vjsverify --verbose --skip-es-check",
24 | "start": "npm-run-all -p server watch",
25 | "server": "karma start scripts/karma.conf.js --singleRun=false --auto-watch",
26 | "test": "npm-run-all lint build-test && npm-run-all test:*",
27 | "test:browser": "karma start scripts/karma.conf.js",
28 | "test:node": "qunit test/dist/bundle-node.js",
29 | "posttest": "shx cat test/dist/coverage/text.txt",
30 | "version": "is-prerelease || npm run update-changelog && git add CHANGELOG.md",
31 | "update-changelog": "conventional-changelog -p videojs -i CHANGELOG.md -s",
32 | "watch": "npm-run-all -p watch:*",
33 | "watch:js": "npm run build:js -- -w"
34 | },
35 | "keywords": [
36 | "videojs",
37 | "videojs-plugin"
38 | ],
39 | "author": "Brightcove, Inc",
40 | "license": "Apache-2.0",
41 | "vjsstandard": {
42 | "ignore": [
43 | "dist",
44 | "docs",
45 | "test/dist"
46 | ]
47 | },
48 | "files": [
49 | "CONTRIBUTING.md",
50 | "dist/",
51 | "docs/",
52 | "index.html",
53 | "scripts/",
54 | "src/",
55 | "test/"
56 | ],
57 | "dependencies": {
58 | "@babel/runtime": "^7.12.5",
59 | "@videojs/vhs-utils": "^4.0.0",
60 | "@xmldom/xmldom": "^0.8.3",
61 | "global": "^4.4.0"
62 | },
63 | "devDependencies": {
64 | "@rollup/plugin-replace": "^2.3.4",
65 | "@videojs/generator-helpers": "~2.0.1",
66 | "jsdom": "^16.4.0",
67 | "karma": "^5.2.3",
68 | "rollup": "^2.38.0",
69 | "rollup-plugin-string": "^3.0.0",
70 | "sinon": "^11.1.1",
71 | "videojs-generate-karma-config": "^8.0.1",
72 | "videojs-generate-rollup-config": "~7.0.0",
73 | "videojs-generator-verify": "~3.0.2",
74 | "videojs-standard": "^9.0.1"
75 | },
76 | "generator-videojs-plugin": {
77 | "version": "7.7.3"
78 | },
79 | "lint-staged": {
80 | "*.js": "vjsstandard --fix",
81 | "README.md": "doctoc --notitle"
82 | },
83 | "husky": {
84 | "hooks": {
85 | "pre-commit": "lint-staged"
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/scripts/karma.conf.js:
--------------------------------------------------------------------------------
1 | const generate = require('videojs-generate-karma-config');
2 |
3 | module.exports = function(config) {
4 |
5 | // see https://github.com/videojs/videojs-generate-karma-config
6 | // for options
7 | const options = {};
8 |
9 | config = generate(config, options);
10 |
11 | // any other custom stuff not supported by options here!
12 | };
13 |
14 |
--------------------------------------------------------------------------------
/scripts/netlify.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const sh = require('shelljs');
3 |
4 | const files = ['dist', 'index.html'];
5 | const deployDir = 'deploy';
6 |
7 | // cleanup previous deploy
8 | sh.rm('-rf', deployDir);
9 | // make sure the directory exists
10 | sh.mkdir('-p', deployDir);
11 |
12 | // copy over dist, and html files
13 | files
14 | .forEach((file) => sh.cp('-r', file, path.join(deployDir, file)));
15 |
--------------------------------------------------------------------------------
/scripts/rollup.config.js:
--------------------------------------------------------------------------------
1 | const generate = require('videojs-generate-rollup-config');
2 | const string = require('rollup-plugin-string').string;
3 | const replace = require('@rollup/plugin-replace');
4 |
5 | // see https://github.com/videojs/videojs-generate-rollup-config
6 | // for options
7 | const options = {
8 | input: 'src/index.js',
9 | plugins(defaults) {
10 | defaults.test.unshift('string');
11 | defaults.module.unshift('replace');
12 |
13 | return defaults;
14 | },
15 | primedPlugins(defaults) {
16 | defaults.string = string({include: ['test/manifests/*.mpd']});
17 | // when using "require" rather than import
18 | // require cjs module
19 | defaults.replace = replace({
20 | // single quote replace
21 | "require('@videojs/vhs-utils/es": "require('@videojs/vhs-utils/cjs",
22 | // double quote replace
23 | 'require("@videojs/vhs-utils/es': 'require("@videojs/vhs-utils/cjs'
24 | });
25 |
26 | return defaults;
27 | },
28 | externals(defaults) {
29 | defaults.module.push('@videojs/vhs-utils');
30 | defaults.module.push('@xmldom/xmldom');
31 | defaults.module.push('atob');
32 | defaults.module.push('url-toolkit');
33 | return defaults;
34 | },
35 | globals(defaults) {
36 | defaults.browser['@xmldom/xmldom'] = 'window';
37 | defaults.browser.atob = 'window.atob';
38 | defaults.test['@xmldom/xmldom'] = 'window';
39 | defaults.test.atob = 'window.atob';
40 | defaults.test.jsdom = '{JSDOM: function() { return {window: window}; }}';
41 | return defaults;
42 | }
43 | };
44 | const config = generate(options);
45 |
46 | if (config.builds.test) {
47 | config.builds.testNode = config.makeBuild('test', {
48 | input: 'test/**/*.test.js',
49 | output: [{
50 | name: `${config.settings.exportName}Tests`,
51 | file: 'test/dist/bundle-node.js',
52 | format: 'cjs'
53 | }]
54 | });
55 |
56 | config.builds.testNode.output[0].globals = {};
57 | config.builds.testNode.external = [].concat(config.settings.externals.module).concat([
58 | 'jsdom',
59 | 'qunit'
60 | ]);
61 | }
62 |
63 | // Add additonal builds/customization here!
64 |
65 | // export the builds to rollup
66 | export default Object.values(config.builds);
67 |
--------------------------------------------------------------------------------
/src/errors.js:
--------------------------------------------------------------------------------
1 | export default {
2 | INVALID_NUMBER_OF_PERIOD: 'INVALID_NUMBER_OF_PERIOD',
3 | INVALID_NUMBER_OF_CONTENT_STEERING: 'INVALID_NUMBER_OF_CONTENT_STEERING',
4 | DASH_EMPTY_MANIFEST: 'DASH_EMPTY_MANIFEST',
5 | DASH_INVALID_XML: 'DASH_INVALID_XML',
6 | NO_BASE_URL: 'NO_BASE_URL',
7 | MISSING_SEGMENT_INFORMATION: 'MISSING_SEGMENT_INFORMATION',
8 | SEGMENT_TIME_UNSPECIFIED: 'SEGMENT_TIME_UNSPECIFIED',
9 | UNSUPPORTED_UTC_TIMING_SCHEME: 'UNSUPPORTED_UTC_TIMING_SCHEME'
10 | };
11 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { version } from '../package.json';
2 | import { toM3u8, generateSidxKey } from './toM3u8';
3 | import { toPlaylists } from './toPlaylists';
4 | import { inheritAttributes } from './inheritAttributes';
5 | import { stringToMpdXml } from './stringToMpdXml';
6 | import { parseUTCTimingScheme } from './parseUTCTimingScheme';
7 | import {addSidxSegmentsToPlaylist} from './segment/segmentBase.js';
8 |
9 | const VERSION = version;
10 |
11 | /*
12 | * Given a DASH manifest string and options, parses the DASH manifest into an object in the
13 | * form outputed by m3u8-parser and accepted by videojs/http-streaming.
14 | *
15 | * For live DASH manifests, if `previousManifest` is provided in options, then the newly
16 | * parsed DASH manifest will have its media sequence and discontinuity sequence values
17 | * updated to reflect its position relative to the prior manifest.
18 | *
19 | * @param {string} manifestString - the DASH manifest as a string
20 | * @param {options} [options] - any options
21 | *
22 | * @return {Object} the manifest object
23 | */
24 | const parse = (manifestString, options = {}) => {
25 | const parsedManifestInfo = inheritAttributes(stringToMpdXml(manifestString), options);
26 | const playlists = toPlaylists(parsedManifestInfo.representationInfo);
27 |
28 | return toM3u8({
29 | dashPlaylists: playlists,
30 | locations: parsedManifestInfo.locations,
31 | contentSteering: parsedManifestInfo.contentSteeringInfo,
32 | sidxMapping: options.sidxMapping,
33 | previousManifest: options.previousManifest,
34 | eventStream: parsedManifestInfo.eventStream
35 | });
36 | };
37 |
38 | /**
39 | * Parses the manifest for a UTCTiming node, returning the nodes attributes if found
40 | *
41 | * @param {string} manifestString
42 | * XML string of the MPD manifest
43 | * @return {Object|null}
44 | * Attributes of UTCTiming node specified in the manifest. Null if none found
45 | */
46 | const parseUTCTiming = (manifestString) =>
47 | parseUTCTimingScheme(stringToMpdXml(manifestString));
48 |
49 | export {
50 | VERSION,
51 | parse,
52 | parseUTCTiming,
53 | stringToMpdXml,
54 | inheritAttributes,
55 | toPlaylists,
56 | toM3u8,
57 | addSidxSegmentsToPlaylist,
58 | generateSidxKey
59 | };
60 |
--------------------------------------------------------------------------------
/src/parseAttributes.js:
--------------------------------------------------------------------------------
1 | import { parseDivisionValue } from './utils/string';
2 | import { from } from './utils/list';
3 | import { parseDuration, parseDate } from './utils/time';
4 |
5 | // TODO: maybe order these in some way that makes it easy to find specific attributes
6 | export const parsers = {
7 | /**
8 | * Specifies the duration of the entire Media Presentation. Format is a duration string
9 | * as specified in ISO 8601
10 | *
11 | * @param {string} value
12 | * value of attribute as a string
13 | * @return {number}
14 | * The duration in seconds
15 | */
16 | mediaPresentationDuration(value) {
17 | return parseDuration(value);
18 | },
19 |
20 | /**
21 | * Specifies the Segment availability start time for all Segments referred to in this
22 | * MPD. For a dynamic manifest, it specifies the anchor for the earliest availability
23 | * time. Format is a date string as specified in ISO 8601
24 | *
25 | * @param {string} value
26 | * value of attribute as a string
27 | * @return {number}
28 | * The date as seconds from unix epoch
29 | */
30 | availabilityStartTime(value) {
31 | return parseDate(value) / 1000;
32 | },
33 |
34 | /**
35 | * Specifies the smallest period between potential changes to the MPD. Format is a
36 | * duration string as specified in ISO 8601
37 | *
38 | * @param {string} value
39 | * value of attribute as a string
40 | * @return {number}
41 | * The duration in seconds
42 | */
43 | minimumUpdatePeriod(value) {
44 | return parseDuration(value);
45 | },
46 |
47 | /**
48 | * Specifies the suggested presentation delay. Format is a
49 | * duration string as specified in ISO 8601
50 | *
51 | * @param {string} value
52 | * value of attribute as a string
53 | * @return {number}
54 | * The duration in seconds
55 | */
56 | suggestedPresentationDelay(value) {
57 | return parseDuration(value);
58 | },
59 |
60 | /**
61 | * specifices the type of mpd. Can be either "static" or "dynamic"
62 | *
63 | * @param {string} value
64 | * value of attribute as a string
65 | *
66 | * @return {string}
67 | * The type as a string
68 | */
69 | type(value) {
70 | return value;
71 | },
72 |
73 | /**
74 | * Specifies the duration of the smallest time shifting buffer for any Representation
75 | * in the MPD. Format is a duration string as specified in ISO 8601
76 | *
77 | * @param {string} value
78 | * value of attribute as a string
79 | * @return {number}
80 | * The duration in seconds
81 | */
82 | timeShiftBufferDepth(value) {
83 | return parseDuration(value);
84 | },
85 |
86 | /**
87 | * Specifies the PeriodStart time of the Period relative to the availabilityStarttime.
88 | * Format is a duration string as specified in ISO 8601
89 | *
90 | * @param {string} value
91 | * value of attribute as a string
92 | * @return {number}
93 | * The duration in seconds
94 | */
95 | start(value) {
96 | return parseDuration(value);
97 | },
98 |
99 | /**
100 | * Specifies the width of the visual presentation
101 | *
102 | * @param {string} value
103 | * value of attribute as a string
104 | * @return {number}
105 | * The parsed width
106 | */
107 | width(value) {
108 | return parseInt(value, 10);
109 | },
110 |
111 | /**
112 | * Specifies the height of the visual presentation
113 | *
114 | * @param {string} value
115 | * value of attribute as a string
116 | * @return {number}
117 | * The parsed height
118 | */
119 | height(value) {
120 | return parseInt(value, 10);
121 | },
122 |
123 | /**
124 | * Specifies the bitrate of the representation
125 | *
126 | * @param {string} value
127 | * value of attribute as a string
128 | * @return {number}
129 | * The parsed bandwidth
130 | */
131 | bandwidth(value) {
132 | return parseInt(value, 10);
133 | },
134 |
135 | /**
136 | * Specifies the frame rate of the representation
137 | *
138 | * @param {string} value
139 | * value of attribute as a string
140 | * @return {number}
141 | * The parsed frame rate
142 | */
143 | frameRate(value) {
144 | return parseDivisionValue(value);
145 | },
146 |
147 | /**
148 | * Specifies the number of the first Media Segment in this Representation in the Period
149 | *
150 | * @param {string} value
151 | * value of attribute as a string
152 | * @return {number}
153 | * The parsed number
154 | */
155 | startNumber(value) {
156 | return parseInt(value, 10);
157 | },
158 |
159 | /**
160 | * Specifies the timescale in units per seconds
161 | *
162 | * @param {string} value
163 | * value of attribute as a string
164 | * @return {number}
165 | * The parsed timescale
166 | */
167 | timescale(value) {
168 | return parseInt(value, 10);
169 | },
170 |
171 | /**
172 | * Specifies the presentationTimeOffset.
173 | *
174 | * @param {string} value
175 | * value of the attribute as a string
176 | *
177 | * @return {number}
178 | * The parsed presentationTimeOffset
179 | */
180 | presentationTimeOffset(value) {
181 | return parseInt(value, 10);
182 | },
183 |
184 | /**
185 | * Specifies the constant approximate Segment duration
186 | * NOTE: The element also contains an @duration attribute. This duration
187 | * specifies the duration of the Period. This attribute is currently not
188 | * supported by the rest of the parser, however we still check for it to prevent
189 | * errors.
190 | *
191 | * @param {string} value
192 | * value of attribute as a string
193 | * @return {number}
194 | * The parsed duration
195 | */
196 | duration(value) {
197 | const parsedValue = parseInt(value, 10);
198 |
199 | if (isNaN(parsedValue)) {
200 | return parseDuration(value);
201 | }
202 |
203 | return parsedValue;
204 | },
205 |
206 | /**
207 | * Specifies the Segment duration, in units of the value of the @timescale.
208 | *
209 | * @param {string} value
210 | * value of attribute as a string
211 | * @return {number}
212 | * The parsed duration
213 | */
214 | d(value) {
215 | return parseInt(value, 10);
216 | },
217 |
218 | /**
219 | * Specifies the MPD start time, in @timescale units, the first Segment in the series
220 | * starts relative to the beginning of the Period
221 | *
222 | * @param {string} value
223 | * value of attribute as a string
224 | * @return {number}
225 | * The parsed time
226 | */
227 | t(value) {
228 | return parseInt(value, 10);
229 | },
230 |
231 | /**
232 | * Specifies the repeat count of the number of following contiguous Segments with the
233 | * same duration expressed by the value of @d
234 | *
235 | * @param {string} value
236 | * value of attribute as a string
237 | * @return {number}
238 | * The parsed number
239 | */
240 | r(value) {
241 | return parseInt(value, 10);
242 | },
243 |
244 | /**
245 | * Specifies the presentationTime.
246 | *
247 | * @param {string} value
248 | * value of the attribute as a string
249 | *
250 | * @return {number}
251 | * The parsed presentationTime
252 | */
253 | presentationTime(value) {
254 | return parseInt(value, 10);
255 | },
256 |
257 | /**
258 | * Default parser for all other attributes. Acts as a no-op and just returns the value
259 | * as a string
260 | *
261 | * @param {string} value
262 | * value of attribute as a string
263 | * @return {string}
264 | * Unparsed value
265 | */
266 | DEFAULT(value) {
267 | return value;
268 | }
269 | };
270 |
271 | /**
272 | * Gets all the attributes and values of the provided node, parses attributes with known
273 | * types, and returns an object with attribute names mapped to values.
274 | *
275 | * @param {Node} el
276 | * The node to parse attributes from
277 | * @return {Object}
278 | * Object with all attributes of el parsed
279 | */
280 | export const parseAttributes = (el) => {
281 | if (!(el && el.attributes)) {
282 | return {};
283 | }
284 |
285 | return from(el.attributes)
286 | .reduce((a, e) => {
287 | const parseFn = parsers[e.name] || parsers.DEFAULT;
288 |
289 | a[e.name] = parseFn(e.value);
290 |
291 | return a;
292 | }, {});
293 | };
294 |
--------------------------------------------------------------------------------
/src/parseUTCTimingScheme.js:
--------------------------------------------------------------------------------
1 | import { findChildren } from './utils/xml';
2 | import { parseAttributes } from './parseAttributes';
3 | import errors from './errors';
4 |
5 | /**
6 | * Parses the manifest for a UTCTiming node, returning the nodes attributes if found
7 | *
8 | * @param {string} mpd
9 | * XML string of the MPD manifest
10 | * @return {Object|null}
11 | * Attributes of UTCTiming node specified in the manifest. Null if none found
12 | */
13 | export const parseUTCTimingScheme = (mpd) => {
14 | const UTCTimingNode = findChildren(mpd, 'UTCTiming')[0];
15 |
16 | if (!UTCTimingNode) {
17 | return null;
18 | }
19 |
20 | const attributes = parseAttributes(UTCTimingNode);
21 |
22 | switch (attributes.schemeIdUri) {
23 | case 'urn:mpeg:dash:utc:http-head:2014':
24 | case 'urn:mpeg:dash:utc:http-head:2012':
25 | attributes.method = 'HEAD';
26 | break;
27 | case 'urn:mpeg:dash:utc:http-xsdate:2014':
28 | case 'urn:mpeg:dash:utc:http-iso:2014':
29 | case 'urn:mpeg:dash:utc:http-xsdate:2012':
30 | case 'urn:mpeg:dash:utc:http-iso:2012':
31 | attributes.method = 'GET';
32 | break;
33 | case 'urn:mpeg:dash:utc:direct:2014':
34 | case 'urn:mpeg:dash:utc:direct:2012':
35 | attributes.method = 'DIRECT';
36 | attributes.value = Date.parse(attributes.value);
37 | break;
38 | case 'urn:mpeg:dash:utc:http-ntp:2014':
39 | case 'urn:mpeg:dash:utc:ntp:2014':
40 | case 'urn:mpeg:dash:utc:sntp:2014':
41 | default:
42 | throw new Error(errors.UNSUPPORTED_UTC_TIMING_SCHEME);
43 | }
44 |
45 | return attributes;
46 | };
47 |
--------------------------------------------------------------------------------
/src/playlist-merge.js:
--------------------------------------------------------------------------------
1 | import { forEachMediaGroup } from '@videojs/vhs-utils/es/media-groups';
2 | import { union } from './utils/list';
3 |
4 | const SUPPORTED_MEDIA_TYPES = ['AUDIO', 'SUBTITLES'];
5 | // allow one 60fps frame as leniency (arbitrarily chosen)
6 | const TIME_FUDGE = 1 / 60;
7 |
8 | /**
9 | * Given a list of timelineStarts, combines, dedupes, and sorts them.
10 | *
11 | * @param {TimelineStart[]} timelineStarts - list of timeline starts
12 | *
13 | * @return {TimelineStart[]} the combined and deduped timeline starts
14 | */
15 | export const getUniqueTimelineStarts = (timelineStarts) => {
16 | return union(timelineStarts, ({ timeline }) => timeline)
17 | .sort((a, b) => (a.timeline > b.timeline) ? 1 : -1);
18 | };
19 |
20 | /**
21 | * Finds the playlist with the matching NAME attribute.
22 | *
23 | * @param {Array} playlists - playlists to search through
24 | * @param {string} name - the NAME attribute to search for
25 | *
26 | * @return {Object|null} the matching playlist object, or null
27 | */
28 | export const findPlaylistWithName = (playlists, name) => {
29 | for (let i = 0; i < playlists.length; i++) {
30 | if (playlists[i].attributes.NAME === name) {
31 | return playlists[i];
32 | }
33 | }
34 |
35 | return null;
36 | };
37 |
38 | /**
39 | * Gets a flattened array of media group playlists.
40 | *
41 | * @param {Object} manifest - the main manifest object
42 | *
43 | * @return {Array} the media group playlists
44 | */
45 | export const getMediaGroupPlaylists = (manifest) => {
46 | let mediaGroupPlaylists = [];
47 |
48 | forEachMediaGroup(manifest, SUPPORTED_MEDIA_TYPES, (properties, type, group, label) => {
49 | mediaGroupPlaylists = mediaGroupPlaylists.concat(properties.playlists || []);
50 | });
51 |
52 | return mediaGroupPlaylists;
53 | };
54 |
55 | /**
56 | * Updates the playlist's media sequence numbers.
57 | *
58 | * @param {Object} config - options object
59 | * @param {Object} config.playlist - the playlist to update
60 | * @param {number} config.mediaSequence - the mediaSequence number to start with
61 | */
62 | export const updateMediaSequenceForPlaylist = ({ playlist, mediaSequence }) => {
63 | playlist.mediaSequence = mediaSequence;
64 | playlist.segments.forEach((segment, index) => {
65 | segment.number = playlist.mediaSequence + index;
66 | });
67 | };
68 |
69 | /**
70 | * Updates the media and discontinuity sequence numbers of newPlaylists given oldPlaylists
71 | * and a complete list of timeline starts.
72 | *
73 | * If no matching playlist is found, only the discontinuity sequence number of the playlist
74 | * will be updated.
75 | *
76 | * Since early available timelines are not supported, at least one segment must be present.
77 | *
78 | * @param {Object} config - options object
79 | * @param {Object[]} oldPlaylists - the old playlists to use as a reference
80 | * @param {Object[]} newPlaylists - the new playlists to update
81 | * @param {Object} timelineStarts - all timelineStarts seen in the stream to this point
82 | */
83 | export const updateSequenceNumbers = ({ oldPlaylists, newPlaylists, timelineStarts }) => {
84 | newPlaylists.forEach((playlist) => {
85 | playlist.discontinuitySequence = timelineStarts.findIndex(function({
86 | timeline
87 | }) {
88 | return timeline === playlist.timeline;
89 | });
90 |
91 | // Playlists NAMEs come from DASH Representation IDs, which are mandatory
92 | // (see ISO_23009-1-2012 5.3.5.2).
93 | //
94 | // If the same Representation existed in a prior Period, it will retain the same NAME.
95 | const oldPlaylist = findPlaylistWithName(oldPlaylists, playlist.attributes.NAME);
96 |
97 | if (!oldPlaylist) {
98 | // Since this is a new playlist, the media sequence values can start from 0 without
99 | // consequence.
100 | return;
101 | }
102 |
103 | // TODO better support for live SIDX
104 | //
105 | // As of this writing, mpd-parser does not support multiperiod SIDX (in live or VOD).
106 | // This is evident by a playlist only having a single SIDX reference. In a multiperiod
107 | // playlist there would need to be multiple SIDX references. In addition, live SIDX is
108 | // not supported when the SIDX properties change on refreshes.
109 | //
110 | // In the future, if support needs to be added, the merging logic here can be called
111 | // after SIDX references are resolved. For now, exit early to prevent exceptions being
112 | // thrown due to undefined references.
113 | if (playlist.sidx) {
114 | return;
115 | }
116 |
117 | // Since we don't yet support early available timelines, we don't need to support
118 | // playlists with no segments.
119 | const firstNewSegment = playlist.segments[0];
120 | const oldMatchingSegmentIndex = oldPlaylist.segments.findIndex(function(oldSegment) {
121 | return (
122 | Math.abs(oldSegment.presentationTime - firstNewSegment.presentationTime) < TIME_FUDGE
123 | );
124 | });
125 |
126 | // No matching segment from the old playlist means the entire playlist was refreshed.
127 | // In this case the media sequence should account for this update, and the new segments
128 | // should be marked as discontinuous from the prior content, since the last prior
129 | // timeline was removed.
130 | if (oldMatchingSegmentIndex === -1) {
131 | updateMediaSequenceForPlaylist({
132 | playlist,
133 | mediaSequence: oldPlaylist.mediaSequence + oldPlaylist.segments.length
134 | });
135 | playlist.segments[0].discontinuity = true;
136 | playlist.discontinuityStarts.unshift(0);
137 |
138 | // No matching segment does not necessarily mean there's missing content.
139 | //
140 | // If the new playlist's timeline is the same as the last seen segment's timeline,
141 | // then a discontinuity can be added to identify that there's potentially missing
142 | // content. If there's no missing content, the discontinuity should still be rather
143 | // harmless. It's possible that if segment durations are accurate enough, that the
144 | // existence of a gap can be determined using the presentation times and durations,
145 | // but if the segment timing info is off, it may introduce more problems than simply
146 | // adding the discontinuity.
147 | //
148 | // If the new playlist's timeline is different from the last seen segment's timeline,
149 | // then a discontinuity can be added to identify that this is the first seen segment
150 | // of a new timeline. However, the logic at the start of this function that
151 | // determined the disconinuity sequence by timeline index is now off by one (the
152 | // discontinuity of the newest timeline hasn't yet fallen off the manifest...since
153 | // we added it), so the disconinuity sequence must be decremented.
154 | //
155 | // A period may also have a duration of zero, so the case of no segments is handled
156 | // here even though we don't yet support early available periods.
157 | if ((!oldPlaylist.segments.length && playlist.timeline > oldPlaylist.timeline) ||
158 | (oldPlaylist.segments.length && playlist.timeline >
159 | oldPlaylist.segments[oldPlaylist.segments.length - 1].timeline)) {
160 | playlist.discontinuitySequence--;
161 | }
162 | return;
163 | }
164 |
165 | // If the first segment matched with a prior segment on a discontinuity (it's matching
166 | // on the first segment of a period), then the discontinuitySequence shouldn't be the
167 | // timeline's matching one, but instead should be the one prior, and the first segment
168 | // of the new manifest should be marked with a discontinuity.
169 | //
170 | // The reason for this special case is that discontinuity sequence shows how many
171 | // discontinuities have fallen off of the playlist, and discontinuities are marked on
172 | // the first segment of a new "timeline." Because of this, while DASH will retain that
173 | // Period while the "timeline" exists, HLS keeps track of it via the discontinuity
174 | // sequence, and that first segment is an indicator, but can be removed before that
175 | // timeline is gone.
176 | const oldMatchingSegment = oldPlaylist.segments[oldMatchingSegmentIndex];
177 |
178 | if (oldMatchingSegment.discontinuity && !firstNewSegment.discontinuity) {
179 | firstNewSegment.discontinuity = true;
180 | playlist.discontinuityStarts.unshift(0);
181 | playlist.discontinuitySequence--;
182 | }
183 |
184 | updateMediaSequenceForPlaylist({
185 | playlist,
186 | mediaSequence: oldPlaylist.segments[oldMatchingSegmentIndex].number
187 | });
188 | });
189 | };
190 |
191 | /**
192 | * Given an old parsed manifest object and a new parsed manifest object, updates the
193 | * sequence and timing values within the new manifest to ensure that it lines up with the
194 | * old.
195 | *
196 | * @param {Array} oldManifest - the old main manifest object
197 | * @param {Array} newManifest - the new main manifest object
198 | *
199 | * @return {Object} the updated new manifest object
200 | */
201 | export const positionManifestOnTimeline = ({ oldManifest, newManifest }) => {
202 | // Starting from v4.1.2 of the IOP, section 4.4.3.3 states:
203 | //
204 | // "MPD@availabilityStartTime and Period@start shall not be changed over MPD updates."
205 | //
206 | // This was added from https://github.com/Dash-Industry-Forum/DASH-IF-IOP/issues/160
207 | //
208 | // Because of this change, and the difficulty of supporting periods with changing start
209 | // times, periods with changing start times are not supported. This makes the logic much
210 | // simpler, since periods with the same start time can be considerred the same period
211 | // across refreshes.
212 | //
213 | // To give an example as to the difficulty of handling periods where the start time may
214 | // change, if a single period manifest is refreshed with another manifest with a single
215 | // period, and both the start and end times are increased, then the only way to determine
216 | // if it's a new period or an old one that has changed is to look through the segments of
217 | // each playlist and determine the presentation time bounds to find a match. In addition,
218 | // if the period start changed to exceed the old period end, then there would be no
219 | // match, and it would not be possible to determine whether the refreshed period is a new
220 | // one or the old one.
221 | const oldPlaylists = oldManifest.playlists.concat(getMediaGroupPlaylists(oldManifest));
222 | const newPlaylists = newManifest.playlists.concat(getMediaGroupPlaylists(newManifest));
223 |
224 | // Save all seen timelineStarts to the new manifest. Although this potentially means that
225 | // there's a "memory leak" in that it will never stop growing, in reality, only a couple
226 | // of properties are saved for each seen Period. Even long running live streams won't
227 | // generate too many Periods, unless the stream is watched for decades. In the future,
228 | // this can be optimized by mapping to discontinuity sequence numbers for each timeline,
229 | // but it may not become an issue, and the additional info can be useful for debugging.
230 | newManifest.timelineStarts = getUniqueTimelineStarts([oldManifest.timelineStarts,
231 | newManifest.timelineStarts]);
232 |
233 | updateSequenceNumbers({
234 | oldPlaylists,
235 | newPlaylists,
236 | timelineStarts: newManifest.timelineStarts
237 | });
238 |
239 | return newManifest;
240 | };
241 |
--------------------------------------------------------------------------------
/src/segment/durationTimeParser.js:
--------------------------------------------------------------------------------
1 | import { range } from '../utils/list';
2 |
3 | /**
4 | * parse the end number attribue that can be a string
5 | * number, or undefined.
6 | *
7 | * @param {string|number|undefined} endNumber
8 | * The end number attribute.
9 | *
10 | * @return {number|null}
11 | * The result of parsing the end number.
12 | */
13 | const parseEndNumber = (endNumber) => {
14 | if (endNumber && typeof endNumber !== 'number') {
15 | endNumber = parseInt(endNumber, 10);
16 | }
17 |
18 | if (isNaN(endNumber)) {
19 | return null;
20 | }
21 |
22 | return endNumber;
23 | };
24 |
25 | /**
26 | * Functions for calculating the range of available segments in static and dynamic
27 | * manifests.
28 | */
29 | export const segmentRange = {
30 | /**
31 | * Returns the entire range of available segments for a static MPD
32 | *
33 | * @param {Object} attributes
34 | * Inheritied MPD attributes
35 | * @return {{ start: number, end: number }}
36 | * The start and end numbers for available segments
37 | */
38 | static(attributes) {
39 | const {
40 | duration,
41 | timescale = 1,
42 | sourceDuration,
43 | periodDuration
44 | } = attributes;
45 | const endNumber = parseEndNumber(attributes.endNumber);
46 | const segmentDuration = duration / timescale;
47 |
48 | if (typeof endNumber === 'number') {
49 | return { start: 0, end: endNumber };
50 | }
51 |
52 | if (typeof periodDuration === 'number') {
53 | return { start: 0, end: periodDuration / segmentDuration };
54 | }
55 |
56 | return { start: 0, end: sourceDuration / segmentDuration };
57 | },
58 |
59 | /**
60 | * Returns the current live window range of available segments for a dynamic MPD
61 | *
62 | * @param {Object} attributes
63 | * Inheritied MPD attributes
64 | * @return {{ start: number, end: number }}
65 | * The start and end numbers for available segments
66 | */
67 | dynamic(attributes) {
68 | const {
69 | NOW,
70 | clientOffset,
71 | availabilityStartTime,
72 | timescale = 1,
73 | duration,
74 | periodStart = 0,
75 | minimumUpdatePeriod = 0,
76 | timeShiftBufferDepth = Infinity
77 | } = attributes;
78 | const endNumber = parseEndNumber(attributes.endNumber);
79 | // clientOffset is passed in at the top level of mpd-parser and is an offset calculated
80 | // after retrieving UTC server time.
81 | const now = (NOW + clientOffset) / 1000;
82 | // WC stands for Wall Clock.
83 | // Convert the period start time to EPOCH.
84 | const periodStartWC = availabilityStartTime + periodStart;
85 | // Period end in EPOCH is manifest's retrieval time + time until next update.
86 | const periodEndWC = now + minimumUpdatePeriod;
87 | const periodDuration = periodEndWC - periodStartWC;
88 | const segmentCount = Math.ceil(periodDuration * timescale / duration);
89 | const availableStart =
90 | Math.floor((now - periodStartWC - timeShiftBufferDepth) * timescale / duration);
91 | const availableEnd = Math.floor((now - periodStartWC) * timescale / duration);
92 |
93 | return {
94 | start: Math.max(0, availableStart),
95 | end: typeof endNumber === 'number' ? endNumber : Math.min(segmentCount, availableEnd)
96 | };
97 | }
98 | };
99 |
100 | /**
101 | * Maps a range of numbers to objects with information needed to build the corresponding
102 | * segment list
103 | *
104 | * @name toSegmentsCallback
105 | * @function
106 | * @param {number} number
107 | * Number of the segment
108 | * @param {number} index
109 | * Index of the number in the range list
110 | * @return {{ number: Number, duration: Number, timeline: Number, time: Number }}
111 | * Object with segment timing and duration info
112 | */
113 |
114 | /**
115 | * Returns a callback for Array.prototype.map for mapping a range of numbers to
116 | * information needed to build the segment list.
117 | *
118 | * @param {Object} attributes
119 | * Inherited MPD attributes
120 | * @return {toSegmentsCallback}
121 | * Callback map function
122 | */
123 | export const toSegments = (attributes) => (number) => {
124 | const {
125 | duration,
126 | timescale = 1,
127 | periodStart,
128 | startNumber = 1
129 | } = attributes;
130 |
131 | return {
132 | number: startNumber + number,
133 | duration: duration / timescale,
134 | timeline: periodStart,
135 | time: number * duration
136 | };
137 | };
138 |
139 | /**
140 | * Returns a list of objects containing segment timing and duration info used for
141 | * building the list of segments. This uses the @duration attribute specified
142 | * in the MPD manifest to derive the range of segments.
143 | *
144 | * @param {Object} attributes
145 | * Inherited MPD attributes
146 | * @return {{number: number, duration: number, time: number, timeline: number}[]}
147 | * List of Objects with segment timing and duration info
148 | */
149 | export const parseByDuration = (attributes) => {
150 | const {
151 | type,
152 | duration,
153 | timescale = 1,
154 | periodDuration,
155 | sourceDuration
156 | } = attributes;
157 |
158 | const { start, end } = segmentRange[type](attributes);
159 | const segments = range(start, end).map(toSegments(attributes));
160 |
161 | if (type === 'static') {
162 | const index = segments.length - 1;
163 | // section is either a period or the full source
164 | const sectionDuration =
165 | typeof periodDuration === 'number' ? periodDuration : sourceDuration;
166 |
167 | // final segment may be less than full segment duration
168 | segments[index].duration = sectionDuration - (duration / timescale * index);
169 | }
170 |
171 | return segments;
172 | };
173 |
--------------------------------------------------------------------------------
/src/segment/segmentBase.js:
--------------------------------------------------------------------------------
1 | import errors from '../errors';
2 | import urlTypeConverter from './urlType';
3 | import { parseByDuration } from './durationTimeParser';
4 | import window from 'global/window';
5 |
6 | /**
7 | * Translates SegmentBase into a set of segments.
8 | * (DASH SPEC Section 5.3.9.3.2) contains a set of nodes. Each
9 | * node should be translated into segment.
10 | *
11 | * @param {Object} attributes
12 | * Object containing all inherited attributes from parent elements with attribute
13 | * names as keys
14 | * @return {Object.} list of segments
15 | */
16 | export const segmentsFromBase = (attributes) => {
17 | const {
18 | baseUrl,
19 | initialization = {},
20 | sourceDuration,
21 | indexRange = '',
22 | periodStart,
23 | presentationTime,
24 | number = 0,
25 | duration
26 | } = attributes;
27 |
28 | // base url is required for SegmentBase to work, per spec (Section 5.3.9.2.1)
29 | if (!baseUrl) {
30 | throw new Error(errors.NO_BASE_URL);
31 | }
32 |
33 | const initSegment = urlTypeConverter({
34 | baseUrl,
35 | source: initialization.sourceURL,
36 | range: initialization.range
37 | });
38 |
39 | const segment = urlTypeConverter({ baseUrl, source: baseUrl, indexRange });
40 |
41 | segment.map = initSegment;
42 |
43 | // If there is a duration, use it, otherwise use the given duration of the source
44 | // (since SegmentBase is only for one total segment)
45 | if (duration) {
46 | const segmentTimeInfo = parseByDuration(attributes);
47 |
48 | if (segmentTimeInfo.length) {
49 | segment.duration = segmentTimeInfo[0].duration;
50 | segment.timeline = segmentTimeInfo[0].timeline;
51 | }
52 | } else if (sourceDuration) {
53 | segment.duration = sourceDuration;
54 | segment.timeline = periodStart;
55 | }
56 |
57 | // If presentation time is provided, these segments are being generated by SIDX
58 | // references, and should use the time provided. For the general case of SegmentBase,
59 | // there should only be one segment in the period, so its presentation time is the same
60 | // as its period start.
61 | segment.presentationTime = presentationTime || periodStart;
62 | segment.number = number;
63 |
64 | return [segment];
65 | };
66 |
67 | /**
68 | * Given a playlist, a sidx box, and a baseUrl, update the segment list of the playlist
69 | * according to the sidx information given.
70 | *
71 | * playlist.sidx has metadadata about the sidx where-as the sidx param
72 | * is the parsed sidx box itself.
73 | *
74 | * @param {Object} playlist the playlist to update the sidx information for
75 | * @param {Object} sidx the parsed sidx box
76 | * @return {Object} the playlist object with the updated sidx information
77 | */
78 | export const addSidxSegmentsToPlaylist = (playlist, sidx, baseUrl) => {
79 | // Retain init segment information
80 | const initSegment = playlist.sidx.map ? playlist.sidx.map : null;
81 | // Retain source duration from initial main manifest parsing
82 | const sourceDuration = playlist.sidx.duration;
83 | // Retain source timeline
84 | const timeline = playlist.timeline || 0;
85 | const sidxByteRange = playlist.sidx.byterange;
86 | const sidxEnd = sidxByteRange.offset + sidxByteRange.length;
87 | // Retain timescale of the parsed sidx
88 | const timescale = sidx.timescale;
89 | // referenceType 1 refers to other sidx boxes
90 | const mediaReferences = sidx.references.filter(r => r.referenceType !== 1);
91 | const segments = [];
92 | const type = playlist.endList ? 'static' : 'dynamic';
93 | const periodStart = playlist.sidx.timeline;
94 | let presentationTime = periodStart;
95 | let number = playlist.mediaSequence || 0;
96 |
97 | // firstOffset is the offset from the end of the sidx box
98 | let startIndex;
99 |
100 | // eslint-disable-next-line
101 | if (typeof sidx.firstOffset === 'bigint') {
102 | startIndex = window.BigInt(sidxEnd) + sidx.firstOffset;
103 | } else {
104 | startIndex = sidxEnd + sidx.firstOffset;
105 | }
106 |
107 | for (let i = 0; i < mediaReferences.length; i++) {
108 | const reference = sidx.references[i];
109 | // size of the referenced (sub)segment
110 | const size = reference.referencedSize;
111 | // duration of the referenced (sub)segment, in the timescale
112 | // this will be converted to seconds when generating segments
113 | const duration = reference.subsegmentDuration;
114 | // should be an inclusive range
115 | let endIndex;
116 |
117 | // eslint-disable-next-line
118 | if (typeof startIndex === 'bigint') {
119 | endIndex = startIndex + window.BigInt(size) - window.BigInt(1);
120 | } else {
121 | endIndex = startIndex + size - 1;
122 | }
123 | const indexRange = `${startIndex}-${endIndex}`;
124 |
125 | const attributes = {
126 | baseUrl,
127 | timescale,
128 | timeline,
129 | periodStart,
130 | presentationTime,
131 | number,
132 | duration,
133 | sourceDuration,
134 | indexRange,
135 | type
136 | };
137 |
138 | const segment = segmentsFromBase(attributes)[0];
139 |
140 | if (initSegment) {
141 | segment.map = initSegment;
142 | }
143 |
144 | segments.push(segment);
145 | if (typeof startIndex === 'bigint') {
146 | startIndex += window.BigInt(size);
147 | } else {
148 | startIndex += size;
149 | }
150 | presentationTime += duration / timescale;
151 | number++;
152 | }
153 |
154 | playlist.segments = segments;
155 |
156 | return playlist;
157 | };
158 |
--------------------------------------------------------------------------------
/src/segment/segmentList.js:
--------------------------------------------------------------------------------
1 | import { parseByTimeline } from './timelineTimeParser';
2 | import { parseByDuration } from './durationTimeParser';
3 | import urlTypeConverter from './urlType';
4 | import errors from '../errors';
5 |
6 | /**
7 | * Converts a (of type URLType from the DASH spec 5.3.9.2 Table 14)
8 | * to an object that matches the output of a segment in videojs/mpd-parser
9 | *
10 | * @param {Object} attributes
11 | * Object containing all inherited attributes from parent elements with attribute
12 | * names as keys
13 | * @param {Object} segmentUrl
14 | * node to translate into a segment object
15 | * @return {Object} translated segment object
16 | */
17 | const SegmentURLToSegmentObject = (attributes, segmentUrl) => {
18 | const { baseUrl, initialization = {} } = attributes;
19 |
20 | const initSegment = urlTypeConverter({
21 | baseUrl,
22 | source: initialization.sourceURL,
23 | range: initialization.range
24 | });
25 |
26 | const segment = urlTypeConverter({
27 | baseUrl,
28 | source: segmentUrl.media,
29 | range: segmentUrl.mediaRange
30 | });
31 |
32 | segment.map = initSegment;
33 |
34 | return segment;
35 | };
36 |
37 | /**
38 | * Generates a list of segments using information provided by the SegmentList element
39 | * SegmentList (DASH SPEC Section 5.3.9.3.2) contains a set of nodes. Each
40 | * node should be translated into segment.
41 | *
42 | * @param {Object} attributes
43 | * Object containing all inherited attributes from parent elements with attribute
44 | * names as keys
45 | * @param {Object[]|undefined} segmentTimeline
46 | * List of objects representing the attributes of each S element contained within
47 | * the SegmentTimeline element
48 | * @return {Object.} list of segments
49 | */
50 | export const segmentsFromList = (attributes, segmentTimeline) => {
51 | const {
52 | duration,
53 | segmentUrls = [],
54 | periodStart
55 | } = attributes;
56 |
57 | // Per spec (5.3.9.2.1) no way to determine segment duration OR
58 | // if both SegmentTimeline and @duration are defined, it is outside of spec.
59 | if ((!duration && !segmentTimeline) ||
60 | (duration && segmentTimeline)) {
61 | throw new Error(errors.SEGMENT_TIME_UNSPECIFIED);
62 | }
63 |
64 | const segmentUrlMap = segmentUrls.map(segmentUrlObject =>
65 | SegmentURLToSegmentObject(attributes, segmentUrlObject));
66 | let segmentTimeInfo;
67 |
68 | if (duration) {
69 | segmentTimeInfo = parseByDuration(attributes);
70 | }
71 |
72 | if (segmentTimeline) {
73 | segmentTimeInfo = parseByTimeline(attributes, segmentTimeline);
74 | }
75 |
76 | const segments = segmentTimeInfo.map((segmentTime, index) => {
77 | if (segmentUrlMap[index]) {
78 | const segment = segmentUrlMap[index];
79 | // See DASH spec section 5.3.9.2.2
80 | // - if timescale isn't present on any level, default to 1.
81 | const timescale = attributes.timescale || 1;
82 | // - if presentationTimeOffset isn't present on any level, default to 0
83 | const presentationTimeOffset = attributes.presentationTimeOffset || 0;
84 |
85 | segment.timeline = segmentTime.timeline;
86 | segment.duration = segmentTime.duration;
87 | segment.number = segmentTime.number;
88 | segment.presentationTime =
89 | periodStart + ((segmentTime.time - presentationTimeOffset) / timescale);
90 |
91 | return segment;
92 | }
93 | // Since we're mapping we should get rid of any blank segments (in case
94 | // the given SegmentTimeline is handling for more elements than we have
95 | // SegmentURLs for).
96 | }).filter(segment => segment);
97 |
98 | return segments;
99 | };
100 |
--------------------------------------------------------------------------------
/src/segment/segmentTemplate.js:
--------------------------------------------------------------------------------
1 | import resolveUrl from '@videojs/vhs-utils/es/resolve-url';
2 | import urlTypeToSegment from './urlType';
3 | import { parseByTimeline } from './timelineTimeParser';
4 | import { parseByDuration } from './durationTimeParser';
5 |
6 | const identifierPattern = /\$([A-z]*)(?:(%0)([0-9]+)d)?\$/g;
7 |
8 | /**
9 | * Replaces template identifiers with corresponding values. To be used as the callback
10 | * for String.prototype.replace
11 | *
12 | * @name replaceCallback
13 | * @function
14 | * @param {string} match
15 | * Entire match of identifier
16 | * @param {string} identifier
17 | * Name of matched identifier
18 | * @param {string} format
19 | * Format tag string. Its presence indicates that padding is expected
20 | * @param {string} width
21 | * Desired length of the replaced value. Values less than this width shall be left
22 | * zero padded
23 | * @return {string}
24 | * Replacement for the matched identifier
25 | */
26 |
27 | /**
28 | * Returns a function to be used as a callback for String.prototype.replace to replace
29 | * template identifiers
30 | *
31 | * @param {Obect} values
32 | * Object containing values that shall be used to replace known identifiers
33 | * @param {number} values.RepresentationID
34 | * Value of the Representation@id attribute
35 | * @param {number} values.Number
36 | * Number of the corresponding segment
37 | * @param {number} values.Bandwidth
38 | * Value of the Representation@bandwidth attribute.
39 | * @param {number} values.Time
40 | * Timestamp value of the corresponding segment
41 | * @return {replaceCallback}
42 | * Callback to be used with String.prototype.replace to replace identifiers
43 | */
44 | export const identifierReplacement = (values) => (match, identifier, format, width) => {
45 | if (match === '$$') {
46 | // escape sequence
47 | return '$';
48 | }
49 |
50 | if (typeof values[identifier] === 'undefined') {
51 | return match;
52 | }
53 |
54 | const value = '' + values[identifier];
55 |
56 | if (identifier === 'RepresentationID') {
57 | // Format tag shall not be present with RepresentationID
58 | return value;
59 | }
60 |
61 | if (!format) {
62 | width = 1;
63 | } else {
64 | width = parseInt(width, 10);
65 | }
66 |
67 | if (value.length >= width) {
68 | return value;
69 | }
70 |
71 | return `${(new Array(width - value.length + 1)).join('0')}${value}`;
72 | };
73 |
74 | /**
75 | * Constructs a segment url from a template string
76 | *
77 | * @param {string} url
78 | * Template string to construct url from
79 | * @param {Obect} values
80 | * Object containing values that shall be used to replace known identifiers
81 | * @param {number} values.RepresentationID
82 | * Value of the Representation@id attribute
83 | * @param {number} values.Number
84 | * Number of the corresponding segment
85 | * @param {number} values.Bandwidth
86 | * Value of the Representation@bandwidth attribute.
87 | * @param {number} values.Time
88 | * Timestamp value of the corresponding segment
89 | * @return {string}
90 | * Segment url with identifiers replaced
91 | */
92 | export const constructTemplateUrl = (url, values) =>
93 | url.replace(identifierPattern, identifierReplacement(values));
94 |
95 | /**
96 | * Generates a list of objects containing timing and duration information about each
97 | * segment needed to generate segment uris and the complete segment object
98 | *
99 | * @param {Object} attributes
100 | * Object containing all inherited attributes from parent elements with attribute
101 | * names as keys
102 | * @param {Object[]|undefined} segmentTimeline
103 | * List of objects representing the attributes of each S element contained within
104 | * the SegmentTimeline element
105 | * @return {{number: number, duration: number, time: number, timeline: number}[]}
106 | * List of Objects with segment timing and duration info
107 | */
108 | export const parseTemplateInfo = (attributes, segmentTimeline) => {
109 | if (!attributes.duration && !segmentTimeline) {
110 | // if neither @duration or SegmentTimeline are present, then there shall be exactly
111 | // one media segment
112 | return [{
113 | number: attributes.startNumber || 1,
114 | duration: attributes.sourceDuration,
115 | time: 0,
116 | timeline: attributes.periodStart
117 | }];
118 | }
119 |
120 | if (attributes.duration) {
121 | return parseByDuration(attributes);
122 | }
123 |
124 | return parseByTimeline(attributes, segmentTimeline);
125 | };
126 |
127 | /**
128 | * Generates a list of segments using information provided by the SegmentTemplate element
129 | *
130 | * @param {Object} attributes
131 | * Object containing all inherited attributes from parent elements with attribute
132 | * names as keys
133 | * @param {Object[]|undefined} segmentTimeline
134 | * List of objects representing the attributes of each S element contained within
135 | * the SegmentTimeline element
136 | * @return {Object[]}
137 | * List of segment objects
138 | */
139 | export const segmentsFromTemplate = (attributes, segmentTimeline) => {
140 | const templateValues = {
141 | RepresentationID: attributes.id,
142 | Bandwidth: attributes.bandwidth || 0
143 | };
144 |
145 | const { initialization = { sourceURL: '', range: '' } } = attributes;
146 |
147 | const mapSegment = urlTypeToSegment({
148 | baseUrl: attributes.baseUrl,
149 | source: constructTemplateUrl(initialization.sourceURL, templateValues),
150 | range: initialization.range
151 | });
152 |
153 | const segments = parseTemplateInfo(attributes, segmentTimeline);
154 |
155 | return segments.map(segment => {
156 | templateValues.Number = segment.number;
157 | templateValues.Time = segment.time;
158 |
159 | const uri = constructTemplateUrl(attributes.media || '', templateValues);
160 | // See DASH spec section 5.3.9.2.2
161 | // - if timescale isn't present on any level, default to 1.
162 | const timescale = attributes.timescale || 1;
163 | // - if presentationTimeOffset isn't present on any level, default to 0
164 | const presentationTimeOffset = attributes.presentationTimeOffset || 0;
165 | const presentationTime =
166 | // Even if the @t attribute is not specified for the segment, segment.time is
167 | // calculated in mpd-parser prior to this, so it's assumed to be available.
168 | attributes.periodStart + ((segment.time - presentationTimeOffset) / timescale);
169 |
170 | const map = {
171 | uri,
172 | timeline: segment.timeline,
173 | duration: segment.duration,
174 | resolvedUri: resolveUrl(attributes.baseUrl || '', uri),
175 | map: mapSegment,
176 | number: segment.number,
177 | presentationTime
178 | };
179 |
180 | return map;
181 | });
182 | };
183 |
--------------------------------------------------------------------------------
/src/segment/timelineTimeParser.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Calculates the R (repetition) value for a live stream (for the final segment
3 | * in a manifest where the r value is negative 1)
4 | *
5 | * @param {Object} attributes
6 | * Object containing all inherited attributes from parent elements with attribute
7 | * names as keys
8 | * @param {number} time
9 | * current time (typically the total time up until the final segment)
10 | * @param {number} duration
11 | * duration property for the given
12 | *
13 | * @return {number}
14 | * R value to reach the end of the given period
15 | */
16 | const getLiveRValue = (attributes, time, duration) => {
17 | const {
18 | NOW,
19 | clientOffset,
20 | availabilityStartTime,
21 | timescale = 1,
22 | periodStart = 0,
23 | minimumUpdatePeriod = 0
24 | } = attributes;
25 | const now = (NOW + clientOffset) / 1000;
26 | const periodStartWC = availabilityStartTime + periodStart;
27 | const periodEndWC = now + minimumUpdatePeriod;
28 | const periodDuration = periodEndWC - periodStartWC;
29 |
30 | return Math.ceil(((periodDuration * timescale) - time) / duration);
31 | };
32 |
33 | /**
34 | * Uses information provided by SegmentTemplate.SegmentTimeline to determine segment
35 | * timing and duration
36 | *
37 | * @param {Object} attributes
38 | * Object containing all inherited attributes from parent elements with attribute
39 | * names as keys
40 | * @param {Object[]} segmentTimeline
41 | * List of objects representing the attributes of each S element contained within
42 | *
43 | * @return {{number: number, duration: number, time: number, timeline: number}[]}
44 | * List of Objects with segment timing and duration info
45 | */
46 | export const parseByTimeline = (attributes, segmentTimeline) => {
47 | const {
48 | type,
49 | minimumUpdatePeriod = 0,
50 | media = '',
51 | sourceDuration,
52 | timescale = 1,
53 | startNumber = 1,
54 | periodStart: timeline
55 | } = attributes;
56 | const segments = [];
57 | let time = -1;
58 |
59 | for (let sIndex = 0; sIndex < segmentTimeline.length; sIndex++) {
60 | const S = segmentTimeline[sIndex];
61 | const duration = S.d;
62 | const repeat = S.r || 0;
63 | const segmentTime = S.t || 0;
64 |
65 | if (time < 0) {
66 | // first segment
67 | time = segmentTime;
68 | }
69 |
70 | if (segmentTime && segmentTime > time) {
71 | // discontinuity
72 |
73 | // TODO: How to handle this type of discontinuity
74 | // timeline++ here would treat it like HLS discontuity and content would
75 | // get appended without gap
76 | // E.G.
77 | //
78 | //
79 | //
80 | //
81 | // would have $Time$ values of [0, 1, 2, 5]
82 | // should this be appened at time positions [0, 1, 2, 3],(#EXT-X-DISCONTINUITY)
83 | // or [0, 1, 2, gap, gap, 5]? (#EXT-X-GAP)
84 | // does the value of sourceDuration consider this when calculating arbitrary
85 | // negative @r repeat value?
86 | // E.G. Same elements as above with this added at the end
87 | //
88 | // with a sourceDuration of 10
89 | // Would the 2 gaps be included in the time duration calculations resulting in
90 | // 8 segments with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9] or 10 segments
91 | // with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9, 10, 11] ?
92 |
93 | time = segmentTime;
94 | }
95 |
96 | let count;
97 |
98 | if (repeat < 0) {
99 | const nextS = sIndex + 1;
100 |
101 | if (nextS === segmentTimeline.length) {
102 | // last segment
103 | if (type === 'dynamic' &&
104 | minimumUpdatePeriod > 0 &&
105 | media.indexOf('$Number$') > 0) {
106 | count = getLiveRValue(attributes, time, duration);
107 | } else {
108 | // TODO: This may be incorrect depending on conclusion of TODO above
109 | count = ((sourceDuration * timescale) - time) / duration;
110 | }
111 | } else {
112 | count = (segmentTimeline[nextS].t - time) / duration;
113 | }
114 | } else {
115 | count = repeat + 1;
116 | }
117 |
118 | const end = startNumber + segments.length + count;
119 | let number = startNumber + segments.length;
120 |
121 | while (number < end) {
122 | segments.push({ number, duration: duration / timescale, time, timeline });
123 | time += duration;
124 | number++;
125 | }
126 | }
127 |
128 | return segments;
129 | };
130 |
--------------------------------------------------------------------------------
/src/segment/urlType.js:
--------------------------------------------------------------------------------
1 | import resolveUrl from '@videojs/vhs-utils/es/resolve-url';
2 | import window from 'global/window';
3 |
4 | /**
5 | * @typedef {Object} SingleUri
6 | * @property {string} uri - relative location of segment
7 | * @property {string} resolvedUri - resolved location of segment
8 | * @property {Object} byterange - Object containing information on how to make byte range
9 | * requests following byte-range-spec per RFC2616.
10 | * @property {String} byterange.length - length of range request
11 | * @property {String} byterange.offset - byte offset of range request
12 | *
13 | * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1
14 | */
15 |
16 | /**
17 | * Converts a URLType node (5.3.9.2.3 Table 13) to a segment object
18 | * that conforms to how m3u8-parser is structured
19 | *
20 | * @see https://github.com/videojs/m3u8-parser
21 | *
22 | * @param {string} baseUrl - baseUrl provided by nodes
23 | * @param {string} source - source url for segment
24 | * @param {string} range - optional range used for range calls,
25 | * follows RFC 2616, Clause 14.35.1
26 | * @return {SingleUri} full segment information transformed into a format similar
27 | * to m3u8-parser
28 | */
29 | export const urlTypeToSegment = ({ baseUrl = '', source = '', range = '', indexRange = '' }) => {
30 | const segment = {
31 | uri: source,
32 | resolvedUri: resolveUrl(baseUrl || '', source)
33 | };
34 |
35 | if (range || indexRange) {
36 | const rangeStr = range ? range : indexRange;
37 | const ranges = rangeStr.split('-');
38 |
39 | // default to parsing this as a BigInt if possible
40 | let startRange = window.BigInt ? window.BigInt(ranges[0]) : parseInt(ranges[0], 10);
41 | let endRange = window.BigInt ? window.BigInt(ranges[1]) : parseInt(ranges[1], 10);
42 |
43 | // convert back to a number if less than MAX_SAFE_INTEGER
44 | if (startRange < Number.MAX_SAFE_INTEGER && typeof startRange === 'bigint') {
45 | startRange = Number(startRange);
46 | }
47 |
48 | if (endRange < Number.MAX_SAFE_INTEGER && typeof endRange === 'bigint') {
49 | endRange = Number(endRange);
50 | }
51 |
52 | let length;
53 |
54 | if (typeof endRange === 'bigint' || typeof startRange === 'bigint') {
55 | length = window.BigInt(endRange) - window.BigInt(startRange) + window.BigInt(1);
56 | } else {
57 | length = endRange - startRange + 1;
58 | }
59 |
60 | if (typeof length === 'bigint' && length < Number.MAX_SAFE_INTEGER) {
61 | length = Number(length);
62 | }
63 |
64 | // byterange should be inclusive according to
65 | // RFC 2616, Clause 14.35.1
66 | segment.byterange = {
67 | length,
68 | offset: startRange
69 | };
70 | }
71 |
72 | return segment;
73 | };
74 |
75 | export const byteRangeToString = (byterange) => {
76 | // `endRange` is one less than `offset + length` because the HTTP range
77 | // header uses inclusive ranges
78 | let endRange;
79 |
80 | if (typeof byterange.offset === 'bigint' || typeof byterange.length === 'bigint') {
81 | endRange = window.BigInt(byterange.offset) + window.BigInt(byterange.length) - window.BigInt(1);
82 | } else {
83 | endRange = byterange.offset + byterange.length - 1;
84 | }
85 |
86 | return `${byterange.offset}-${endRange}`;
87 | };
88 |
89 | export default urlTypeToSegment;
90 |
--------------------------------------------------------------------------------
/src/stringToMpdXml.js:
--------------------------------------------------------------------------------
1 | import {DOMParser} from '@xmldom/xmldom';
2 | import errors from './errors';
3 |
4 | export const stringToMpdXml = (manifestString) => {
5 | if (manifestString === '') {
6 | throw new Error(errors.DASH_EMPTY_MANIFEST);
7 | }
8 |
9 | const parser = new DOMParser();
10 | let xml;
11 | let mpd;
12 |
13 | try {
14 | xml = parser.parseFromString(manifestString, 'application/xml');
15 | mpd = xml && xml.documentElement.tagName === 'MPD' ?
16 | xml.documentElement : null;
17 | } catch (e) {
18 | // ie 11 throws on invalid xml
19 | }
20 |
21 | if (!mpd || mpd &&
22 | mpd.getElementsByTagName('parsererror').length > 0) {
23 | throw new Error(errors.DASH_INVALID_XML);
24 | }
25 |
26 | return mpd;
27 | };
28 |
--------------------------------------------------------------------------------
/src/toPlaylists.js:
--------------------------------------------------------------------------------
1 | import { merge } from './utils/object';
2 | import { segmentsFromTemplate } from './segment/segmentTemplate';
3 | import { segmentsFromList } from './segment/segmentList';
4 | import { segmentsFromBase } from './segment/segmentBase';
5 |
6 | export const generateSegments = ({ attributes, segmentInfo }) => {
7 | let segmentAttributes;
8 | let segmentsFn;
9 |
10 | if (segmentInfo.template) {
11 | segmentsFn = segmentsFromTemplate;
12 | segmentAttributes = merge(attributes, segmentInfo.template);
13 | } else if (segmentInfo.base) {
14 | segmentsFn = segmentsFromBase;
15 | segmentAttributes = merge(attributes, segmentInfo.base);
16 | } else if (segmentInfo.list) {
17 | segmentsFn = segmentsFromList;
18 | segmentAttributes = merge(attributes, segmentInfo.list);
19 | }
20 |
21 | const segmentsInfo = {
22 | attributes
23 | };
24 |
25 | if (!segmentsFn) {
26 | return segmentsInfo;
27 | }
28 |
29 | const segments = segmentsFn(segmentAttributes, segmentInfo.segmentTimeline);
30 |
31 | // The @duration attribute will be used to determin the playlist's targetDuration which
32 | // must be in seconds. Since we've generated the segment list, we no longer need
33 | // @duration to be in @timescale units, so we can convert it here.
34 | if (segmentAttributes.duration) {
35 | const { duration, timescale = 1 } = segmentAttributes;
36 |
37 | segmentAttributes.duration = duration / timescale;
38 | } else if (segments.length) {
39 | // if there is no @duration attribute, use the largest segment duration as
40 | // as target duration
41 | segmentAttributes.duration = segments.reduce((max, segment) => {
42 | return Math.max(max, Math.ceil(segment.duration));
43 | }, 0);
44 | } else {
45 | segmentAttributes.duration = 0;
46 | }
47 |
48 | segmentsInfo.attributes = segmentAttributes;
49 | segmentsInfo.segments = segments;
50 |
51 | // This is a sidx box without actual segment information
52 | if (segmentInfo.base && segmentAttributes.indexRange) {
53 | segmentsInfo.sidx = segments[0];
54 | segmentsInfo.segments = [];
55 | }
56 |
57 | return segmentsInfo;
58 | };
59 |
60 | export const toPlaylists = (representations) => representations.map(generateSegments);
61 |
--------------------------------------------------------------------------------
/src/utils/list.js:
--------------------------------------------------------------------------------
1 | import { values } from './object';
2 |
3 | export const range = (start, end) => {
4 | const result = [];
5 |
6 | for (let i = start; i < end; i++) {
7 | result.push(i);
8 | }
9 |
10 | return result;
11 | };
12 |
13 | export const flatten = lists => lists.reduce((x, y) => x.concat(y), []);
14 |
15 | export const from = list => {
16 | if (!list.length) {
17 | return [];
18 | }
19 |
20 | const result = [];
21 |
22 | for (let i = 0; i < list.length; i++) {
23 | result.push(list[i]);
24 | }
25 |
26 | return result;
27 | };
28 |
29 | export const findIndexes = (l, key) => l.reduce((a, e, i) => {
30 | if (e[key]) {
31 | a.push(i);
32 | }
33 |
34 | return a;
35 | }, []);
36 |
37 | /**
38 | * Returns a union of the included lists provided each element can be identified by a key.
39 | *
40 | * @param {Array} list - list of lists to get the union of
41 | * @param {Function} keyFunction - the function to use as a key for each element
42 | *
43 | * @return {Array} the union of the arrays
44 | */
45 | export const union = (lists, keyFunction) => {
46 | return values(lists.reduce((acc, list) => {
47 | list.forEach((el) => {
48 | acc[keyFunction(el)] = el;
49 | });
50 |
51 | return acc;
52 | }, {}));
53 | };
54 |
--------------------------------------------------------------------------------
/src/utils/object.js:
--------------------------------------------------------------------------------
1 | const isObject = (obj) => {
2 | return !!obj && typeof obj === 'object';
3 | };
4 |
5 | export const merge = (...objects) => {
6 |
7 | return objects.reduce((result, source) => {
8 |
9 | if (typeof source !== 'object') {
10 | return result;
11 | }
12 |
13 | Object.keys(source).forEach(key => {
14 |
15 | if (Array.isArray(result[key]) && Array.isArray(source[key])) {
16 | result[key] = result[key].concat(source[key]);
17 | } else if (isObject(result[key]) && isObject(source[key])) {
18 | result[key] = merge(result[key], source[key]);
19 | } else {
20 | result[key] = source[key];
21 | }
22 | });
23 | return result;
24 | }, {});
25 | };
26 |
27 | export const values = o => Object.keys(o).map(k => o[k]);
28 |
--------------------------------------------------------------------------------
/src/utils/string.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Converts the provided string that may contain a division operation to a number.
3 | *
4 | * @param {string} value - the provided string value
5 | *
6 | * @return {number} the parsed string value
7 | */
8 | export const parseDivisionValue = (value) => {
9 | return parseFloat(value.split('/').reduce((prev, current) => prev / current));
10 | };
11 |
--------------------------------------------------------------------------------
/src/utils/time.js:
--------------------------------------------------------------------------------
1 | export const parseDuration = (str) => {
2 | const SECONDS_IN_YEAR = 365 * 24 * 60 * 60;
3 | const SECONDS_IN_MONTH = 30 * 24 * 60 * 60;
4 | const SECONDS_IN_DAY = 24 * 60 * 60;
5 | const SECONDS_IN_HOUR = 60 * 60;
6 | const SECONDS_IN_MIN = 60;
7 |
8 | // P10Y10M10DT10H10M10.1S
9 | const durationRegex =
10 | /P(?:(\d*)Y)?(?:(\d*)M)?(?:(\d*)D)?(?:T(?:(\d*)H)?(?:(\d*)M)?(?:([\d.]*)S)?)?/;
11 | const match = durationRegex.exec(str);
12 |
13 | if (!match) {
14 | return 0;
15 | }
16 |
17 | const [year, month, day, hour, minute, second] = match.slice(1);
18 |
19 | return (parseFloat(year || 0) * SECONDS_IN_YEAR +
20 | parseFloat(month || 0) * SECONDS_IN_MONTH +
21 | parseFloat(day || 0) * SECONDS_IN_DAY +
22 | parseFloat(hour || 0) * SECONDS_IN_HOUR +
23 | parseFloat(minute || 0) * SECONDS_IN_MIN +
24 | parseFloat(second || 0));
25 | };
26 |
27 | export const parseDate = (str) => {
28 | // Date format without timezone according to ISO 8601
29 | // YYY-MM-DDThh:mm:ss.ssssss
30 | const dateRegex = /^\d+-\d+-\d+T\d+:\d+:\d+(\.\d+)?$/;
31 |
32 | // If the date string does not specifiy a timezone, we must specifiy UTC. This is
33 | // expressed by ending with 'Z'
34 | if (dateRegex.test(str)) {
35 | str += 'Z';
36 | }
37 |
38 | return Date.parse(str);
39 | };
40 |
--------------------------------------------------------------------------------
/src/utils/xml.js:
--------------------------------------------------------------------------------
1 | import { from } from './list';
2 |
3 | export const findChildren = (element, name) =>
4 | from(element.childNodes).filter(({tagName}) => tagName === name);
5 |
6 | export const getContent = element => element.textContent.trim();
7 |
--------------------------------------------------------------------------------
/test/index.test.js:
--------------------------------------------------------------------------------
1 | import { parse, VERSION } from '../src';
2 | import QUnit from 'qunit';
3 |
4 | QUnit.dump.maxDepth = Infinity;
5 |
6 | // manifests
7 | import vttCodecsTemplate from './manifests/vtt_codecs.mpd';
8 | import maatVttSegmentTemplate from './manifests/maat_vtt_segmentTemplate.mpd';
9 | import segmentBaseTemplate from './manifests/segmentBase.mpd';
10 | import segmentListTemplate from './manifests/segmentList.mpd';
11 | import cc608CaptionsTemplate from './manifests/608-captions.mpd';
12 | import cc708CaptionsTemplate from './manifests/708-captions.mpd';
13 | import locationTemplate from './manifests/location.mpd';
14 | import locationsTemplate from './manifests/locations.mpd';
15 | import multiperiod from './manifests/multiperiod.mpd';
16 | import webmsegments from './manifests/webmsegments.mpd';
17 | import multiperiodSegmentTemplate from './manifests/multiperiod-segment-template.mpd';
18 | import multiperiodSegmentList from './manifests/multiperiod-segment-list.mpd';
19 | import multiperiodDynamic from './manifests/multiperiod-dynamic.mpd';
20 | import audioOnly from './manifests/audio-only.mpd';
21 | import multiperiodStartnumber from './manifests/multiperiod-startnumber.mpd';
22 | import multiperiodStartnumberRemovedPeriods from
23 | './manifests/multiperiod-startnumber-removed-periods.mpd';
24 | import {
25 | parsedManifest as maatVttSegmentTemplateManifest
26 | } from './manifests/maat_vtt_segmentTemplate.js';
27 | import {
28 | parsedManifest as segmentBaseManifest
29 | } from './manifests/segmentBase.js';
30 | import {
31 | parsedManifest as segmentListManifest
32 | } from './manifests/segmentList.js';
33 | import {
34 | parsedManifest as cc608CaptionsManifest
35 | } from './manifests/608-captions.js';
36 | import {
37 | parsedManifest as cc708CaptionsManifest
38 | } from './manifests/708-captions.js';
39 | import {
40 | parsedManifest as multiperiodManifest
41 | } from './manifests/multiperiod.js';
42 | import {
43 | parsedManifest as webmsegmentsManifest
44 | } from './manifests/webmsegments.js';
45 | import {
46 | parsedManifest as multiperiodSegmentTemplateManifest
47 | } from './manifests/multiperiod-segment-template.js';
48 | import {
49 | parsedManifest as multiperiodSegmentListManifest
50 | } from './manifests/multiperiod-segment-list.js';
51 | import {
52 | parsedManifest as multiperiodDynamicManifest
53 | } from './manifests/multiperiod-dynamic.js';
54 | import {
55 | parsedManifest as locationManifest
56 | } from './manifests/location.js';
57 | import {
58 | parsedManifest as locationsManifest
59 | } from './manifests/locations.js';
60 |
61 | import {
62 | parsedManifest as vttCodecsManifest
63 | } from './manifests/vtt_codecs.js';
64 |
65 | import {
66 | parsedManifest as audioOnlyManifest
67 | } from './manifests/audio-only.js';
68 | import {
69 | parsedManifest as multiperiodStartnumberManifest
70 | } from './manifests/multiperiod-startnumber.js';
71 | import {
72 | parsedManifest as multiperiodStartnumberRemovedPeriodsManifest
73 | } from './manifests/multiperiod-startnumber-removed-periods.js';
74 |
75 | QUnit.module('mpd-parser');
76 |
77 | QUnit.test('has VERSION', function(assert) {
78 | assert.ok(VERSION);
79 | });
80 |
81 | QUnit.test('has parse', function(assert) {
82 | assert.ok(parse);
83 | });
84 |
85 | [{
86 | name: 'maat_vtt_segmentTemplate',
87 | input: maatVttSegmentTemplate,
88 | expected: maatVttSegmentTemplateManifest
89 | }, {
90 | name: 'segmentBase',
91 | input: segmentBaseTemplate,
92 | expected: segmentBaseManifest
93 | }, {
94 | name: 'segmentList',
95 | input: segmentListTemplate,
96 | expected: segmentListManifest
97 | }, {
98 | name: '608-captions',
99 | input: cc608CaptionsTemplate,
100 | expected: cc608CaptionsManifest
101 | }, {
102 | name: '708-captions',
103 | input: cc708CaptionsTemplate,
104 | expected: cc708CaptionsManifest
105 | }, {
106 | name: 'multiperiod',
107 | input: multiperiod,
108 | expected: multiperiodManifest
109 | }, {
110 | name: 'webmsegments',
111 | input: webmsegments,
112 | expected: webmsegmentsManifest
113 | }, {
114 | name: 'multiperiod_segment_template',
115 | input: multiperiodSegmentTemplate,
116 | expected: multiperiodSegmentTemplateManifest
117 | }, {
118 | name: 'multiperiod_segment_list',
119 | input: multiperiodSegmentList,
120 | expected: multiperiodSegmentListManifest
121 | }, {
122 | name: 'multiperiod_dynamic',
123 | input: multiperiodDynamic,
124 | expected: multiperiodDynamicManifest
125 | }, {
126 | name: 'location',
127 | input: locationTemplate,
128 | expected: locationManifest
129 | }, {
130 | name: 'locations',
131 | input: locationsTemplate,
132 | expected: locationsManifest
133 | }, {
134 | name: 'vtt_codecs',
135 | input: vttCodecsTemplate,
136 | expected: vttCodecsManifest
137 | }, {
138 | name: 'audio-only',
139 | input: audioOnly,
140 | expected: audioOnlyManifest
141 | }, {
142 | name: 'multiperiod_startnumber',
143 | input: multiperiodStartnumber,
144 | expected: multiperiodStartnumberManifest
145 | }].forEach(({ name, input, expected }) => {
146 | QUnit.test(`${name} test manifest`, function(assert) {
147 | const actual = parse(input);
148 |
149 | assert.deepEqual(actual, expected);
150 | });
151 | });
152 |
153 | // this test is handled separately as a `previousManifest` needs to be parsed and provided
154 | QUnit.test('multiperiod_startnumber_removed_periods test manifest', function(assert) {
155 | const previousManifest = parse(multiperiodStartnumber);
156 | const actual = parse(multiperiodStartnumberRemovedPeriods, { previousManifest });
157 |
158 | assert.deepEqual(actual, multiperiodStartnumberRemovedPeriodsManifest);
159 | });
160 |
--------------------------------------------------------------------------------
/test/manifests/608-captions.js:
--------------------------------------------------------------------------------
1 | export const parsedManifest = {
2 | allowCache: true,
3 | discontinuityStarts: [],
4 | duration: 6,
5 | endList: true,
6 | mediaGroups: {
7 | 'AUDIO': {},
8 | 'CLOSED-CAPTIONS': {
9 | cc: {
10 | eng: {
11 | autoselect: false,
12 | default: false,
13 | instreamId: 'CC1',
14 | language: 'eng'
15 | },
16 | swe: {
17 | autoselect: false,
18 | default: false,
19 | instreamId: 'CC3',
20 | language: 'swe'
21 | },
22 | Hello: {
23 | autoselect: false,
24 | default: false,
25 | instreamId: 'CC4',
26 | language: 'Hello'
27 | }
28 | }
29 | },
30 | 'SUBTITLES': {},
31 | 'VIDEO': {}
32 | },
33 | playlists: [
34 | {
35 | attributes: {
36 | 'AUDIO': 'audio',
37 | 'BANDWIDTH': 449000,
38 | 'CODECS': 'avc1.420015',
39 | 'FRAME-RATE': 23.976,
40 | 'NAME': '482',
41 | 'PROGRAM-ID': 1,
42 | 'RESOLUTION': {
43 | height: 270,
44 | width: 482
45 | },
46 | 'SUBTITLES': 'subs'
47 | },
48 | endList: true,
49 | resolvedUri: 'https://www.example.com/1080p.ts',
50 | targetDuration: 6,
51 | mediaSequence: 0,
52 | timelineStarts: [{ start: 0, timeline: 0 }],
53 | discontinuitySequence: 0,
54 | discontinuityStarts: [],
55 | segments: [
56 | {
57 | duration: 6,
58 | timeline: 0,
59 | number: 0,
60 | presentationTime: 0,
61 | map: {
62 | uri: '',
63 | resolvedUri: 'https://www.example.com/1080p.ts'
64 | },
65 | resolvedUri: 'https://www.example.com/1080p.ts',
66 | uri: 'https://www.example.com/1080p.ts'
67 | }
68 | ],
69 | timeline: 0,
70 | uri: ''
71 | }
72 | ],
73 | segments: [],
74 | timelineStarts: [{ start: 0, timeline: 0 }],
75 | uri: ''
76 | };
77 |
--------------------------------------------------------------------------------
/test/manifests/608-captions.mpd:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://www.example.com/base
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | 1080p.ts
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/test/manifests/708-captions.js:
--------------------------------------------------------------------------------
1 | export const parsedManifest = {
2 | allowCache: true,
3 | discontinuityStarts: [],
4 | duration: 6,
5 | endList: true,
6 | mediaGroups: {
7 | 'AUDIO': {},
8 | 'CLOSED-CAPTIONS': {
9 | cc: {
10 | // eng: {
11 | // autoselect: false,
12 | // default: false,
13 | // instreamId: '1',
14 | // language: 'eng',
15 | // aspectRatio: 1,
16 | // easyReader: 0,
17 | // '3D': 0
18 | // },
19 | // TODO only this one ends up being represented and not both
20 | eng: {
21 | 'autoselect': false,
22 | 'default': false,
23 | 'instreamId': 'SERVICE2',
24 | 'language': 'eng',
25 | 'aspectRatio': 1,
26 | 'easyReader': 1,
27 | '3D': 0
28 | }
29 | }
30 | },
31 | 'SUBTITLES': {},
32 | 'VIDEO': {}
33 | },
34 | playlists: [
35 | {
36 | attributes: {
37 | 'AUDIO': 'audio',
38 | 'BANDWIDTH': 449000,
39 | 'CODECS': 'avc1.420015',
40 | 'FRAME-RATE': 23.976,
41 | 'NAME': '482',
42 | 'PROGRAM-ID': 1,
43 | 'RESOLUTION': {
44 | height: 270,
45 | width: 482
46 | },
47 | 'SUBTITLES': 'subs'
48 | },
49 | endList: true,
50 | resolvedUri: 'https://www.example.com/1080p.ts',
51 | targetDuration: 6,
52 | mediaSequence: 0,
53 | timelineStarts: [{ start: 0, timeline: 0 }],
54 | discontinuitySequence: 0,
55 | discontinuityStarts: [],
56 | segments: [
57 | {
58 | duration: 6,
59 | timeline: 0,
60 | number: 0,
61 | presentationTime: 0,
62 | map: {
63 | uri: '',
64 | resolvedUri: 'https://www.example.com/1080p.ts'
65 | },
66 | resolvedUri: 'https://www.example.com/1080p.ts',
67 | uri: 'https://www.example.com/1080p.ts'
68 | }
69 | ],
70 | timeline: 0,
71 | uri: ''
72 | }
73 | ],
74 | segments: [],
75 | timelineStarts: [{ start: 0, timeline: 0 }],
76 | uri: ''
77 | };
78 |
--------------------------------------------------------------------------------
/test/manifests/708-captions.mpd:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://www.example.com/base
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | 1080p.ts
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/test/manifests/audio-only.js:
--------------------------------------------------------------------------------
1 | export const parsedManifest = {
2 | allowCache: true,
3 | discontinuityStarts: [],
4 | segments: [],
5 | timelineStarts: [{
6 | start: 0,
7 | timeline: 0
8 | }],
9 | endList: true,
10 | mediaGroups: {
11 | 'AUDIO': {
12 | audio: {
13 | en: {
14 | language: 'en',
15 | autoselect: true,
16 | default: true,
17 | playlists: [
18 | {
19 | attributes: {
20 | 'NAME': '0',
21 | 'BANDWIDTH': 130803,
22 | 'CODECS': 'mp4a.40.2',
23 | 'PROGRAM-ID': 1,
24 | 'AUDIO': 'audio',
25 | 'SUBTITLES': 'subs'
26 | },
27 | uri: '',
28 | endList: true,
29 | timeline: 0,
30 | resolvedUri: 'http://example.com/audio_en_2c_128k_aac.mp4',
31 | targetDuration: 60,
32 | segments: [],
33 | mediaSequence: 0,
34 | discontinuitySequence: 0,
35 | discontinuityStarts: [],
36 | timelineStarts: [{
37 | start: 0,
38 | timeline: 0
39 | }],
40 | sidx: {
41 | uri: 'http://example.com/audio_en_2c_128k_aac.mp4',
42 | resolvedUri: 'http://example.com/audio_en_2c_128k_aac.mp4',
43 | byterange: {
44 | length: 224,
45 | offset: 786
46 | },
47 | map: {
48 | uri: '',
49 | resolvedUri: 'http://example.com/audio_en_2c_128k_aac.mp4',
50 | byterange: {
51 | length: 786,
52 | offset: 0
53 | }
54 | },
55 | duration: 60,
56 | timeline: 0,
57 | presentationTime: 0,
58 | number: 0
59 | }
60 | }
61 | ],
62 | uri: ''
63 | },
64 | es: {
65 | language: 'es',
66 | autoselect: true,
67 | default: false,
68 | playlists: [
69 | {
70 | attributes: {
71 | 'NAME': '1',
72 | 'BANDWIDTH': 130405,
73 | 'CODECS': 'mp4a.40.2',
74 | 'PROGRAM-ID': 1,
75 | 'AUDIO': 'audio',
76 | 'SUBTITLES': 'subs'
77 | },
78 | uri: '',
79 | endList: true,
80 | timeline: 0,
81 | resolvedUri: 'http://example.com/audio_es_2c_128k_aac.mp4',
82 | targetDuration: 60,
83 | segments: [],
84 | mediaSequence: 0,
85 | discontinuitySequence: 0,
86 | discontinuityStarts: [],
87 | timelineStarts: [{
88 | start: 0,
89 | timeline: 0
90 | }],
91 | sidx: {
92 | uri: 'http://example.com/audio_es_2c_128k_aac.mp4',
93 | resolvedUri: 'http://example.com/audio_es_2c_128k_aac.mp4',
94 | byterange: {
95 | length: 224,
96 | offset: 786
97 | },
98 | map: {
99 | uri: '',
100 | resolvedUri: 'http://example.com/audio_es_2c_128k_aac.mp4',
101 | byterange: {
102 | length: 786,
103 | offset: 0
104 | }
105 | },
106 | duration: 60,
107 | timeline: 0,
108 | presentationTime: 0,
109 | number: 0
110 | }
111 | }
112 | ],
113 | uri: ''
114 | }
115 | }
116 | },
117 | 'VIDEO': {},
118 | 'CLOSED-CAPTIONS': {},
119 | 'SUBTITLES': {}
120 | },
121 | uri: '',
122 | duration: 60,
123 | playlists: []
124 | };
125 |
--------------------------------------------------------------------------------
/test/manifests/audio-only.mpd:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | http://example.com/audio_en_2c_128k_aac.mp4
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | http://example.com/audio_es_2c_128k_aac.mp4
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/test/manifests/location.js:
--------------------------------------------------------------------------------
1 | export const parsedManifest = {
2 | locations: [
3 | 'https://www.example.com/updates'
4 | ],
5 | allowCache: true,
6 | discontinuityStarts: [],
7 | timelineStarts: [{
8 | start: 0,
9 | timeline: 0
10 | }],
11 | duration: 6,
12 | endList: true,
13 | mediaGroups: {
14 | 'AUDIO': {},
15 | 'CLOSED-CAPTIONS': {},
16 | 'SUBTITLES': {},
17 | 'VIDEO': {}
18 | },
19 | playlists: [
20 | {
21 | attributes: {
22 | 'AUDIO': 'audio',
23 | 'BANDWIDTH': 449000,
24 | 'CODECS': 'avc1.420015',
25 | 'FRAME-RATE': 23.976,
26 | 'NAME': '482',
27 | 'PROGRAM-ID': 1,
28 | 'RESOLUTION': {
29 | height: 270,
30 | width: 482
31 | },
32 | 'SUBTITLES': 'subs'
33 | },
34 | endList: true,
35 | resolvedUri: 'https://www.example.com/1080p.ts',
36 | targetDuration: 6,
37 | mediaSequence: 0,
38 | discontinuitySequence: 0,
39 | discontinuityStarts: [],
40 | timelineStarts: [{
41 | start: 0,
42 | timeline: 0
43 | }],
44 | segments: [
45 | {
46 | duration: 6,
47 | timeline: 0,
48 | number: 0,
49 | presentationTime: 0,
50 | map: {
51 | uri: '',
52 | resolvedUri: 'https://www.example.com/1080p.ts'
53 | },
54 | resolvedUri: 'https://www.example.com/1080p.ts',
55 | uri: 'https://www.example.com/1080p.ts'
56 | }
57 | ],
58 | timeline: 0,
59 | uri: ''
60 | }
61 | ],
62 | segments: [],
63 | uri: ''
64 | };
65 |
--------------------------------------------------------------------------------
/test/manifests/location.mpd:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://www.example.com/base
4 | https://www.example.com/updates
5 |
6 |
7 |
8 |
9 |
10 |
11 | 1080p.ts
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/test/manifests/locations.js:
--------------------------------------------------------------------------------
1 | export const parsedManifest = {
2 | locations: [
3 | 'https://www.example.com/updates',
4 | 'https://www.example.com/updates2'
5 | ],
6 | allowCache: true,
7 | discontinuityStarts: [],
8 | timelineStarts: [{
9 | start: 0,
10 | timeline: 0
11 | }],
12 | duration: 6,
13 | endList: true,
14 | mediaGroups: {
15 | 'AUDIO': {},
16 | 'CLOSED-CAPTIONS': {},
17 | 'SUBTITLES': {},
18 | 'VIDEO': {}
19 | },
20 | playlists: [
21 | {
22 | attributes: {
23 | 'AUDIO': 'audio',
24 | 'BANDWIDTH': 449000,
25 | 'CODECS': 'avc1.420015',
26 | 'FRAME-RATE': 23.976,
27 | 'NAME': '482',
28 | 'PROGRAM-ID': 1,
29 | 'RESOLUTION': {
30 | height: 270,
31 | width: 482
32 | },
33 | 'SUBTITLES': 'subs'
34 | },
35 | endList: true,
36 | resolvedUri: 'https://www.example.com/1080p.ts',
37 | targetDuration: 6,
38 | mediaSequence: 0,
39 | discontinuitySequence: 0,
40 | discontinuityStarts: [],
41 | timelineStarts: [{
42 | start: 0,
43 | timeline: 0
44 | }],
45 | segments: [
46 | {
47 | duration: 6,
48 | timeline: 0,
49 | number: 0,
50 | presentationTime: 0,
51 | map: {
52 | uri: '',
53 | resolvedUri: 'https://www.example.com/1080p.ts'
54 | },
55 | resolvedUri: 'https://www.example.com/1080p.ts',
56 | uri: 'https://www.example.com/1080p.ts'
57 | }
58 | ],
59 | timeline: 0,
60 | uri: ''
61 | }
62 | ],
63 | segments: [],
64 | uri: ''
65 | };
66 |
--------------------------------------------------------------------------------
/test/manifests/locations.mpd:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://www.example.com/base
4 | https://www.example.com/updates
5 | https://www.example.com/updates2
6 |
7 |
8 |
9 |
10 |
11 |
12 | 1080p.ts
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/test/manifests/maat_vtt_segmentTemplate.mpd:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://www.example.com/base
4 |
5 |
6 |
7 |
8 |
9 | test
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | test
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | test
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | https://example.com/en.vtt
51 |
52 |
53 |
54 |
55 | https://example.com/es.vtt
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/test/manifests/multiperiod-dynamic.mpd:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | test
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | test
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | test
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | test
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
--------------------------------------------------------------------------------
/test/manifests/multiperiod-segment-list.js:
--------------------------------------------------------------------------------
1 | export const parsedManifest = {
2 | allowCache: true,
3 | discontinuityStarts: [],
4 | duration: 12,
5 | endList: true,
6 | mediaGroups: {
7 | 'AUDIO': {},
8 | 'CLOSED-CAPTIONS': {},
9 | 'SUBTITLES': {},
10 | 'VIDEO': {}
11 | },
12 | playlists: [
13 | {
14 | attributes: {
15 | 'AUDIO': 'audio',
16 | 'BANDWIDTH': 449000,
17 | 'CODECS': 'avc1.420015',
18 | 'FRAME-RATE': 23.976,
19 | 'NAME': '482',
20 | 'PROGRAM-ID': 1,
21 | 'RESOLUTION': {
22 | height: 270,
23 | width: 482
24 | },
25 | 'SUBTITLES': 'subs'
26 | },
27 | endList: true,
28 | mediaSequence: 0,
29 | discontinuitySequence: 0,
30 | discontinuityStarts: [2],
31 | timelineStarts: [{
32 | start: 0,
33 | timeline: 0
34 | }, {
35 | start: 6,
36 | timeline: 6
37 | }],
38 | targetDuration: 3,
39 | resolvedUri: 'https://www.example.com/base',
40 | segments: [
41 | {
42 | duration: 3,
43 | map: {
44 | uri: '',
45 | resolvedUri: 'https://www.example.com/base'
46 | },
47 | resolvedUri: 'https://www.example.com/low/segment-1.ts',
48 | timeline: 0,
49 | presentationTime: 0,
50 | uri: 'low/segment-1.ts',
51 | number: 0
52 | },
53 | {
54 | duration: 3,
55 | map: {
56 | uri: '',
57 | resolvedUri: 'https://www.example.com/base'
58 | },
59 | resolvedUri: 'https://www.example.com/low/segment-2.ts',
60 | timeline: 0,
61 | presentationTime: 3,
62 | uri: 'low/segment-2.ts',
63 | number: 1
64 | },
65 | {
66 | discontinuity: true,
67 | duration: 3,
68 | map: {
69 | uri: '',
70 | resolvedUri: 'https://www.example.com/base'
71 | },
72 | resolvedUri: 'https://www.example.com/low/segment-1.ts',
73 | timeline: 6,
74 | presentationTime: 6,
75 | uri: 'low/segment-1.ts',
76 | number: 2
77 | },
78 | {
79 | duration: 3,
80 | map: {
81 | uri: '',
82 | resolvedUri: 'https://www.example.com/base'
83 | },
84 | resolvedUri: 'https://www.example.com/low/segment-2.ts',
85 | timeline: 6,
86 | presentationTime: 9,
87 | uri: 'low/segment-2.ts',
88 | number: 3
89 | }
90 | ],
91 | timeline: 0,
92 | uri: ''
93 | }
94 | ],
95 | segments: [],
96 | timelineStarts: [{
97 | start: 0,
98 | timeline: 0
99 | }, {
100 | start: 6,
101 | timeline: 6
102 | }],
103 | uri: ''
104 | };
105 |
--------------------------------------------------------------------------------
/test/manifests/multiperiod-segment-list.mpd:
--------------------------------------------------------------------------------
1 |
2 |
8 | https://www.example.com/base
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/test/manifests/multiperiod-segment-template.js:
--------------------------------------------------------------------------------
1 | export const parsedManifest = {
2 | allowCache: true,
3 | uri: '',
4 | duration: 30,
5 | discontinuityStarts: [],
6 | segments: [],
7 | timelineStarts: [{
8 | start: 0,
9 | timeline: 0
10 | }, {
11 | start: 15,
12 | timeline: 15
13 | }],
14 | endList: true,
15 | mediaGroups: {
16 | 'AUDIO': {
17 | audio: {
18 | en: {
19 | language: 'en',
20 | autoselect: true,
21 | default: true,
22 | playlists: [
23 | {
24 | attributes: {
25 | 'NAME': '2',
26 | 'BANDWIDTH': 32000,
27 | 'CODECS': 'mp4a.40.2',
28 | 'PROGRAM-ID': 1
29 | },
30 | uri: '',
31 | endList: true,
32 | timeline: 0,
33 | resolvedUri: 'https://www.example.com/base',
34 | targetDuration: 5,
35 | segments: [
36 | {
37 | uri: 'audio/segment_0.m4f',
38 | timeline: 0,
39 | duration: 5,
40 | resolvedUri: 'https://www.example.com/audio/segment_0.m4f',
41 | map: {
42 | uri: 'audio/init.m4f',
43 | resolvedUri: 'https://www.example.com/audio/init.m4f'
44 | },
45 | number: 0,
46 | presentationTime: 0
47 | },
48 | {
49 | uri: 'audio/segment_1.m4f',
50 | timeline: 0,
51 | duration: 5,
52 | resolvedUri: 'https://www.example.com/audio/segment_1.m4f',
53 | map: {
54 | uri: 'audio/init.m4f',
55 | resolvedUri: 'https://www.example.com/audio/init.m4f'
56 | },
57 | number: 1,
58 | presentationTime: 5
59 | },
60 | {
61 | uri: 'audio/segment_2.m4f',
62 | timeline: 0,
63 | duration: 5,
64 | resolvedUri: 'https://www.example.com/audio/segment_2.m4f',
65 | map: {
66 | uri: 'audio/init.m4f',
67 | resolvedUri: 'https://www.example.com/audio/init.m4f'
68 | },
69 | number: 2,
70 | presentationTime: 10
71 | },
72 | {
73 | discontinuity: true,
74 | uri: 'audio/segment_0.m4f',
75 | timeline: 15,
76 | duration: 5,
77 | resolvedUri: 'https://www.example.com/audio/segment_0.m4f',
78 | map: {
79 | uri: 'audio/init.m4f',
80 | resolvedUri: 'https://www.example.com/audio/init.m4f'
81 | },
82 | number: 3,
83 | presentationTime: 15
84 | },
85 | {
86 | uri: 'audio/segment_1.m4f',
87 | timeline: 15,
88 | duration: 5,
89 | resolvedUri: 'https://www.example.com/audio/segment_1.m4f',
90 | map: {
91 | uri: 'audio/init.m4f',
92 | resolvedUri: 'https://www.example.com/audio/init.m4f'
93 | },
94 | number: 4,
95 | presentationTime: 20
96 | },
97 | {
98 | uri: 'audio/segment_2.m4f',
99 | timeline: 15,
100 | duration: 5,
101 | resolvedUri: 'https://www.example.com/audio/segment_2.m4f',
102 | map: {
103 | uri: 'audio/init.m4f',
104 | resolvedUri: 'https://www.example.com/audio/init.m4f'
105 | },
106 | number: 5,
107 | presentationTime: 25
108 | }
109 | ],
110 | mediaSequence: 0,
111 | discontinuitySequence: 0,
112 | discontinuityStarts: [3],
113 | timelineStarts: [{
114 | start: 0,
115 | timeline: 0
116 | }, {
117 | start: 15,
118 | timeline: 15
119 | }]
120 | }
121 | ],
122 | uri: ''
123 | }
124 | }
125 | },
126 | 'VIDEO': {},
127 | 'CLOSED-CAPTIONS': {},
128 | 'SUBTITLES': {}
129 | },
130 | playlists: [
131 | {
132 | attributes: {
133 | 'AUDIO': 'audio',
134 | 'BANDWIDTH': 100000,
135 | 'CODECS': 'avc1.4d001f',
136 | 'FRAME-RATE': 24,
137 | 'NAME': '1',
138 | 'PROGRAM-ID': 1,
139 | 'RESOLUTION': {
140 | height: 200,
141 | width: 480
142 | },
143 | 'SUBTITLES': 'subs'
144 | },
145 | uri: '',
146 | endList: true,
147 | timeline: 0,
148 | resolvedUri: 'https://www.example.com/base',
149 | targetDuration: 5,
150 | segments: [
151 | {
152 | uri: 'video/segment_0.m4f',
153 | timeline: 0,
154 | duration: 5,
155 | resolvedUri: 'https://www.example.com/video/segment_0.m4f',
156 | map: {
157 | uri: 'video/init.m4f',
158 | resolvedUri: 'https://www.example.com/video/init.m4f'
159 | },
160 | number: 0,
161 | presentationTime: 0
162 | },
163 | {
164 | uri: 'video/segment_1.m4f',
165 | timeline: 0,
166 | duration: 5,
167 | resolvedUri: 'https://www.example.com/video/segment_1.m4f',
168 | map: {
169 | uri: 'video/init.m4f',
170 | resolvedUri: 'https://www.example.com/video/init.m4f'
171 | },
172 | number: 1,
173 | presentationTime: 5
174 | },
175 | {
176 | uri: 'video/segment_2.m4f',
177 | timeline: 0,
178 | duration: 5,
179 | resolvedUri: 'https://www.example.com/video/segment_2.m4f',
180 | map: {
181 | uri: 'video/init.m4f',
182 | resolvedUri: 'https://www.example.com/video/init.m4f'
183 | },
184 | number: 2,
185 | presentationTime: 10
186 | },
187 | {
188 | discontinuity: true,
189 | uri: 'video/segment_0.m4f',
190 | timeline: 15,
191 | duration: 5,
192 | resolvedUri: 'https://www.example.com/video/segment_0.m4f',
193 | map: {
194 | uri: 'video/init.m4f',
195 | resolvedUri: 'https://www.example.com/video/init.m4f'
196 | },
197 | number: 3,
198 | presentationTime: 15
199 | },
200 | {
201 | uri: 'video/segment_1.m4f',
202 | timeline: 15,
203 | duration: 5,
204 | resolvedUri: 'https://www.example.com/video/segment_1.m4f',
205 | map: {
206 | uri: 'video/init.m4f',
207 | resolvedUri: 'https://www.example.com/video/init.m4f'
208 | },
209 | number: 4,
210 | presentationTime: 20
211 | },
212 | {
213 | uri: 'video/segment_2.m4f',
214 | timeline: 15,
215 | duration: 5,
216 | resolvedUri: 'https://www.example.com/video/segment_2.m4f',
217 | map: {
218 | uri: 'video/init.m4f',
219 | resolvedUri: 'https://www.example.com/video/init.m4f'
220 | },
221 | number: 5,
222 | presentationTime: 25
223 | }
224 | ],
225 | mediaSequence: 0,
226 | discontinuitySequence: 0,
227 | discontinuityStarts: [3],
228 | timelineStarts: [{
229 | start: 0,
230 | timeline: 0
231 | }, {
232 | start: 15,
233 | timeline: 15
234 | }]
235 | }
236 | ]
237 | };
238 |
--------------------------------------------------------------------------------
/test/manifests/multiperiod-segment-template.mpd:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 | https://www.example.com/base
10 |
11 |
18 |
24 |
25 |
26 |
27 |
30 |
34 |
40 |
41 |
42 |
43 |
44 | https://www.example.com/base
45 |
46 |
53 |
59 |
60 |
61 |
62 |
65 |
69 |
75 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/test/manifests/multiperiod-startnumber-removed-periods.js:
--------------------------------------------------------------------------------
1 | export const parsedManifest = {
2 | allowCache: true,
3 | discontinuityStarts: [],
4 | duration: 0,
5 | endList: true,
6 | timelineStarts: [
7 | { start: 100, timeline: 100},
8 | { start: 103, timeline: 103},
9 | { start: 107, timeline: 107},
10 | { start: 111, timeline: 111}
11 | ],
12 | mediaGroups: {
13 | 'AUDIO': {
14 | audio: {
15 | 'en (main)': {
16 | autoselect: true,
17 | default: true,
18 | language: 'en',
19 | playlists: [
20 | {
21 | attributes: {
22 | 'BANDWIDTH': 129262,
23 | 'CODECS': 'mp4a.40.5',
24 | 'NAME': 'v0',
25 | 'PROGRAM-ID': 1
26 | },
27 | endList: false,
28 | mediaSequence: 3,
29 | discontinuitySequence: 2,
30 | discontinuityStarts: [0],
31 | timelineStarts: [
32 | { start: 111, timeline: 111}
33 | ],
34 | resolvedUri: 'http://example.com/audio/v0/',
35 | segments: [
36 | {
37 | discontinuity: true,
38 | duration: 1,
39 | map: {
40 | resolvedUri: 'http://example.com/audio/v0/init.mp4',
41 | uri: 'init.mp4'
42 | },
43 | presentationTime: 111,
44 | number: 3,
45 | resolvedUri: 'http://example.com/audio/v0/862.m4f',
46 | timeline: 111,
47 | uri: '862.m4f'
48 | },
49 | {
50 | duration: 1,
51 | map: {
52 | resolvedUri: 'http://example.com/audio/v0/init.mp4',
53 | uri: 'init.mp4'
54 | },
55 | presentationTime: 112,
56 | number: 4,
57 | resolvedUri: 'http://example.com/audio/v0/863.m4f',
58 | timeline: 111,
59 | uri: '863.m4f'
60 | },
61 | {
62 | duration: 1,
63 | map: {
64 | resolvedUri: 'http://example.com/audio/v0/init.mp4',
65 | uri: 'init.mp4'
66 | },
67 | presentationTime: 113,
68 | number: 5,
69 | resolvedUri: 'http://example.com/audio/v0/864.m4f',
70 | timeline: 111,
71 | uri: '864.m4f'
72 | }
73 | ],
74 | targetDuration: 1,
75 | timeline: 111,
76 | uri: ''
77 | }
78 | ],
79 | uri: ''
80 | }
81 | }
82 | },
83 | 'CLOSED-CAPTIONS': {},
84 | 'SUBTITLES': {},
85 | 'VIDEO': {}
86 | },
87 | minimumUpdatePeriod: 2000,
88 | playlists: [
89 | {
90 | attributes: {
91 | 'AUDIO': 'audio',
92 | 'BANDWIDTH': 2942295,
93 | 'CODECS': 'avc1.4d001f',
94 | 'FRAME-RATE': 30,
95 | 'NAME': 'D',
96 | 'PROGRAM-ID': 1,
97 | 'RESOLUTION': {
98 | height: 720,
99 | width: 1280
100 | },
101 | 'SUBTITLES': 'subs'
102 | },
103 | endList: false,
104 | mediaSequence: 7,
105 | discontinuitySequence: 2,
106 | discontinuityStarts: [0],
107 | timelineStarts: [
108 | { start: 111, timeline: 111}
109 | ],
110 | resolvedUri: 'http://example.com/video/D/',
111 | segments: [
112 | {
113 | discontinuity: true,
114 | duration: 1,
115 | map: {
116 | resolvedUri: 'http://example.com/video/D/D_init.mp4',
117 | uri: 'D_init.mp4'
118 | },
119 | presentationTime: 111,
120 | number: 7,
121 | resolvedUri: 'http://example.com/video/D/D862.m4f',
122 | timeline: 111,
123 | uri: 'D862.m4f'
124 | },
125 | {
126 | duration: 1,
127 | map: {
128 | resolvedUri: 'http://example.com/video/D/D_init.mp4',
129 | uri: 'D_init.mp4'
130 | },
131 | presentationTime: 112,
132 | number: 8,
133 | resolvedUri: 'http://example.com/video/D/D863.m4f',
134 | timeline: 111,
135 | uri: 'D863.m4f'
136 | },
137 | {
138 | duration: 1,
139 | map: {
140 | resolvedUri: 'http://example.com/video/D/D_init.mp4',
141 | uri: 'D_init.mp4'
142 | },
143 | presentationTime: 113,
144 | number: 9,
145 | resolvedUri: 'http://example.com/video/D/D864.m4f',
146 | timeline: 111,
147 | uri: 'D864.m4f'
148 | }
149 | ],
150 | targetDuration: 1,
151 | timeline: 111,
152 | uri: ''
153 | },
154 | {
155 | attributes: {
156 | 'AUDIO': 'audio',
157 | 'BANDWIDTH': 4267536,
158 | 'CODECS': 'avc1.640020',
159 | 'FRAME-RATE': 60,
160 | 'NAME': 'E',
161 | 'PROGRAM-ID': 1,
162 | 'RESOLUTION': {
163 | height: 720,
164 | width: 1280
165 | },
166 | 'SUBTITLES': 'subs'
167 | },
168 | endList: false,
169 | mediaSequence: 7,
170 | discontinuitySequence: 2,
171 | timelineStarts: [
172 | { start: 111, timeline: 111}
173 | ],
174 | discontinuityStarts: [0],
175 | resolvedUri: 'http://example.com/video/E/',
176 | segments: [
177 | {
178 | discontinuity: true,
179 | duration: 1,
180 | map: {
181 | resolvedUri: 'http://example.com/video/E/E_init.mp4',
182 | uri: 'E_init.mp4'
183 | },
184 | presentationTime: 111,
185 | number: 7,
186 | resolvedUri: 'http://example.com/video/E/E862.m4f',
187 | timeline: 111,
188 | uri: 'E862.m4f'
189 | },
190 | {
191 | duration: 1,
192 | map: {
193 | resolvedUri: 'http://example.com/video/E/E_init.mp4',
194 | uri: 'E_init.mp4'
195 | },
196 | presentationTime: 112,
197 | number: 8,
198 | resolvedUri: 'http://example.com/video/E/E863.m4f',
199 | timeline: 111,
200 | uri: 'E863.m4f'
201 | },
202 | {
203 | duration: 1,
204 | map: {
205 | resolvedUri: 'http://example.com/video/E/E_init.mp4',
206 | uri: 'E_init.mp4'
207 | },
208 | presentationTime: 113,
209 | number: 9,
210 | resolvedUri: 'http://example.com/video/E/E864.m4f',
211 | timeline: 111,
212 | uri: 'E864.m4f'
213 | }
214 | ],
215 | targetDuration: 1,
216 | timeline: 111,
217 | uri: ''
218 | },
219 | {
220 | attributes: {
221 | 'AUDIO': 'audio',
222 | 'BANDWIDTH': 5256859,
223 | 'CODECS': 'avc1.640020',
224 | 'FRAME-RATE': 60,
225 | 'NAME': 'F',
226 | 'PROGRAM-ID': 1,
227 | 'RESOLUTION': {
228 | height: 720,
229 | width: 1280
230 | },
231 | 'SUBTITLES': 'subs'
232 | },
233 | endList: false,
234 | mediaSequence: 3,
235 | discontinuitySequence: 2,
236 | timelineStarts: [
237 | { start: 111, timeline: 111}
238 | ],
239 | discontinuityStarts: [0],
240 | resolvedUri: 'http://example.com/video/F/',
241 | segments: [
242 | {
243 | discontinuity: true,
244 | duration: 1,
245 | map: {
246 | resolvedUri: 'http://example.com/video/F/F_init.mp4',
247 | uri: 'F_init.mp4'
248 | },
249 | presentationTime: 111,
250 | number: 3,
251 | resolvedUri: 'http://example.com/video/F/F862.m4f',
252 | timeline: 111,
253 | uri: 'F862.m4f'
254 | },
255 | {
256 | duration: 1,
257 | map: {
258 | resolvedUri: 'http://example.com/video/F/F_init.mp4',
259 | uri: 'F_init.mp4'
260 | },
261 | presentationTime: 112,
262 | number: 4,
263 | resolvedUri: 'http://example.com/video/F/F863.m4f',
264 | timeline: 111,
265 | uri: 'F863.m4f'
266 | },
267 | {
268 | duration: 1,
269 | map: {
270 | resolvedUri: 'http://example.com/video/F/F_init.mp4',
271 | uri: 'F_init.mp4'
272 | },
273 | presentationTime: 113,
274 | number: 5,
275 | resolvedUri: 'http://example.com/video/F/F864.m4f',
276 | timeline: 111,
277 | uri: 'F864.m4f'
278 | }
279 | ],
280 | targetDuration: 1,
281 | timeline: 111,
282 | uri: ''
283 | },
284 | {
285 | attributes: {
286 | 'AUDIO': 'audio',
287 | 'BANDWIDTH': 240781,
288 | 'CODECS': 'avc1.4d000d',
289 | 'FRAME-RATE': 30,
290 | 'NAME': 'A',
291 | 'PROGRAM-ID': 1,
292 | 'RESOLUTION': {
293 | height: 234,
294 | width: 416
295 | },
296 | 'SUBTITLES': 'subs'
297 | },
298 | endList: false,
299 | mediaSequence: 7,
300 | discontinuitySequence: 2,
301 | timelineStarts: [
302 | { start: 111, timeline: 111}
303 | ],
304 | discontinuityStarts: [0],
305 | resolvedUri: 'http://example.com/video/A/',
306 | segments: [
307 | {
308 | discontinuity: true,
309 | duration: 1,
310 | map: {
311 | resolvedUri: 'http://example.com/video/A/A_init.mp4',
312 | uri: 'A_init.mp4'
313 | },
314 | presentationTime: 111,
315 | number: 7,
316 | resolvedUri: 'http://example.com/video/A/A862.m4f',
317 | timeline: 111,
318 | uri: 'A862.m4f'
319 | },
320 | {
321 | duration: 1,
322 | map: {
323 | resolvedUri: 'http://example.com/video/A/A_init.mp4',
324 | uri: 'A_init.mp4'
325 | },
326 | presentationTime: 112,
327 | number: 8,
328 | resolvedUri: 'http://example.com/video/A/A863.m4f',
329 | timeline: 111,
330 | uri: 'A863.m4f'
331 | },
332 | {
333 | duration: 1,
334 | map: {
335 | resolvedUri: 'http://example.com/video/A/A_init.mp4',
336 | uri: 'A_init.mp4'
337 | },
338 | presentationTime: 113,
339 | number: 9,
340 | resolvedUri: 'http://example.com/video/A/A864.m4f',
341 | timeline: 111,
342 | uri: 'A864.m4f'
343 | }
344 | ],
345 | targetDuration: 1,
346 | timeline: 111,
347 | uri: ''
348 | },
349 | {
350 | attributes: {
351 | 'AUDIO': 'audio',
352 | 'BANDWIDTH': 494354,
353 | 'CODECS': 'avc1.4d001e',
354 | 'FRAME-RATE': 30,
355 | 'NAME': 'B',
356 | 'PROGRAM-ID': 1,
357 | 'RESOLUTION': {
358 | height: 360,
359 | width: 640
360 | },
361 | 'SUBTITLES': 'subs'
362 | },
363 | endList: false,
364 | mediaSequence: 7,
365 | discontinuitySequence: 2,
366 | timelineStarts: [
367 | { start: 111, timeline: 111}
368 | ],
369 | discontinuityStarts: [0],
370 | resolvedUri: 'http://example.com/video/B/',
371 | segments: [
372 | {
373 | discontinuity: true,
374 | duration: 1,
375 | map: {
376 | resolvedUri: 'http://example.com/video/B/B_init.mp4',
377 | uri: 'B_init.mp4'
378 | },
379 | presentationTime: 111,
380 | number: 7,
381 | resolvedUri: 'http://example.com/video/B/B862.m4f',
382 | timeline: 111,
383 | uri: 'B862.m4f'
384 | },
385 | {
386 | duration: 1,
387 | map: {
388 | resolvedUri: 'http://example.com/video/B/B_init.mp4',
389 | uri: 'B_init.mp4'
390 | },
391 | presentationTime: 112,
392 | number: 8,
393 | resolvedUri: 'http://example.com/video/B/B863.m4f',
394 | timeline: 111,
395 | uri: 'B863.m4f'
396 | },
397 | {
398 | duration: 1,
399 | map: {
400 | resolvedUri: 'http://example.com/video/B/B_init.mp4',
401 | uri: 'B_init.mp4'
402 | },
403 | presentationTime: 113,
404 | number: 9,
405 | resolvedUri: 'http://example.com/video/B/B864.m4f',
406 | timeline: 111,
407 | uri: 'B864.m4f'
408 | }
409 | ],
410 | targetDuration: 1,
411 | timeline: 111,
412 | uri: ''
413 | },
414 | {
415 | attributes: {
416 | 'AUDIO': 'audio',
417 | 'BANDWIDTH': 1277155,
418 | 'CODECS': 'avc1.4d001f',
419 | 'FRAME-RATE': 30,
420 | 'NAME': 'C',
421 | 'PROGRAM-ID': 1,
422 | 'RESOLUTION': {
423 | height: 540,
424 | width: 960
425 | },
426 | 'SUBTITLES': 'subs'
427 | },
428 | endList: false,
429 | mediaSequence: 3,
430 | discontinuitySequence: 2,
431 | timelineStarts: [
432 | { start: 111, timeline: 111}
433 | ],
434 | discontinuityStarts: [0],
435 | resolvedUri: 'http://example.com/video/C/',
436 | segments: [
437 | {
438 | discontinuity: true,
439 | duration: 1,
440 | map: {
441 | resolvedUri: 'http://example.com/video/C/C_init.mp4',
442 | uri: 'C_init.mp4'
443 | },
444 | presentationTime: 111,
445 | number: 3,
446 | resolvedUri: 'http://example.com/video/C/C862.m4f',
447 | timeline: 111,
448 | uri: 'C862.m4f'
449 | },
450 | {
451 | duration: 1,
452 | map: {
453 | resolvedUri: 'http://example.com/video/C/C_init.mp4',
454 | uri: 'C_init.mp4'
455 | },
456 | presentationTime: 112,
457 | number: 4,
458 | resolvedUri: 'http://example.com/video/C/C863.m4f',
459 | timeline: 111,
460 | uri: 'C863.m4f'
461 | },
462 | {
463 | duration: 1,
464 | map: {
465 | resolvedUri: 'http://example.com/video/C/C_init.mp4',
466 | uri: 'C_init.mp4'
467 | },
468 | presentationTime: 113,
469 | number: 5,
470 | resolvedUri: 'http://example.com/video/C/C864.m4f',
471 | timeline: 111,
472 | uri: 'C864.m4f'
473 | }
474 | ],
475 | targetDuration: 1,
476 | timeline: 111,
477 | uri: ''
478 | }
479 | ],
480 | segments: [],
481 | suggestedPresentationDelay: 6,
482 | uri: ''
483 | };
484 |
--------------------------------------------------------------------------------
/test/manifests/multiperiod-startnumber-removed-periods.mpd:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
23 |
24 |
28 |
29 | http://example.com/audio/v0/
30 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
51 |
52 |
60 | http://example.com/video/D/
61 |
67 |
68 |
69 |
70 |
71 |
72 |
80 | http://example.com/video/E/
81 |
87 |
88 |
89 |
90 |
91 |
92 |
100 | http://example.com/video/F/
101 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
122 |
130 | http://example.com/video/A/
131 |
137 |
138 |
139 |
140 |
141 |
142 |
150 | http://example.com/video/B/
151 |
157 |
158 |
159 |
160 |
161 |
162 |
170 | http://example.com/video/C/
171 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
--------------------------------------------------------------------------------
/test/manifests/multiperiod.mpd:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | test
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | test
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | test
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | test
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
--------------------------------------------------------------------------------
/test/manifests/segmentBase.js:
--------------------------------------------------------------------------------
1 | export const parsedManifest = {
2 | allowCache: true,
3 | discontinuityStarts: [],
4 | duration: 6,
5 | endList: true,
6 | mediaGroups: {
7 | 'AUDIO': {},
8 | 'CLOSED-CAPTIONS': {},
9 | 'SUBTITLES': {},
10 | 'VIDEO': {}
11 | },
12 | playlists: [
13 | {
14 | attributes: {
15 | 'AUDIO': 'audio',
16 | 'BANDWIDTH': 449000,
17 | 'CODECS': 'avc1.420015',
18 | 'FRAME-RATE': 23.976,
19 | 'NAME': '482',
20 | 'PROGRAM-ID': 1,
21 | 'RESOLUTION': {
22 | height: 270,
23 | width: 482
24 | },
25 | 'SUBTITLES': 'subs'
26 | },
27 | endList: true,
28 | resolvedUri: 'https://www.example.com/1080p.ts',
29 | targetDuration: 6,
30 | mediaSequence: 0,
31 | segments: [
32 | {
33 | duration: 6,
34 | timeline: 0,
35 | number: 0,
36 | presentationTime: 0,
37 | map: {
38 | uri: '',
39 | resolvedUri: 'https://www.example.com/1080p.ts'
40 | },
41 | resolvedUri: 'https://www.example.com/1080p.ts',
42 | uri: 'https://www.example.com/1080p.ts'
43 | }
44 | ],
45 | timeline: 0,
46 | timelineStarts: [{ start: 0, timeline: 0 }],
47 | discontinuitySequence: 0,
48 | discontinuityStarts: [],
49 | uri: ''
50 | }
51 | ],
52 | segments: [],
53 | timelineStarts: [{ start: 0, timeline: 0 }],
54 | uri: ''
55 | };
56 |
--------------------------------------------------------------------------------
/test/manifests/segmentBase.mpd:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://www.example.com/base
4 |
5 |
6 |
7 |
8 |
9 |
10 | 1080p.ts
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/test/manifests/segmentList.js:
--------------------------------------------------------------------------------
1 | export const parsedManifest = {
2 | allowCache: true,
3 | discontinuityStarts: [],
4 | duration: 6,
5 | endList: true,
6 | mediaGroups: {
7 | 'AUDIO': {},
8 | 'CLOSED-CAPTIONS': {},
9 | 'SUBTITLES': {},
10 | 'VIDEO': {}
11 | },
12 | playlists: [
13 | {
14 | attributes: {
15 | 'AUDIO': 'audio',
16 | 'BANDWIDTH': 449000,
17 | 'CODECS': 'avc1.420015',
18 | 'FRAME-RATE': 23.976,
19 | 'NAME': '482',
20 | 'PROGRAM-ID': 1,
21 | 'RESOLUTION': {
22 | height: 270,
23 | width: 482
24 | },
25 | 'SUBTITLES': 'subs'
26 | },
27 | endList: true,
28 | mediaSequence: 0,
29 | targetDuration: 1,
30 | resolvedUri: 'https://www.example.com/base',
31 | segments: [
32 | {
33 | duration: 1,
34 | map: {
35 | uri: '',
36 | resolvedUri: 'https://www.example.com/base'
37 | },
38 | resolvedUri: 'https://www.example.com/low/segment-1.ts',
39 | timeline: 0,
40 | presentationTime: 0,
41 | uri: 'low/segment-1.ts',
42 | number: 0
43 | },
44 | {
45 | duration: 1,
46 | map: {
47 | uri: '',
48 | resolvedUri: 'https://www.example.com/base'
49 | },
50 | resolvedUri: 'https://www.example.com/low/segment-2.ts',
51 | timeline: 0,
52 | presentationTime: 1,
53 | uri: 'low/segment-2.ts',
54 | number: 1
55 | },
56 | {
57 | duration: 1,
58 | map: {
59 | uri: '',
60 | resolvedUri: 'https://www.example.com/base'
61 | },
62 | resolvedUri: 'https://www.example.com/low/segment-3.ts',
63 | timeline: 0,
64 | presentationTime: 2,
65 | uri: 'low/segment-3.ts',
66 | number: 2
67 | },
68 | {
69 | duration: 1,
70 | map: {
71 | uri: '',
72 | resolvedUri: 'https://www.example.com/base'
73 | },
74 | resolvedUri: 'https://www.example.com/low/segment-4.ts',
75 | timeline: 0,
76 | presentationTime: 3,
77 | uri: 'low/segment-4.ts',
78 | number: 3
79 | },
80 | {
81 | duration: 1,
82 | map: {
83 | uri: '',
84 | resolvedUri: 'https://www.example.com/base'
85 | },
86 | resolvedUri: 'https://www.example.com/low/segment-5.ts',
87 | timeline: 0,
88 | presentationTime: 4,
89 | uri: 'low/segment-5.ts',
90 | number: 4
91 | },
92 | {
93 | duration: 1,
94 | map: {
95 | uri: '',
96 | resolvedUri: 'https://www.example.com/base'
97 | },
98 | resolvedUri: 'https://www.example.com/low/segment-6.ts',
99 | timeline: 0,
100 | presentationTime: 5,
101 | uri: 'low/segment-6.ts',
102 | number: 5
103 | }
104 | ],
105 | timeline: 0,
106 | timelineStarts: [{ start: 0, timeline: 0 }],
107 | discontinuitySequence: 0,
108 | discontinuityStarts: [],
109 | uri: ''
110 | },
111 | {
112 | attributes: {
113 | 'AUDIO': 'audio',
114 | 'BANDWIDTH': 3971000,
115 | 'CODECS': 'avc1.420015',
116 | 'FRAME-RATE': 23.976,
117 | 'NAME': '720',
118 | 'PROGRAM-ID': 1,
119 | 'RESOLUTION': {
120 | height: 404,
121 | width: 720
122 | },
123 | 'SUBTITLES': 'subs'
124 | },
125 | endList: true,
126 | resolvedUri: 'https://www.example.com/base',
127 | mediaSequence: 0,
128 | targetDuration: 60,
129 | segments: [
130 | {
131 | duration: 60,
132 | map: {
133 | uri: '',
134 | resolvedUri: 'https://www.example.com/base'
135 | },
136 | resolvedUri: 'https://www.example.com/high/segment-1.ts',
137 | timeline: 0,
138 | presentationTime: 0,
139 | uri: 'high/segment-1.ts',
140 | number: 0
141 | },
142 | {
143 | duration: 60,
144 | map: {
145 | uri: '',
146 | resolvedUri: 'https://www.example.com/base'
147 | },
148 | resolvedUri: 'https://www.example.com/high/segment-2.ts',
149 | timeline: 0,
150 | presentationTime: 60,
151 | uri: 'high/segment-2.ts',
152 | number: 1
153 | },
154 | {
155 | duration: 60,
156 | map: {
157 | uri: '',
158 | resolvedUri: 'https://www.example.com/base'
159 | },
160 | resolvedUri: 'https://www.example.com/high/segment-3.ts',
161 | timeline: 0,
162 | presentationTime: 120,
163 | uri: 'high/segment-3.ts',
164 | number: 2
165 | },
166 | {
167 | duration: 60,
168 | map: {
169 | uri: '',
170 | resolvedUri: 'https://www.example.com/base'
171 | },
172 | resolvedUri: 'https://www.example.com/high/segment-4.ts',
173 | timeline: 0,
174 | presentationTime: 180,
175 | uri: 'high/segment-4.ts',
176 | number: 3
177 | },
178 | {
179 | duration: 60,
180 | map: {
181 | uri: '',
182 | resolvedUri: 'https://www.example.com/base'
183 | },
184 | resolvedUri: 'https://www.example.com/high/segment-5.ts',
185 | timeline: 0,
186 | presentationTime: 240,
187 | uri: 'high/segment-5.ts',
188 | number: 4
189 | },
190 | {
191 | duration: 60,
192 | map: {
193 | uri: '',
194 | resolvedUri: 'https://www.example.com/base'
195 | },
196 | resolvedUri: 'https://www.example.com/high/segment-6.ts',
197 | timeline: 0,
198 | presentationTime: 300,
199 | uri: 'high/segment-6.ts',
200 | number: 5
201 | },
202 | {
203 | duration: 60,
204 | map: {
205 | uri: '',
206 | resolvedUri: 'https://www.example.com/base'
207 | },
208 | resolvedUri: 'https://www.example.com/high/segment-7.ts',
209 | timeline: 0,
210 | presentationTime: 360,
211 | uri: 'high/segment-7.ts',
212 | number: 6
213 | },
214 | {
215 | duration: 60,
216 | map: {
217 | uri: '',
218 | resolvedUri: 'https://www.example.com/base'
219 | },
220 | resolvedUri: 'https://www.example.com/high/segment-8.ts',
221 | timeline: 0,
222 | presentationTime: 420,
223 | uri: 'high/segment-8.ts',
224 | number: 7
225 | },
226 | {
227 | duration: 60,
228 | map: {
229 | uri: '',
230 | resolvedUri: 'https://www.example.com/base'
231 | },
232 | resolvedUri: 'https://www.example.com/high/segment-9.ts',
233 | timeline: 0,
234 | presentationTime: 480,
235 | uri: 'high/segment-9.ts',
236 | number: 8
237 | },
238 | {
239 | duration: 60,
240 | map: {
241 | uri: '',
242 | resolvedUri: 'https://www.example.com/base'
243 | },
244 | resolvedUri: 'https://www.example.com/high/segment-10.ts',
245 | timeline: 0,
246 | presentationTime: 540,
247 | uri: 'high/segment-10.ts',
248 | number: 9
249 | }
250 | ],
251 | timeline: 0,
252 | timelineStarts: [{ start: 0, timeline: 0 }],
253 | discontinuitySequence: 0,
254 | discontinuityStarts: [],
255 | uri: ''
256 | }
257 | ],
258 | segments: [],
259 | timelineStarts: [{ start: 0, timeline: 0 }],
260 | uri: ''
261 | };
262 |
--------------------------------------------------------------------------------
/test/manifests/segmentList.mpd:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://www.example.com/base
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/test/manifests/vtt_codecs.mpd:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://www.example.com/base
4 |
5 |
6 |
7 |
8 |
9 | test
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | test
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | test
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | https://example.com/en.dash
51 |
52 |
53 |
54 |
55 | https://example.com/es.vtt
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/test/manifests/webmsegments.js:
--------------------------------------------------------------------------------
1 | export const parsedManifest = {
2 | allowCache: true,
3 | discontinuityStarts: [],
4 | segments: [],
5 | timelineStarts: [{ start: 0, timeline: 0 }],
6 | endList: true,
7 | mediaGroups: {
8 | 'AUDIO': {
9 | audio: {
10 | en: {
11 | language: 'en',
12 | autoselect: true,
13 | default: true,
14 | playlists: [
15 | {
16 | attributes: {
17 | 'NAME': '2',
18 | 'BANDWIDTH': 32000,
19 | 'CODECS': 'opus',
20 | 'PROGRAM-ID': 1
21 | },
22 | uri: '',
23 | endList: true,
24 | timeline: 0,
25 | resolvedUri: 'https://www.example.com/base',
26 | targetDuration: 4,
27 | segments: [
28 | {
29 | uri: 'audio/segment_0.chk',
30 | timeline: 0,
31 | duration: 4,
32 | resolvedUri: 'https://www.example.com/audio/segment_0.chk',
33 | map: {
34 | uri: 'audio/init.hdr',
35 | resolvedUri: 'https://www.example.com/audio/init.hdr'
36 | },
37 | number: 0,
38 | presentationTime: 0
39 | },
40 | {
41 | uri: 'audio/segment_1.chk',
42 | timeline: 0,
43 | duration: 4,
44 | resolvedUri: 'https://www.example.com/audio/segment_1.chk',
45 | map: {
46 | uri: 'audio/init.hdr',
47 | resolvedUri: 'https://www.example.com/audio/init.hdr'
48 | },
49 | number: 1,
50 | presentationTime: 4
51 | },
52 | {
53 | uri: 'audio/segment_2.chk',
54 | timeline: 0,
55 | duration: 4,
56 | resolvedUri: 'https://www.example.com/audio/segment_2.chk',
57 | map: {
58 | uri: 'audio/init.hdr',
59 | resolvedUri: 'https://www.example.com/audio/init.hdr'
60 | },
61 | number: 2,
62 | presentationTime: 8
63 | },
64 | {
65 | uri: 'audio/segment_3.chk',
66 | timeline: 0,
67 | duration: 4,
68 | resolvedUri: 'https://www.example.com/audio/segment_3.chk',
69 | map: {
70 | uri: 'audio/init.hdr',
71 | resolvedUri: 'https://www.example.com/audio/init.hdr'
72 | },
73 | number: 3,
74 | presentationTime: 12
75 | }
76 | ],
77 | mediaSequence: 0,
78 | timelineStarts: [{ start: 0, timeline: 0 }],
79 | discontinuitySequence: 0,
80 | discontinuityStarts: []
81 | }
82 | ],
83 | uri: ''
84 | }
85 | }
86 | },
87 | 'VIDEO': {},
88 | 'CLOSED-CAPTIONS': {},
89 | 'SUBTITLES': {}
90 | },
91 | uri: '',
92 | duration: 16,
93 | playlists: [
94 | {
95 | attributes: {
96 | 'AUDIO': 'audio',
97 | 'BANDWIDTH': 100000,
98 | 'CODECS': 'av1',
99 | 'FRAME-RATE': 24,
100 | 'NAME': '1',
101 | 'PROGRAM-ID': 1,
102 | 'RESOLUTION': {
103 | width: 480,
104 | height: 200
105 | },
106 | 'SUBTITLES': 'subs'
107 | },
108 | uri: '',
109 | endList: true,
110 | timeline: 0,
111 | resolvedUri: 'https://www.example.com/base',
112 | targetDuration: 4,
113 | segments: [
114 | {
115 | uri: 'video/segment_0.chk',
116 | timeline: 0,
117 | duration: 4,
118 | resolvedUri: 'https://www.example.com/video/segment_0.chk',
119 | map: {
120 | uri: 'video/init.hdr',
121 | resolvedUri: 'https://www.example.com/video/init.hdr'
122 | },
123 | number: 0,
124 | presentationTime: 0
125 | },
126 | {
127 | uri: 'video/segment_1.chk',
128 | timeline: 0,
129 | duration: 4,
130 | resolvedUri: 'https://www.example.com/video/segment_1.chk',
131 | map: {
132 | uri: 'video/init.hdr',
133 | resolvedUri: 'https://www.example.com/video/init.hdr'
134 | },
135 | number: 1,
136 | presentationTime: 4
137 | },
138 | {
139 | uri: 'video/segment_2.chk',
140 | timeline: 0,
141 | duration: 4,
142 | resolvedUri: 'https://www.example.com/video/segment_2.chk',
143 | map: {
144 | uri: 'video/init.hdr',
145 | resolvedUri: 'https://www.example.com/video/init.hdr'
146 | },
147 | number: 2,
148 | presentationTime: 8
149 | },
150 | {
151 | uri: 'video/segment_3.chk',
152 | timeline: 0,
153 | duration: 4,
154 | resolvedUri: 'https://www.example.com/video/segment_3.chk',
155 | map: {
156 | uri: 'video/init.hdr',
157 | resolvedUri: 'https://www.example.com/video/init.hdr'
158 | },
159 | number: 3,
160 | presentationTime: 12
161 | }
162 | ],
163 | mediaSequence: 0,
164 | timelineStarts: [{ start: 0, timeline: 0 }],
165 | discontinuitySequence: 0,
166 | discontinuityStarts: []
167 | }
168 | ]
169 | };
170 |
--------------------------------------------------------------------------------
/test/manifests/webmsegments.mpd:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | https://www.example.com/base
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/test/parseAttributes.test.js:
--------------------------------------------------------------------------------
1 | import {JSDOM} from 'jsdom';
2 | import QUnit from 'qunit';
3 | import { parseAttributes } from '../src/parseAttributes';
4 |
5 | const document = new JSDOM().window.document;
6 |
7 | QUnit.module('parseAttributes');
8 |
9 | QUnit.test('simple', function(assert) {
10 | const el = document.createElement('el');
11 |
12 | el.setAttribute('foo', 1);
13 |
14 | assert.deepEqual(parseAttributes(el), { foo: '1' });
15 | });
16 |
17 | QUnit.test('empty', function(assert) {
18 | const el = document.createElement('el');
19 |
20 | assert.deepEqual(parseAttributes(el), {});
21 | });
22 |
--------------------------------------------------------------------------------
/test/segment/durationTimeParser.test.js:
--------------------------------------------------------------------------------
1 | import QUnit from 'qunit';
2 | import { segmentRange } from '../../src/segment/durationTimeParser.js';
3 |
4 | QUnit.module('segmentRange');
5 |
6 | QUnit.test('static range uses periodDuration if available', function(assert) {
7 | assert.deepEqual(
8 | segmentRange.static({
9 | periodDuration: 10,
10 | sourceDuration: 20,
11 | duration: 2,
12 | timescale: 1
13 | }),
14 | { start: 0, end: 5 },
15 | 'uses periodDuration if available'
16 | );
17 | });
18 |
19 | QUnit.test('static range uses sourceDuration if available', function(assert) {
20 | assert.deepEqual(
21 | segmentRange.static({
22 | sourceDuration: 20,
23 | duration: 2,
24 | timescale: 1
25 | }),
26 | { start: 0, end: 10 },
27 | 'uses periodDuration if available'
28 | );
29 | });
30 |
--------------------------------------------------------------------------------
/test/segment/segmentBase.test.js:
--------------------------------------------------------------------------------
1 | import QUnit from 'qunit';
2 | import {
3 | segmentsFromBase,
4 | addSidxSegmentsToPlaylist
5 | } from '../../src/segment/segmentBase';
6 | import errors from '../../src/errors';
7 | import window from 'global/window';
8 |
9 | QUnit.module('segmentBase - segmentsFromBase');
10 |
11 | QUnit.test('sets segment to baseUrl', function(assert) {
12 | const inputAttributes = {
13 | baseUrl: 'http://www.example.com/i.fmp4',
14 | initialization: { sourceURL: 'http://www.example.com/init.fmp4' },
15 | periodStart: 0,
16 | type: 'static'
17 | };
18 |
19 | assert.deepEqual(segmentsFromBase(inputAttributes), [{
20 | map: {
21 | resolvedUri: 'http://www.example.com/init.fmp4',
22 | uri: 'http://www.example.com/init.fmp4'
23 | },
24 | resolvedUri: 'http://www.example.com/i.fmp4',
25 | uri: 'http://www.example.com/i.fmp4',
26 | presentationTime: 0,
27 | number: 0
28 | }]);
29 | });
30 |
31 | QUnit.test('sets duration based on sourceDuration', function(assert) {
32 | const inputAttributes = {
33 | baseUrl: 'http://www.example.com/i.fmp4',
34 | initialization: { sourceURL: 'http://www.example.com/init.fmp4' },
35 | sourceDuration: 10,
36 | periodStart: 0,
37 | type: 'static'
38 | };
39 |
40 | assert.deepEqual(segmentsFromBase(inputAttributes), [{
41 | duration: 10,
42 | timeline: 0,
43 | map: {
44 | resolvedUri: 'http://www.example.com/init.fmp4',
45 | uri: 'http://www.example.com/init.fmp4'
46 | },
47 | resolvedUri: 'http://www.example.com/i.fmp4',
48 | uri: 'http://www.example.com/i.fmp4',
49 | presentationTime: 0,
50 | number: 0
51 | }]);
52 | });
53 |
54 | // sourceDuration comes from mediaPresentationDuration. The DASH spec defines the type of
55 | // mediaPresentationDuration as xs:duration, which follows ISO 8601. It does not need to
56 | // be adjusted based on timescale.
57 | //
58 | // References:
59 | // https://www.w3.org/TR/xmlschema-2/#duration
60 | // https://en.wikipedia.org/wiki/ISO_8601
61 | QUnit.test('sets duration based on sourceDuration and not @timescale', function(assert) {
62 | const inputAttributes = {
63 | baseUrl: 'http://www.example.com/i.fmp4',
64 | initialization: { sourceURL: 'http://www.example.com/init.fmp4' },
65 | sourceDuration: 10,
66 | timescale: 2,
67 | periodStart: 0,
68 | type: 'static'
69 | };
70 |
71 | assert.deepEqual(segmentsFromBase(inputAttributes), [{
72 | duration: 10,
73 | timeline: 0,
74 | map: {
75 | resolvedUri: 'http://www.example.com/init.fmp4',
76 | uri: 'http://www.example.com/init.fmp4'
77 | },
78 | resolvedUri: 'http://www.example.com/i.fmp4',
79 | uri: 'http://www.example.com/i.fmp4',
80 | presentationTime: 0,
81 | number: 0
82 | }]);
83 | });
84 |
85 | QUnit.test('sets duration based on @duration', function(assert) {
86 | const inputAttributes = {
87 | duration: 10,
88 | sourceDuration: 20,
89 | baseUrl: 'http://www.example.com/i.fmp4',
90 | initialization: { sourceURL: 'http://www.example.com/init.fmp4' },
91 | periodStart: 0,
92 | type: 'static'
93 | };
94 |
95 | assert.deepEqual(segmentsFromBase(inputAttributes), [{
96 | duration: 10,
97 | timeline: 0,
98 | map: {
99 | resolvedUri: 'http://www.example.com/init.fmp4',
100 | uri: 'http://www.example.com/init.fmp4'
101 | },
102 | resolvedUri: 'http://www.example.com/i.fmp4',
103 | uri: 'http://www.example.com/i.fmp4',
104 | presentationTime: 0,
105 | number: 0
106 | }]);
107 | });
108 |
109 | QUnit.test('sets duration based on @duration and @timescale', function(assert) {
110 | const inputAttributes = {
111 | duration: 10,
112 | sourceDuration: 20,
113 | timescale: 5,
114 | baseUrl: 'http://www.example.com/i.fmp4',
115 | initialization: { sourceURL: 'http://www.example.com/init.fmp4' },
116 | periodStart: 0,
117 | type: 'static'
118 | };
119 |
120 | assert.deepEqual(segmentsFromBase(inputAttributes), [{
121 | duration: 2,
122 | timeline: 0,
123 | map: {
124 | resolvedUri: 'http://www.example.com/init.fmp4',
125 | uri: 'http://www.example.com/init.fmp4'
126 | },
127 | resolvedUri: 'http://www.example.com/i.fmp4',
128 | uri: 'http://www.example.com/i.fmp4',
129 | presentationTime: 0,
130 | number: 0
131 | }]);
132 | });
133 |
134 | QUnit.test('translates ranges in node', function(assert) {
135 | const inputAttributes = {
136 | duration: 10,
137 | sourceDuration: 20,
138 | timescale: 5,
139 | baseUrl: 'http://www.example.com/i.fmp4',
140 | initialization: {
141 | sourceURL: 'http://www.example.com/init.fmp4',
142 | range: '121-125'
143 | },
144 | periodStart: 0,
145 | type: 'static'
146 | };
147 |
148 | assert.deepEqual(segmentsFromBase(inputAttributes), [{
149 | duration: 2,
150 | timeline: 0,
151 | map: {
152 | resolvedUri: 'http://www.example.com/init.fmp4',
153 | uri: 'http://www.example.com/init.fmp4',
154 | byterange: {
155 | length: 5,
156 | offset: 121
157 | }
158 | },
159 | resolvedUri: 'http://www.example.com/i.fmp4',
160 | uri: 'http://www.example.com/i.fmp4',
161 | presentationTime: 0,
162 | number: 0
163 | }]);
164 | });
165 |
166 | QUnit.test('errors if no baseUrl exists', function(assert) {
167 | assert.throws(() => segmentsFromBase({}), new Error(errors.NO_BASE_URL));
168 | });
169 |
170 | QUnit.module('segmentBase - addSidxSegmentsToPlaylist');
171 |
172 | QUnit.test('generates playlist from sidx references', function(assert) {
173 | const baseUrl = 'http://www.example.com/i.fmp4';
174 | const playlist = {
175 | sidx: {
176 | map: {
177 | byterange: {
178 | offset: 0,
179 | length: 10
180 | }
181 | },
182 | duration: 10,
183 | byterange: {
184 | offset: 9,
185 | length: 11
186 | },
187 | timeline: 0
188 | },
189 | segments: [],
190 | endList: true
191 | };
192 | const sidx = {
193 | timescale: 1,
194 | firstOffset: 0,
195 | references: [{
196 | referenceType: 0,
197 | referencedSize: 5,
198 | subsegmentDuration: 2
199 | }]
200 | };
201 |
202 | assert.deepEqual(addSidxSegmentsToPlaylist(playlist, sidx, baseUrl).segments, [{
203 | map: {
204 | byterange: {
205 | offset: 0,
206 | length: 10
207 | }
208 | },
209 | uri: 'http://www.example.com/i.fmp4',
210 | resolvedUri: 'http://www.example.com/i.fmp4',
211 | byterange: {
212 | offset: 20,
213 | length: 5
214 | },
215 | duration: 2,
216 | timeline: 0,
217 | presentationTime: 0,
218 | number: 0
219 | }]);
220 | });
221 |
222 | if (window.BigInt) {
223 | const BigInt = window.BigInt;
224 |
225 | QUnit.test('generates playlist from sidx references with BigInt', function(assert) {
226 | const baseUrl = 'http://www.example.com/i.fmp4';
227 | const playlist = {
228 | sidx: {
229 | map: {
230 | byterange: {
231 | offset: 0,
232 | length: 10
233 | }
234 | },
235 | timeline: 0,
236 | duration: 10,
237 | byterange: {
238 | offset: 9,
239 | length: 11
240 | }
241 | },
242 | segments: []
243 | };
244 | const offset = BigInt(Number.MAX_SAFE_INTEGER) + BigInt(10);
245 | const sidx = {
246 | timescale: 1,
247 | firstOffset: offset,
248 | references: [{
249 | referenceType: 0,
250 | referencedSize: 5,
251 | subsegmentDuration: 2
252 | }]
253 | };
254 |
255 | const segments = addSidxSegmentsToPlaylist(playlist, sidx, baseUrl).segments;
256 |
257 | assert.equal(typeof segments[0].byterange.offset, 'bigint', 'bigint offset');
258 | segments[0].byterange.offset = segments[0].byterange.offset.toString();
259 |
260 | assert.deepEqual(segments, [{
261 | map: {
262 | byterange: {
263 | offset: 0,
264 | length: 10
265 | }
266 | },
267 | uri: 'http://www.example.com/i.fmp4',
268 | resolvedUri: 'http://www.example.com/i.fmp4',
269 | byterange: {
270 | // sidx byterange offset + length = 20
271 | offset: (window.BigInt(20) + offset).toString(),
272 | length: 5
273 | },
274 | number: 0,
275 | presentationTime: 0
276 | }]);
277 | });
278 | }
279 |
--------------------------------------------------------------------------------
/test/segment/urlType.test.js:
--------------------------------------------------------------------------------
1 | import QUnit from 'qunit';
2 | import {
3 | urlTypeToSegment as urlTypeConverter,
4 | byteRangeToString
5 | } from '../../src/segment/urlType';
6 | import window from 'global/window';
7 |
8 | QUnit.module('urlType - urlTypeConverter');
9 |
10 | QUnit.test('returns correct object if given baseUrl only', function(assert) {
11 | assert.deepEqual(urlTypeConverter({ baseUrl: 'http://example.com/' }), {
12 | resolvedUri: 'http://example.com/',
13 | uri: ''
14 | });
15 | });
16 |
17 | QUnit.test('returns correct object if given baseUrl and source', function(assert) {
18 | assert.deepEqual(urlTypeConverter({
19 | baseUrl: 'http://example.com',
20 | source: 'init.fmp4'
21 | }), {
22 | resolvedUri: 'http://example.com/init.fmp4',
23 | uri: 'init.fmp4'
24 | });
25 | });
26 |
27 | QUnit.test('returns correct object if given baseUrl, source and range', function(assert) {
28 | assert.deepEqual(urlTypeConverter({
29 | baseUrl: 'http://example.com',
30 | source: 'init.fmp4',
31 | range: '101-105'
32 | }), {
33 | resolvedUri: 'http://example.com/init.fmp4',
34 | uri: 'init.fmp4',
35 | byterange: {
36 | offset: 101,
37 | length: 5
38 | }
39 | });
40 | });
41 |
42 | QUnit.test('returns correct object if given baseUrl, source and indexRange', function(assert) {
43 | assert.deepEqual(urlTypeConverter({
44 | baseUrl: 'http://example.com',
45 | source: 'sidx.fmp4',
46 | indexRange: '101-105'
47 | }), {
48 | resolvedUri: 'http://example.com/sidx.fmp4',
49 | uri: 'sidx.fmp4',
50 | byterange: {
51 | offset: 101,
52 | length: 5
53 | }
54 | });
55 | });
56 |
57 | QUnit.test('returns correct object if given baseUrl and range', function(assert) {
58 | assert.deepEqual(urlTypeConverter({
59 | baseUrl: 'http://example.com/',
60 | range: '101-105'
61 | }), {
62 | resolvedUri: 'http://example.com/',
63 | uri: '',
64 | byterange: {
65 | offset: 101,
66 | length: 5
67 | }
68 | });
69 | });
70 |
71 | QUnit.test('returns correct object if given baseUrl and indexRange', function(assert) {
72 | assert.deepEqual(urlTypeConverter({
73 | baseUrl: 'http://example.com/',
74 | indexRange: '101-105'
75 | }), {
76 | resolvedUri: 'http://example.com/',
77 | uri: '',
78 | byterange: {
79 | offset: 101,
80 | length: 5
81 | }
82 | });
83 | });
84 |
85 | if (window.BigInt) {
86 | const BigInt = window.BigInt;
87 |
88 | QUnit.test('can use BigInt range', function(assert) {
89 | const bigNumber = BigInt(Number.MAX_SAFE_INTEGER) + BigInt(10);
90 |
91 | const result = urlTypeConverter({
92 | baseUrl: 'http://example.com',
93 | source: 'init.fmp4',
94 | range: `${bigNumber}-${bigNumber + BigInt(4)}`
95 | });
96 |
97 | assert.equal(typeof result.byterange.offset, 'bigint', 'is bigint');
98 | result.byterange.offset = result.byterange.offset.toString();
99 |
100 | assert.deepEqual(result, {
101 | resolvedUri: 'http://example.com/init.fmp4',
102 | uri: 'init.fmp4',
103 | byterange: {
104 | offset: bigNumber.toString(),
105 | length: 5
106 | }
107 | });
108 | });
109 |
110 | QUnit.test('returns number range if bigint not nedeed', function(assert) {
111 | const bigNumber = BigInt(5);
112 |
113 | const result = urlTypeConverter({
114 | baseUrl: 'http://example.com',
115 | source: 'init.fmp4',
116 | range: `${bigNumber}-${bigNumber + BigInt(4)}`
117 | });
118 |
119 | assert.deepEqual(result, {
120 | resolvedUri: 'http://example.com/init.fmp4',
121 | uri: 'init.fmp4',
122 | byterange: {
123 | offset: 5,
124 | length: 5
125 | }
126 | });
127 | });
128 | }
129 |
130 | QUnit.module('urlType - byteRangeToString');
131 |
132 | QUnit.test('returns correct string representing byterange object', function(assert) {
133 | assert.strictEqual(
134 | byteRangeToString({
135 | offset: 0,
136 | length: 100
137 | }),
138 | '0-99'
139 | );
140 | });
141 |
142 | if (window.BigInt) {
143 | const BigInt = window.BigInt;
144 |
145 | QUnit.test('can handle bigint numbers', function(assert) {
146 | const offset = BigInt(Number.MAX_SAFE_INTEGER) + BigInt(10);
147 | const length = BigInt(Number.MAX_SAFE_INTEGER) + BigInt(5);
148 |
149 | assert.strictEqual(
150 | byteRangeToString({
151 | offset,
152 | length
153 | }),
154 | `${offset}-${offset + length - BigInt(1)}`
155 | );
156 | });
157 | }
158 |
--------------------------------------------------------------------------------
/test/stringToMpdXml.test.js:
--------------------------------------------------------------------------------
1 | import { stringToMpdXml } from '../src/stringToMpdXml';
2 | import errors from '../src/errors';
3 | import QUnit from 'qunit';
4 |
5 | QUnit.module('stringToMpdXml');
6 |
7 | QUnit.test('simple mpd', function(assert) {
8 | assert.deepEqual(stringToMpdXml('').tagName, 'MPD');
9 | });
10 |
11 | QUnit.test('invalid xml', function(assert) {
12 | assert.throws(() => stringToMpdXml(' stringToMpdXml(''), new RegExp(errors.DASH_INVALID_XML));
17 | });
18 |
19 | QUnit.test('empty manifest', function(assert) {
20 | assert.throws(() => stringToMpdXml(''), new RegExp(errors.DASH_EMPTY_MANIFEST));
21 | });
22 |
--------------------------------------------------------------------------------
/test/utils.test.js:
--------------------------------------------------------------------------------
1 | import { merge, values } from '../src/utils/object';
2 | import { parseDuration } from '../src/utils/time';
3 | import {
4 | flatten,
5 | range,
6 | from,
7 | findIndexes
8 | } from '../src/utils/list';
9 | import { findChildren, getContent } from '../src/utils/xml';
10 | import {DOMParser} from '@xmldom/xmldom';
11 | import {JSDOM} from 'jsdom';
12 | import QUnit from 'qunit';
13 |
14 | const document = new JSDOM().window.document;
15 |
16 | QUnit.module('utils');
17 |
18 | QUnit.module('merge');
19 | QUnit.test('empty', function(assert) {
20 | assert.deepEqual(merge({}, { a: 1 }), { a: 1 });
21 | assert.deepEqual(merge({ a: 1 }, { a: 1 }), { a: 1 });
22 | assert.deepEqual(merge({ a: 1 }, {}), { a: 1 });
23 | });
24 |
25 | QUnit.test('append', function(assert) {
26 | assert.deepEqual(merge({ a: 1 }, { b: 3 }), { a: 1, b: 3 });
27 | });
28 |
29 | QUnit.test('overwrite', function(assert) {
30 | assert.deepEqual(merge({ a: 1 }, { a: 2 }), { a: 2 });
31 | });
32 |
33 | QUnit.test('empty', function(assert) {
34 | assert.deepEqual(merge({}, {}), {});
35 | assert.deepEqual(merge({}, 1), {});
36 | assert.deepEqual(merge(1, {}), {});
37 | });
38 |
39 | QUnit.test('Test for checking the merge when multiple segment Information are present', function(assert) {
40 |
41 | const adaptationSetInfo = {
42 |
43 | base: { duration: '10'}
44 | };
45 |
46 | const representationInfo = {
47 |
48 | base: { duration: '25', indexRange: '230-252'}
49 | };
50 |
51 | const expected = {
52 |
53 | base: { duration: '25', indexRange: '230-252'}
54 | };
55 |
56 | assert.deepEqual(
57 | merge(adaptationSetInfo, representationInfo), expected,
58 | 'Merged SegmentBase info'
59 | );
60 |
61 | });
62 |
63 | QUnit.test('Test for checking the merge when segment Information is present at a level and is undefined at another', function(assert) {
64 | const periodInfo = {
65 | base: {
66 | initialization: {
67 | range: '0-8888'
68 |
69 | }
70 | }
71 | };
72 |
73 | const adaptationSetInfo = {
74 |
75 | base: { duration: '10', indexRange: '230-252'}
76 | };
77 |
78 | const representationInfo = {};
79 |
80 | const expected = {
81 |
82 | base: { duration: '10', indexRange: '230-252', initialization: {range: '0-8888'}}
83 | };
84 |
85 | assert.deepEqual(
86 | merge(periodInfo, adaptationSetInfo, representationInfo), expected,
87 | 'Merged SegmentBase info'
88 | );
89 |
90 | });
91 |
92 | QUnit.module('values');
93 |
94 | QUnit.test('empty', function(assert) {
95 | assert.deepEqual(values({}), []);
96 | });
97 |
98 | QUnit.test('mixed', function(assert) {
99 | assert.deepEqual(values({ a: 1, b: true, c: 'foo'}), [1, true, 'foo']);
100 | });
101 |
102 | QUnit.module('flatten');
103 | QUnit.test('empty', function(assert) {
104 | assert.deepEqual(flatten([]), []);
105 | });
106 |
107 | QUnit.test('one item', function(assert) {
108 | assert.deepEqual(flatten([[1]]), [1]);
109 | });
110 |
111 | QUnit.test('multiple items', function(assert) {
112 | assert.deepEqual(flatten([[1], [2], [3]]), [1, 2, 3]);
113 | });
114 |
115 | QUnit.test('multiple multiple items', function(assert) {
116 | assert.deepEqual(flatten([[1], [2, 3], [4]]), [1, 2, 3, 4]);
117 | });
118 |
119 | QUnit.test('nested nests', function(assert) {
120 | assert.deepEqual(flatten([[1], [[2]]]), [1, [2]]);
121 | });
122 |
123 | QUnit.test('not a list of lists', function(assert) {
124 | assert.deepEqual(flatten([1, 2]), [1, 2]);
125 | assert.deepEqual(flatten([[1], 2]), [1, 2]);
126 | });
127 |
128 | QUnit.module('parseDuration');
129 | QUnit.test('full date', function(assert) {
130 | assert.deepEqual(parseDuration('P10Y10M10DT10H10M10.1S'), 342180610.1);
131 | });
132 |
133 | QUnit.test('time only', function(assert) {
134 | assert.deepEqual(parseDuration('PT10H10M10.1S'), 36610.1);
135 | });
136 |
137 | QUnit.test('empty', function(assert) {
138 | assert.deepEqual(parseDuration(''), 0);
139 | });
140 |
141 | QUnit.test('invalid', function(assert) {
142 | assert.deepEqual(parseDuration('foo'), 0);
143 | });
144 |
145 | QUnit.module('range');
146 | QUnit.test('simple', function(assert) {
147 | assert.deepEqual(range(1, 4), [1, 2, 3]);
148 | });
149 |
150 | QUnit.test('single number range', function(assert) {
151 | assert.deepEqual(range(1, 1), []);
152 | });
153 |
154 | QUnit.test('negative', function(assert) {
155 | assert.deepEqual(range(-1, 2), [-1, 0, 1]);
156 | });
157 |
158 | QUnit.module('from');
159 |
160 | QUnit.test('simple array', function(assert) {
161 | assert.deepEqual(from([1]), [1]);
162 | });
163 |
164 | QUnit.test('empty array', function(assert) {
165 | assert.deepEqual(from([]), []);
166 | });
167 |
168 | QUnit.test('non-array', function(assert) {
169 | assert.deepEqual(from(1), []);
170 | });
171 |
172 | QUnit.test('array-like', function(assert) {
173 | const fixture = document.createElement('div');
174 |
175 | fixture.innerHTML = '';
176 |
177 | const result = from(fixture.getElementsByTagName('div'));
178 |
179 | assert.ok(result.map);
180 | assert.deepEqual(result.length, 2);
181 | });
182 |
183 | QUnit.module('findIndexes');
184 |
185 | QUnit.test('index not found', function(assert) {
186 | assert.deepEqual(findIndexes([], 'a'), []);
187 | assert.deepEqual(findIndexes([], ''), []);
188 | assert.deepEqual(findIndexes([{ a: true}], 'b'), []);
189 | });
190 |
191 | QUnit.test('indexes found', function(assert) {
192 | assert.deepEqual(findIndexes([{ a: true}], 'a'), [0]);
193 | assert.deepEqual(findIndexes([
194 | { a: true },
195 | { b: true },
196 | { b: true, c: true }
197 | ], 'b'), [1, 2]);
198 | });
199 |
200 | QUnit.module('xml', {
201 | beforeEach() {
202 | const parser = new DOMParser();
203 | const xmlString = `
204 |
205 | foo
206 | bar
207 | baz
208 | `;
209 |
210 | this.fixture = parser.parseFromString(xmlString, 'text/xml').documentElement;
211 | }
212 | });
213 |
214 | QUnit.test('findChildren', function(assert) {
215 | assert.deepEqual(findChildren(this.fixture, 'test').length, 1, 'single');
216 | assert.deepEqual(findChildren(this.fixture, 'div').length, 2, 'multiple');
217 | assert.deepEqual(findChildren(this.fixture, 'el').length, 0, 'none');
218 | });
219 |
220 | QUnit.test('getContent', function(assert) {
221 | const result = findChildren(this.fixture, 'test')[0];
222 |
223 | assert.deepEqual(getContent(result), 'foo', 'gets text and trims');
224 | });
225 |
--------------------------------------------------------------------------------