├── .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 | [![Build Status](https://travis-ci.org/videojs/mpd-parser.svg?branch=master)](https://travis-ci.org/videojs/mpd-parser) 4 | [![Greenkeeper badge](https://badges.greenkeeper.io/videojs/mpd-parser.svg)](https://greenkeeper.io/) 5 | [![Slack Status](http://slack.videojs.com/badge.svg)](http://slack.videojs.com) 6 | 7 | [![NPM](https://nodei.co/npm/mpd-parser.png?downloads=true&downloadRank=true)](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 |
15 | 19 | 20 |
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 | --------------------------------------------------------------------------------