├── .nvmrc ├── .npmignore ├── .editorconfig ├── .gitignore ├── test ├── globals.test.js ├── integration.test.js ├── build.test.js └── dashjs.test.js ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md └── workflows │ └── ci.yml ├── scripts ├── rollup.config.js └── karma.conf.js ├── CONTRIBUTING.md ├── package.json ├── index.html ├── src └── js │ ├── setup-audio-tracks.js │ ├── setup-text-tracks.js │ ├── ttml-text-track-display.js │ └── videojs-dash.js ├── CHANGELOG.md ├── README.md └── LICENSE /.nvmrc: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | .eslintcache 33 | .yo-rc.json 34 | -------------------------------------------------------------------------------- /test/globals.test.js: -------------------------------------------------------------------------------- 1 | import videojs from 'video.js'; 2 | import QUnit from 'qunit'; 3 | import window from 'global/window'; 4 | import '../src/js/videojs-dash.js'; 5 | 6 | QUnit.module('videojs-dash globals'); 7 | 8 | QUnit.test('has expected globals', function(assert) { 9 | assert.ok(videojs.Html5DashJS, 'videojs has "Html5Dash" property'); 10 | assert.ok(window.dashjs, 'global has "dashjs" property'); 11 | assert.ok(window.dashjs.MediaPlayer, 'global has "dashjs.MediaPlayer" property'); 12 | }); 13 | 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | Please describe the change as necessary. 3 | If it's a feature or enhancement please be as detailed as possible. 4 | If it's a bug fix, please link the issue that it fixes or describe the bug in as much detail. 5 | 6 | ## Specific Changes proposed 7 | Please list the specific changes involved in this pull request. 8 | 9 | ## Requirements Checklist 10 | - [ ] Feature implemented / Bug fixed 11 | - [ ] If necessary, more likely in a feature request than a bug fix 12 | - [ ] Unit Tests updated or fixed 13 | - [ ] Docs/guides updated 14 | - [ ] Reviewed by Two Core Contributors 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | Briefly describe the issue. 3 | Include a [reduced test case](https://css-tricks.com/reduced-test-cases/). 4 | 5 | ## Steps to reproduce 6 | Explain in detail the exact steps necessary to reproduce the issue. 7 | 8 | 1. 9 | 2. 10 | 3. 11 | 12 | ## Results 13 | ### Expected 14 | Please describe what you expected to see. 15 | 16 | ### Actual 17 | Please describe what actually happened. 18 | 19 | ### Error output 20 | If there are any errors at all, please include them here. 21 | 22 | ## Additional Information 23 | Please include any additional information necessary here. Including the following: 24 | 25 | ### versions 26 | #### videojs 27 | what version of videojs does this occur with? 28 | 29 | #### browsers 30 | what browser are affected? 31 | 32 | #### OSes 33 | what platforms (operating systems and devices) are affected? 34 | 35 | ### plugins 36 | are any videojs plugins being used on the page? If so, please list them below. 37 | -------------------------------------------------------------------------------- /scripts/rollup.config.js: -------------------------------------------------------------------------------- 1 | const generate = require('videojs-generate-rollup-config'); 2 | const nodeBuiltinsPlugin = require('@gkatsev/rollup-plugin-node-builtins'); 3 | const nodeGlobalsPlugin = require('rollup-plugin-node-globals'); 4 | 5 | // see https://github.com/videojs/videojs-generate-rollup-config 6 | // for options 7 | const options = { 8 | input: 'src/js/videojs-dash.js', 9 | distName: 'videojs-dash', 10 | exportName: 'videojsDash', 11 | // stream and string_decoder are used by some modules 12 | plugins(defaults) { 13 | return { 14 | browser: defaults.browser.concat([ 15 | nodeBuiltinsPlugin(), 16 | nodeGlobalsPlugin() 17 | ]), 18 | module: defaults.module.concat([ 19 | nodeBuiltinsPlugin(), 20 | nodeGlobalsPlugin() 21 | ]), 22 | test: defaults.test.concat([ 23 | nodeBuiltinsPlugin(), 24 | nodeGlobalsPlugin() 25 | ]) 26 | }; 27 | } 28 | }; 29 | const config = generate(options); 30 | 31 | // Add additonal builds/customization here! 32 | 33 | // export the builds to rollup 34 | export default Object.values(config.builds); 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | We welcome contributions from everyone! 4 | 5 | ## Getting Started 6 | 7 | Make sure you have Node.js 4.8 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 | ### Running Tests 20 | 21 | 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]. 22 | 23 | - In all available and supported browsers: `npm test` 24 | - In a specific browser: `npm run test:chrome`, `npm run test:firefox`, etc. 25 | - While development server is running (`npm start`), navigate to [`http://localhost:9999/test/`][local] 26 | 27 | 28 | [karma]: http://karma-runner.github.io/ 29 | [local]: http://localhost:9999/test/ 30 | [conventions]: https://github.com/videojs/generator-videojs-plugin/blob/master/docs/conventions.md 31 | -------------------------------------------------------------------------------- /scripts/karma.conf.js: -------------------------------------------------------------------------------- 1 | const generate = require('videojs-generate-karma-config'); 2 | const isCI = require('is-ci'); 3 | 4 | module.exports = function(config) { 5 | const options = { 6 | serverBrowsers(defaults) { 7 | // run our special chrome in server mode so we get instant test feedback 8 | return ['ChromeHeadlessWithFlags']; 9 | }, 10 | files(defaults) { 11 | // add in dashjs global 12 | defaults.unshift('node_modules/dashjs/dist/dash.all.debug.js'); 13 | 14 | return defaults; 15 | }, 16 | browsers(_browsers) { 17 | // only run on chrome 18 | const browsers = ['ChromeHeadlessWithFlags']; 19 | 20 | if (!isCI) { 21 | browsers.push('FirefoxHeadless'); 22 | } 23 | 24 | return browsers; 25 | }, 26 | customLaunchers(defaults) { 27 | // add no-user-gesture-require variant of chrome 28 | return Object.assign(defaults, { 29 | ChromeHeadlessWithFlags: { 30 | base: 'ChromeHeadless', 31 | flags: ['--no-sandbox', '--autoplay-policy=no-user-gesture-required'] 32 | } 33 | }); 34 | } 35 | }; 36 | 37 | config = generate(config, options); 38 | 39 | // ignore any console logs except for errors 40 | config.browserConsoleLogOptions = { 41 | level: 'error', 42 | terminal: false 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /test/integration.test.js: -------------------------------------------------------------------------------- 1 | import videojs from 'video.js'; 2 | import QUnit from 'qunit'; 3 | import document from 'global/document'; 4 | 5 | const when = function(element, type, fn, condition) { 6 | const func = function() { 7 | if (condition()) { 8 | element.off(type, func); 9 | fn.apply(this, arguments); 10 | } 11 | }; 12 | 13 | element.on(type, func); 14 | }; 15 | 16 | QUnit.module('Integration', { 17 | beforeEach(assert) { 18 | const done = assert.async(); 19 | 20 | this.fixture = document.createElement('div'); 21 | document.body.appendChild(this.fixture); 22 | 23 | const videoEl = document.createElement('video'); 24 | 25 | videoEl.id = 'vid'; 26 | videoEl.setAttribute('controls', ''); 27 | videoEl.setAttribute('width', '600'); 28 | videoEl.setAttribute('height', '300'); 29 | videoEl.setAttribute('muted', 'true'); 30 | videoEl.className = 'video-js vjs-default-skin'; 31 | this.fixture.appendChild(videoEl); 32 | 33 | const player = videojs('vid'); 34 | 35 | this.player = player; 36 | 37 | player.ready(function() { 38 | player.one('loadstart', done); 39 | 40 | player.src({ 41 | src: 'http://dash.edgesuite.net/akamai/bbb_30fps/bbb_30fps.mpd', 42 | type: 'application/dash+xml' 43 | }); 44 | }); 45 | }, 46 | afterEach() { 47 | this.player.dispose(); 48 | } 49 | }); 50 | 51 | QUnit.test('should play', function(assert) { 52 | const done = assert.async(); 53 | 54 | const player = this.player; 55 | 56 | assert.expect(2); 57 | 58 | when(player, 'timeupdate', function() { 59 | assert.ok(true, 'played for at least two seconds'); 60 | assert.equal(player.error(), null, 'has no player errors'); 61 | 62 | done(); 63 | }, function() { 64 | return player.currentTime() >= 2; 65 | }); 66 | 67 | player.play(); 68 | }); 69 | -------------------------------------------------------------------------------- /test/build.test.js: -------------------------------------------------------------------------------- 1 | import QUnit from 'qunit'; 2 | import videojs from 'video.js'; 3 | import document from 'global/document'; 4 | 5 | const when = function(element, type, fn, condition) { 6 | const func = function() { 7 | if (condition()) { 8 | element.off(type, func); 9 | fn.apply(this, arguments); 10 | } 11 | }; 12 | 13 | element.on(type, func); 14 | }; 15 | 16 | QUnit.module('Webpack/Browserify Integration', { 17 | beforeEach(assert) { 18 | const done = assert.async(); 19 | 20 | this.fixture = document.createElement('div'); 21 | document.body.appendChild(this.fixture); 22 | 23 | const videoEl = document.createElement('video'); 24 | 25 | videoEl.id = 'vid'; 26 | videoEl.setAttribute('controls', ''); 27 | videoEl.setAttribute('width', '600'); 28 | videoEl.setAttribute('height', '300'); 29 | videoEl.setAttribute('muted', 'true'); 30 | videoEl.className = 'video-js vjs-default-skin'; 31 | this.fixture.appendChild(videoEl); 32 | 33 | const player = videojs('vid'); 34 | 35 | this.player = player; 36 | 37 | player.ready(function() { 38 | player.one('loadstart', done); 39 | 40 | player.src({ 41 | src: 'http://dash.edgesuite.net/akamai/bbb_30fps/bbb_30fps.mpd', 42 | type: 'application/dash+xml' 43 | }); 44 | }); 45 | }, 46 | afterEach() { 47 | this.player.dispose(); 48 | this.fixture.innerHTML = ''; 49 | } 50 | }); 51 | 52 | QUnit.test('should play', function(assert) { 53 | const 54 | done = assert.async(); 55 | 56 | const player = this.player; 57 | 58 | assert.expect(2); 59 | 60 | when(player, 'timeupdate', function() { 61 | assert.ok(true, 'played for at least two seconds'); 62 | assert.equal(player.error(), null, 'has no player errors'); 63 | 64 | done(); 65 | }, function() { 66 | return player.currentTime() >= 2; 67 | }); 68 | 69 | player.play(); 70 | }); 71 | -------------------------------------------------------------------------------- /.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'}} 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | os: [ubuntu-latest] 25 | env: 26 | BROWSER_STACK_USERNAME: ${{secrets.BROWSER_STACK_USERNAME}} 27 | BROWSER_STACK_ACCESS_KEY: ${{secrets.BROWSER_STACK_ACCESS_KEY}} 28 | runs-on: ${{matrix.os}} 29 | steps: 30 | - name: checkout code 31 | uses: actions/checkout@v3 32 | 33 | - name: Cache dependencies 34 | uses: actions/cache@v3 35 | with: 36 | path: | 37 | ~/.npm 38 | **/node_modules 39 | key: ${{runner.os}}-npm-${{hashFiles('**/package-lock.json')}} 40 | restore-keys: | 41 | ${{runner.os}}-npm- 42 | ${{runner.os}}- 43 | 44 | - name: read node version from .nvmrc 45 | run: echo "NVMRC=$(cat .nvmrc)" >> $GITHUB_OUTPUT 46 | shell: bash 47 | id: nvm 48 | 49 | - name: update apt cache on linux w/o browserstack 50 | run: sudo apt-get update 51 | if: ${{startsWith(matrix.os, 'ubuntu') && !env.BROWSER_STACK_USERNAME}} 52 | 53 | - name: install ffmpeg/pulseaudio for firefox on linux w/o browserstack 54 | run: sudo apt-get install ffmpeg pulseaudio 55 | if: ${{startsWith(matrix.os, 'ubuntu') && !env.BROWSER_STACK_USERNAME}} 56 | 57 | - name: start pulseaudio for firefox on linux w/o browserstack 58 | run: pulseaudio -D 59 | if: ${{startsWith(matrix.os, 'ubuntu') && !env.BROWSER_STACK_USERNAME}} 60 | 61 | - name: setup node 62 | uses: actions/setup-node@v3 63 | with: 64 | node-version: '${{steps.nvm.outputs.NVMRC}}' 65 | 66 | # turn off the default setup-node problem watchers... 67 | - run: echo "::remove-matcher owner=eslint-compact::" 68 | - run: echo "::remove-matcher owner=eslint-stylish::" 69 | - run: echo "::remove-matcher owner=tsc::" 70 | 71 | - name: npm install 72 | run: npm i --prefer-offline --no-audit 73 | 74 | - name: run npm test 75 | uses: coactions/setup-xvfb@v1 76 | with: 77 | run: npm run test 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "videojs-contrib-dash", 3 | "description": "A Video.js source-handler providing MPEG-DASH playback.", 4 | "main": "dist/videojs-dash.cjs.js", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/videojs/videojs-contrib-dash.git" 8 | }, 9 | "version": "5.1.1", 10 | "author": "Brightcove, Inc", 11 | "license": "Apache-2.0", 12 | "scripts": { 13 | "prebuild": "npm run clean", 14 | "build": "npm-run-all -p build:*", 15 | "build:js": "rollup -c scripts/rollup.config.js", 16 | "clean": "shx rm -rf ./dist ./test/dist", 17 | "postclean": "shx mkdir -p ./dist ./test/dist", 18 | "docs": "npm-run-all docs:*", 19 | "docs:api": "jsdoc src -r -d docs/api", 20 | "docs:toc": "doctoc README.md", 21 | "lint": "vjsstandard", 22 | "server": "karma start scripts/karma.conf.js --singleRun=false --auto-watch", 23 | "start": "npm-run-all -p server watch", 24 | "pretest": "npm-run-all lint build", 25 | "test": "karma start scripts/karma.conf.js", 26 | "posttest": "shx cat test/dist/coverage/text.txt", 27 | "update-changelog": "conventional-changelog -p videojs -i CHANGELOG.md -s", 28 | "version": "is-prerelease || npm run update-changelog && git add CHANGELOG.md", 29 | "watch": "npm-run-all -p watch:*", 30 | "watch:js": "npm run build:js -- -w", 31 | "prepublishOnly": "npm run build && vjsverify --skip-require" 32 | }, 33 | "keywords": [ 34 | "MPEG-DASH", 35 | "dash", 36 | "dash.js", 37 | "dashjs", 38 | "playready", 39 | "video.js", 40 | "videojs", 41 | "videojs-plugin", 42 | "widevine" 43 | ], 44 | "browserify-shim": { 45 | "video.js": "global:videojs" 46 | }, 47 | "files": [ 48 | "CONTRIBUTING.md", 49 | "dist/", 50 | "docs/", 51 | "index.html", 52 | "scripts/", 53 | "src/", 54 | "test/" 55 | ], 56 | "dependencies": { 57 | "dashjs": "^4.2.0", 58 | "global": "^4.3.2", 59 | "video.js": "^5.18.0 || ^6 || ^7" 60 | }, 61 | "devDependencies": { 62 | "@gkatsev/rollup-plugin-node-builtins": "^2.1.4", 63 | "conventional-changelog-cli": "^2.0.1", 64 | "conventional-changelog-videojs": "^3.0.1", 65 | "doctoc": "^1.3.1", 66 | "husky": "^1.0.0-rc.13", 67 | "is-ci": "^3.0.0", 68 | "jsdoc": "3.6.6", 69 | "karma": "^6.1.1", 70 | "lint-staged": "^7.2.2", 71 | "not-prerelease": "^1.0.1", 72 | "npm-merge-driver-install": "^2.0.1", 73 | "npm-run-all": "^4.1.5", 74 | "rollup": "^0.66.0", 75 | "rollup-plugin-node-globals": "^1.4.0", 76 | "shx": "^0.3.2", 77 | "sinon": "^6.1.5", 78 | "videojs-generate-karma-config": "^5.0.1", 79 | "videojs-generate-rollup-config": "~2.3.0", 80 | "videojs-generator-verify": "^3.0.1", 81 | "videojs-standard": "~7.1.0" 82 | }, 83 | "module": "dist/videojs-dash.es.js", 84 | "generator-videojs-plugin": { 85 | "version": "7.3.2" 86 | }, 87 | "browserslist": [ 88 | "defaults", 89 | "ie 11" 90 | ], 91 | "vjsstandard": { 92 | "ignore": [ 93 | "dist", 94 | "docs", 95 | "test/dist" 96 | ] 97 | }, 98 | "husky": { 99 | "hooks": { 100 | "pre-commit": "lint-staged" 101 | } 102 | }, 103 | "lint-staged": { 104 | "*.js": [ 105 | "vjsstandard --fix", 106 | "git add" 107 | ], 108 | "README.md": [ 109 | "npm run docs:toc", 110 | "git add" 111 | ] 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test Player 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

BBB

15 | 16 |

BBB - oceans track

17 | 20 |

Angel One - broken subs

21 | 22 |

Angel One - DRM

23 | 24 |

Livesim

25 | 26 |

Livesim - multi-subs

27 | 28 | 29 | 30 | 31 | 32 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /src/js/setup-audio-tracks.js: -------------------------------------------------------------------------------- 1 | import dashjs from 'dashjs'; 2 | import videojs from 'video.js'; 3 | 4 | /** 5 | * Setup audio tracks. Take the tracks from dash and add the tracks to videojs. Listen for when 6 | * videojs changes tracks and apply that to the dash player because videojs doesn't do this 7 | * natively. 8 | * 9 | * @private 10 | * @param {videojs} player the videojs player instance 11 | * @param {videojs.tech} tech the videojs tech being used 12 | */ 13 | function handlePlaybackMetadataLoaded(player, tech) { 14 | const mediaPlayer = player.dash.mediaPlayer; 15 | 16 | const dashAudioTracks = mediaPlayer.getTracksFor('audio'); 17 | const videojsAudioTracks = player.audioTracks(); 18 | 19 | function generateIdFromTrackIndex(index) { 20 | return `dash-audio-${index}`; 21 | } 22 | 23 | function findDashAudioTrack(subDashAudioTracks, videojsAudioTrack) { 24 | return subDashAudioTracks.find(({index}) => 25 | generateIdFromTrackIndex(index) === videojsAudioTrack.id 26 | ); 27 | } 28 | 29 | // Safari creates a single native `AudioTrack` (not `videojs.AudioTrack`) when loading. Clear all 30 | // automatically generated audio tracks so we can create them all ourself. 31 | if (videojsAudioTracks.length) { 32 | tech.clearTracks(['audio']); 33 | } 34 | 35 | const currentAudioTrack = mediaPlayer.getCurrentTrackFor('audio'); 36 | 37 | dashAudioTracks.forEach((dashTrack) => { 38 | let localizedLabel; 39 | 40 | if (Array.isArray(dashTrack.labels)) { 41 | for (let i = 0; i < dashTrack.labels.length; i++) { 42 | if ( 43 | dashTrack.labels[i].lang && 44 | player.language().indexOf(dashTrack.labels[i].lang.toLowerCase()) !== -1 45 | ) { 46 | localizedLabel = dashTrack.labels[i]; 47 | 48 | break; 49 | } 50 | } 51 | } 52 | 53 | let label; 54 | 55 | if (localizedLabel) { 56 | label = localizedLabel.text; 57 | } else if (Array.isArray(dashTrack.labels) && dashTrack.labels.length === 1) { 58 | label = dashTrack.labels[0].text; 59 | } else { 60 | label = dashTrack.lang; 61 | 62 | if (dashTrack.roles && dashTrack.roles.length) { 63 | label += ' (' + dashTrack.roles.join(', ') + ')'; 64 | } 65 | } 66 | 67 | // Add the track to the player's audio track list. 68 | videojsAudioTracks.addTrack( 69 | new videojs.AudioTrack({ 70 | enabled: dashTrack === currentAudioTrack, 71 | id: generateIdFromTrackIndex(dashTrack.index), 72 | kind: dashTrack.kind || 'main', 73 | label, 74 | language: dashTrack.lang 75 | }) 76 | ); 77 | }); 78 | 79 | const audioTracksChangeHandler = () => { 80 | for (let i = 0; i < videojsAudioTracks.length; i++) { 81 | const track = videojsAudioTracks[i]; 82 | 83 | if (track.enabled) { 84 | // Find the audio track we just selected by the id 85 | const dashAudioTrack = findDashAudioTrack(dashAudioTracks, track); 86 | 87 | // Set is as the current track 88 | mediaPlayer.setCurrentTrack(dashAudioTrack); 89 | 90 | // Stop looping 91 | continue; 92 | } 93 | } 94 | }; 95 | 96 | videojsAudioTracks.addEventListener('change', audioTracksChangeHandler); 97 | player.dash.mediaPlayer.on(dashjs.MediaPlayer.events.STREAM_TEARDOWN_COMPLETE, () => { 98 | videojsAudioTracks.removeEventListener('change', audioTracksChangeHandler); 99 | }); 100 | } 101 | 102 | /* 103 | * Call `handlePlaybackMetadataLoaded` when `mediaPlayer` emits 104 | * `dashjs.MediaPlayer.events.PLAYBACK_METADATA_LOADED`. 105 | */ 106 | export default function setupAudioTracks(player, tech) { 107 | // When `dashjs` finishes loading metadata, create audio tracks for `video.js`. 108 | player.dash.mediaPlayer.on( 109 | dashjs.MediaPlayer.events.PLAYBACK_METADATA_LOADED, 110 | handlePlaybackMetadataLoaded.bind(null, player, tech) 111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [5.1.1](https://github.com/videojs/videojs-contrib-dash/compare/v5.1.0...v5.1.1) (2021-11-30) 3 | 4 | ### Bug Fixes 5 | 6 | * update dashjs to 4.2.0 ([#372](https://github.com/videojs/videojs-contrib-dash/issues/372)) ([2f83f71](https://github.com/videojs/videojs-contrib-dash/commit/2f83f71)) 7 | 8 | 9 | # [5.1.0](https://github.com/videojs/videojs-contrib-dash/compare/v5.0.0...v5.1.0) (2021-09-17) 10 | 11 | ### Features 12 | 13 | * ttml support ([#319](https://github.com/videojs/videojs-contrib-dash/issues/319)) ([3859998](https://github.com/videojs/videojs-contrib-dash/commit/3859998)) 14 | 15 | ### Bug Fixes 16 | 17 | * **package:** update dash.js to 4.0.1 ([#369](https://github.com/videojs/videojs-contrib-dash/issues/369)) ([3a7b4a6](https://github.com/videojs/videojs-contrib-dash/commit/3a7b4a6)) 18 | 19 | ### Chores 20 | 21 | * add example that uses TTML subtitles ([#368](https://github.com/videojs/videojs-contrib-dash/issues/368)) ([563aac6](https://github.com/videojs/videojs-contrib-dash/commit/563aac6)) 22 | * don't run tests on version ([6b15e7e](https://github.com/videojs/videojs-contrib-dash/commit/6b15e7e)) 23 | 24 | 25 | # [5.0.0](https://github.com/videojs/videojs-contrib-dash/compare/v4.1.0...v5.0.0) (2021-06-25) 26 | 27 | ### Features 28 | 29 | * Dash.js 4.0.0 ([#363](https://github.com/videojs/videojs-contrib-dash/issues/363)) ([a9d76fc](https://github.com/videojs/videojs-contrib-dash/commit/a9d76fc)) 30 | 31 | ### Chores 32 | 33 | * skip require in vjsverify ([10488f6](https://github.com/videojs/videojs-contrib-dash/commit/10488f6)) 34 | 35 | ### Documentation 36 | 37 | * Update README to show support for Dash 3.x ([#361](https://github.com/videojs/videojs-contrib-dash/issues/361)) ([3854c61](https://github.com/videojs/videojs-contrib-dash/commit/3854c61)), closes [#336](https://github.com/videojs/videojs-contrib-dash/issues/336) 38 | 39 | 40 | ### BREAKING CHANGES 41 | 42 | * no longer able to be required in nodejs. 43 | * update to DASH.js 4.0 44 | 45 | 46 | # [4.1.0](https://github.com/videojs/videojs-contrib-dash/compare/v4.0.1...v4.1.0) (2021-02-18) 47 | 48 | ### Bug Fixes 49 | 50 | * **package:** update to dashjs[@3](https://github.com/3).2.0 ([#356](https://github.com/videojs/videojs-contrib-dash/issues/356)) ([490817d](https://github.com/videojs/videojs-contrib-dash/commit/490817d)) 51 | 52 | 53 | ## [4.0.1](https://github.com/videojs/videojs-contrib-dash/compare/v4.0.0...v4.0.1) (2021-02-18) 54 | 55 | ### Bug Fixes 56 | 57 | * rollup plugins should be dev dependencies ([#352](https://github.com/videojs/videojs-contrib-dash/issues/352)) ([9d09a60](https://github.com/videojs/videojs-contrib-dash/commit/9d09a60)) 58 | 59 | ### Chores 60 | 61 | * setup github ci ([#355](https://github.com/videojs/videojs-contrib-dash/issues/355)) ([e28dbc4](https://github.com/videojs/videojs-contrib-dash/commit/e28dbc4)) 62 | * update deps to resolve all audit issues ([#354](https://github.com/videojs/videojs-contrib-dash/issues/354)) ([c4585b8](https://github.com/videojs/videojs-contrib-dash/commit/c4585b8)) 63 | 64 | 65 | # [4.0.0](https://github.com/videojs/videojs-contrib-dash/compare/v3.0.0...v4.0.0) (2020-11-05) 66 | 67 | ### Features 68 | 69 | * include dash.js in build ([#351](https://github.com/videojs/videojs-contrib-dash/issues/351)) ([6879286](https://github.com/videojs/videojs-contrib-dash/commit/6879286)) 70 | 71 | CHANGELOG 72 | ========= 73 | 74 | ## 3.0.0 (2020-10-26) 75 | * BREAKING CHANGE: Update Dash.js to 3.1.3 (major version 3) 76 | 77 | ## 2.11.0 (2019-03-08) 78 | * Fix bug where VTT captions wouldn't show 79 | * Support for human-readable track labels 80 | 81 | ## 2.10.1 (2018-12-18) 82 | * Change main to be `dist/videojs-dash.cjs.js` 83 | * Reformat test code 84 | * Add v7 to list of supported video.js dependencies 85 | 86 | ## 2.10.0 (2018-07-30) 87 | * Cleanup of event addition and removal on dispose to not bork on source change 88 | * Use MPD type and duration to determine if we should report live status 89 | * Add error handler for new `mssError` in dash.js 2.6.8 90 | * Pass through text track kind to dash.js 91 | 92 | ## 2.9.3 (2018-04-12) 93 | * Retrigger dash.js errors on video.js 94 | 95 | ## 2.9.2 (2017-10-11) 96 | * Depend on either Video.js 5.x or 6.x 97 | 98 | ## 2.9.1 (2017-06-15) 99 | * Fix text tracks in IE 100 | 101 | ## 2.9.0 (2017-05-11) 102 | * Load text tracks from dashjs into video.js 103 | 104 | ## 2.8.2 (2017-04-26) 105 | * Show role in audio track label 106 | 107 | ## 2.8.1 (2017-02-09) 108 | * Call update source hook in canHandleSource 109 | 110 | ## 2.8.0 (2017-02-02) 111 | * Add support for multiple audio tracks 112 | * Introduce videojs 6 forward compatibility while maintaining backward compatibility 113 | 114 | ## 2.7.1 (2017-02-02) 115 | * Allow dash config object to accept setters with multiple args 116 | 117 | ## 2.7.0 (2017-01-30) 118 | * Support all dash.js configuration options 119 | 120 | ## 2.6.1 (2017-01-05) 121 | * Fixed Live display for live streams 122 | 123 | ## 2.6.0 (2016-12-12) 124 | * Added initialization and update source hooks. 125 | 126 | ## 2.5.2 (2016-11-22) 127 | * Don't pass empty object for key systems. 128 | 129 | ## 2.5.1 (2016-09-09) 130 | * Skip source if requestMediaKeySystemAccess isn't present for key system 131 | 132 | ## 2.5.0 (2016-08-24) 133 | * Expose mediaPlayer on player 134 | 135 | ## 2.4.0 (2016-07-07) 136 | * ES6 rewrite 137 | * Allow to pass option to limit bitrate by portal size 138 | 139 | ## 2.3.0 (2016-05-10) 140 | * Add a hook before dash.js media player initialization 141 | 142 | ## 2.2.0 (2016-05-04) 143 | * Added browserify support 144 | * Remove manifest parsing 145 | 146 | ## 2.1.0 (2016-03-08) 147 | * Update project to support dash.js 2.0 148 | * Update deprecated `laURL` to utilize new `serverURL` 149 | * Add canPlayType 150 | 151 | ## 2.0.0 (2015-10-16) 152 | * Update project to be compatible with video.js 5.0 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # video.js MPEG-DASH Source Handler 2 | 3 | [![Build Status](https://travis-ci.org/videojs/videojs-contrib-dash.svg?branch=master)](https://travis-ci.org/videojs/videojs-contrib-dash) 4 | [![Greenkeeper badge](https://badges.greenkeeper.io/videojs/videojs-contrib-dash.svg)](https://greenkeeper.io/) 5 | [![Slack Status](http://slack.videojs.com/badge.svg)](http://slack.videojs.com) 6 | 7 | [![NPM](https://nodei.co/npm/videojs-contrib-dash.png?downloads=true&downloadRank=true)](https://nodei.co/npm/videojs-contrib-dash/) 8 | 9 | A video.js source handler for supporting MPEG-DASH playback through a video.js player on browsers with support for Media Source Extensions. 10 | 11 | __Supported Dash.js version: 4.x__ 12 | 13 | Maintenance Status: Stable 14 | 15 | Drop by our slack channel (#playback) on the [Video.js slack](http://slack.videojs.com). 16 | 17 | 18 | 19 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 20 | 21 | - [Getting Started](#getting-started) 22 | - [Protected Content](#protected-content) 23 | - [Captions](#captions) 24 | - [Using TTML Captions](#using-ttml-captions) 25 | - [Multi-Language Labels](#multi-language-labels) 26 | - [Passing options to Dash.js](#passing-options-to-dashjs) 27 | - [Deprecation Warning](#deprecation-warning) 28 | - [Initialization Hook](#initialization-hook) 29 | 30 | 31 | 32 | ## Getting Started 33 | 34 | Download [Dash.js](https://github.com/Dash-Industry-Forum/dash.js/releases) and [videojs-contrib-dash](https://github.com/videojs/videojs-contrib-dash/releases). Include them both in your web page along with video.js: 35 | 36 | ```html 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 58 | ``` 59 | 60 | Checkout our [live example](http://videojs.github.io/videojs-contrib-dash/) if you're having trouble. 61 | 62 | ## Protected Content 63 | 64 | If the browser supports Encrypted Media Extensions and includes a Content Decryption Module for one of the protection schemes in the dash manifest, video.js will be able to playback protected content. 65 | 66 | For most protection schemes, the license server information (URL & init data) is included inside the manifest. The notable exception to this is Widevine-Modular (WV). To playback WV content, you must provide the URL to a Widevine license server proxy. 67 | 68 | For this purpose, videojs-contrib-dash adds support for a "keySystemOptions" array to the object when using the `player.src()` function: 69 | 70 | ```javascript 71 | player.src({ 72 | src: 'http://example.com/my/manifest.mpd', 73 | type: 'application/dash+xml', 74 | keySystemOptions: [ 75 | { 76 | name: 'com.widevine.alpha', 77 | options: { 78 | serverURL: 'http://m.widevine.com/proxy' 79 | } 80 | } 81 | ] 82 | }); 83 | ``` 84 | 85 | You may also manipulate the source object by registering a function to the `updatesource` hook. Your function should take a source object as an argument and should return a source object. 86 | 87 | ```javascript 88 | var updateSourceData = function(source) { 89 | source.keySystemOptions = [{ 90 | name: 'com.widevine.alpha', 91 | options: { 92 | serverURL:'https://example.com/anotherlicense' 93 | } 94 | }]; 95 | return source; 96 | }; 97 | 98 | videojs.Html5DashJS.hook('updatesource', updateSourceData); 99 | ``` 100 | 101 | ## Captions 102 | 103 | As of `video.js@5.14`, native captions are no longer supported on any browser besides Safari. Dash can handle captions referenced embedded vtt files, embedded captions in the manifest, and with fragmented text streaming. It is impossible to use video.js captions when dash.js is using fragmented text captions, so the user must disable native captions when using `videojs-contrib-dash`. 104 | 105 | ```javascript 106 | videojs('example-video', { 107 | html5: { 108 | nativeCaptions: false 109 | } 110 | }); 111 | ``` 112 | 113 | A warning will be logged if this setting is not applied. 114 | 115 | ### Using TTML Captions 116 | 117 | TTML captions require special rendering by dash.js. To enable this rendering, you must set option `useTTML` to `true`, like so: 118 | 119 | ```javascript 120 | videojs('example-video', { 121 | html5: { 122 | dash: { 123 | useTTML: true 124 | } 125 | } 126 | }); 127 | ``` 128 | 129 | This option is not `true` by default because it will also render CEA608 captions in the same method, and there may be some errors in their display. However, it does enable styling captions via the captions settings dialog. 130 | 131 | ## Multi-Language Labels 132 | 133 | When labels in a playlist file are in multiple languages, the 2-character language code should be used if it exists; this allows the player to auto-select the appropriate label. 134 | 135 | ## Passing options to Dash.js 136 | 137 | It is possible to pass options to Dash.js during initialiation of video.js. All methods in the [`Dash.js#MediaPlayer` docs](http://cdn.dashjs.org/latest/jsdoc/module-MediaPlayer.html) are supported. 138 | 139 | To set these options, pass the exact function name with a scalar or array value to call the correpsonding MediaPlayer function. 140 | 141 | For example: 142 | 143 | ```javascript 144 | var player = videojs('example-video', { 145 | html5: { 146 | dash: { 147 | setLimitBitrateByPortal: true, 148 | setMaxAllowedBitrateFor: ['video', 2000] 149 | } 150 | } 151 | }); 152 | ``` 153 | 154 | A warning will be logged if the configuration property is not found. 155 | 156 | ### Deprecation Warning 157 | 158 | Previously the `set` prefix was expected to be omitted. This has been deprecated and will be removed in a future version. 159 | 160 | ## Initialization Hook 161 | 162 | Sometimes you may need to extend Dash.js, or have access to the Dash.js MediaPlayer before it is initialized. For these cases, you can register a function to the `beforeinitialize` hook, which will be called just before the Dash.js MediaPlayer is initialized. 163 | 164 | Your function should have two parameters: 165 | 1. The video.js Player instance 166 | 2. The Dash.js MediaPlayer instance 167 | 168 | ```javascript 169 | var myCustomCallback = function(player, mediaPlayer) { 170 | // Log MediaPlayer messages through video.js 171 | if (videojs && videojs.log) { 172 | mediaPlayer.getDebug().setLogToBrowserConsole(false); 173 | mediaPlayer.on('log', function(event) { 174 | videojs.log(event.message); 175 | }); 176 | } 177 | }; 178 | 179 | videojs.Html5DashJS.hook('beforeinitialize', myCustomCallback); 180 | ``` 181 | -------------------------------------------------------------------------------- /src/js/setup-text-tracks.js: -------------------------------------------------------------------------------- 1 | import dashjs from 'dashjs'; 2 | import videojs from 'video.js'; 3 | import window from 'global/window'; 4 | 5 | function find(l, f) { 6 | for (let i = 0; i < l.length; i++) { 7 | if (f(l[i])) { 8 | return l[i]; 9 | } 10 | } 11 | } 12 | 13 | /* 14 | * Attach text tracks from dash.js to videojs 15 | * 16 | * @param {videojs} player the videojs player instance 17 | * @param {array} tracks the tracks loaded by dash.js to attach to videojs 18 | * 19 | * @private 20 | */ 21 | function attachDashTextTracksToVideojs(player, tech, tracks) { 22 | const trackDictionary = []; 23 | 24 | // Add remote tracks 25 | const tracksAttached = tracks 26 | // Map input data to match HTMLTrackElement spec 27 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLTrackElement 28 | .map((track) => { 29 | let localizedLabel; 30 | 31 | if (Array.isArray(track.labels)) { 32 | for (let i = 0; i < track.labels.length; i++) { 33 | if ( 34 | track.labels[i].lang && 35 | player.language().indexOf(track.labels[i].lang.toLowerCase()) !== -1 36 | ) { 37 | localizedLabel = track.labels[i]; 38 | 39 | break; 40 | } 41 | } 42 | } 43 | 44 | let label; 45 | 46 | if (localizedLabel) { 47 | label = localizedLabel.text; 48 | } else if (Array.isArray(track.labels) && track.labels.length === 1) { 49 | label = track.labels[0].text; 50 | } else { 51 | label = track.lang || track.label; 52 | } 53 | 54 | return { 55 | dashTrack: track, 56 | trackConfig: { 57 | label, 58 | language: track.lang, 59 | srclang: track.lang, 60 | kind: track.kind 61 | } 62 | }; 63 | }) 64 | 65 | // Add track to videojs track list 66 | .map(({trackConfig, dashTrack}) => { 67 | if (dashTrack.isTTML && !player.getChild('TTMLTextTrackDisplay')) { 68 | return null; 69 | } 70 | 71 | const remoteTextTrack = player.addRemoteTextTrack(trackConfig, false); 72 | 73 | trackDictionary.push({textTrack: remoteTextTrack.track, dashTrack}); 74 | 75 | // Don't add the cues becuase we're going to let dash handle it natively. This will ensure 76 | // that dash handle external time text files and fragmented text tracks. 77 | // 78 | // Example file with external time text files: 79 | // https://storage.googleapis.com/shaka-demo-assets/sintel-mp4-wvtt/dash.mpd 80 | 81 | return remoteTextTrack; 82 | }) 83 | .filter(el => el !== null) 84 | 85 | ; 86 | 87 | /* 88 | * Scan `videojs.textTracks()` to find one that is showing. Set the dash text track. 89 | */ 90 | function updateActiveDashTextTrack() { 91 | const dashMediaPlayer = player.dash.mediaPlayer; 92 | const textTracks = player.textTracks(); 93 | let activeTextTrackIndex = -1; 94 | 95 | // Iterate through the tracks and find the one marked as showing. If none are showing, 96 | // `activeTextTrackIndex` will be set to `-1`, disabling text tracks. 97 | for (let i = 0; i < textTracks.length; i += 1) { 98 | const textTrack = textTracks[i]; 99 | 100 | if (textTrack.mode === 'showing') { 101 | // Find the dash track we want to use 102 | 103 | /* jshint loopfunc: true */ 104 | const dictionaryLookupResult = find(trackDictionary, 105 | (track) => track.textTrack === textTrack); 106 | /* jshint loopfunc: false */ 107 | 108 | const dashTrackToActivate = dictionaryLookupResult ? 109 | dictionaryLookupResult.dashTrack : 110 | null; 111 | 112 | // If we found a track, get it's index. 113 | if (dashTrackToActivate) { 114 | activeTextTrackIndex = tracks.indexOf(dashTrackToActivate); 115 | } 116 | } 117 | } 118 | 119 | // If the text track has changed, then set it in dash 120 | if (activeTextTrackIndex !== dashMediaPlayer.getCurrentTextTrackIndex()) { 121 | dashMediaPlayer.setTextTrack(activeTextTrackIndex); 122 | } 123 | } 124 | 125 | // Update dash when videojs's selected text track changes. 126 | player.textTracks().on('change', updateActiveDashTextTrack); 127 | 128 | // Cleanup event listeners whenever we start loading a new source 129 | player.dash.mediaPlayer.on(dashjs.MediaPlayer.events.STREAM_TEARDOWN_COMPLETE, () => { 130 | player.textTracks().off('change', updateActiveDashTextTrack); 131 | }); 132 | 133 | // Initialize the text track on our first run-through 134 | updateActiveDashTextTrack(); 135 | 136 | return tracksAttached; 137 | } 138 | 139 | /* 140 | * Wait for dash to emit `TEXT_TRACKS_ADDED` and then attach the text tracks loaded by dash if 141 | * we're not using native text tracks. 142 | * 143 | * @param {videojs} player the videojs player instance 144 | * @private 145 | */ 146 | export default function setupTextTracks(player, tech, options) { 147 | // Clear VTTCue if it was shimmed by vttjs and let dash.js use TextTrackCue. 148 | // This is necessary because dash.js creates text tracks 149 | // using addTextTrack which is incompatible with vttjs.VTTCue in IE11 150 | if (window.VTTCue && !(/\[native code\]/).test(window.VTTCue.toString())) { 151 | window.VTTCue = false; 152 | } 153 | 154 | // Store the tracks that we've added so we can remove them later. 155 | let dashTracksAttachedToVideoJs = []; 156 | 157 | // We're relying on the user to disable native captions. Show an error if they didn't do so. 158 | if (tech.featuresNativeTextTracks) { 159 | videojs.log.error('You must pass {html: {nativeCaptions: false}} in the videojs constructor ' + 160 | 'to use text tracks in videojs-contrib-dash'); 161 | return; 162 | } 163 | 164 | const mediaPlayer = player.dash.mediaPlayer; 165 | 166 | // Clear the tracks that we added. We don't clear them all because someone else can add tracks. 167 | function clearDashTracks() { 168 | dashTracksAttachedToVideoJs.forEach(player.removeRemoteTextTrack.bind(player)); 169 | 170 | dashTracksAttachedToVideoJs = []; 171 | } 172 | 173 | function handleTextTracksAdded({index, tracks}) { 174 | // Stop listening for this event. We only want to hear it once. 175 | mediaPlayer.off(dashjs.MediaPlayer.events.TEXT_TRACKS_ADDED, handleTextTracksAdded); 176 | 177 | // Cleanup old tracks 178 | clearDashTracks(); 179 | 180 | if (!tracks.length) { 181 | // Don't try to add text tracks if there aren't any 182 | return; 183 | } 184 | 185 | // Save the tracks so we can remove them later 186 | dashTracksAttachedToVideoJs = attachDashTextTracksToVideojs(player, tech, tracks, options); 187 | } 188 | 189 | // Attach dash text tracks whenever we dash emits `TEXT_TRACKS_ADDED`. 190 | mediaPlayer.on(dashjs.MediaPlayer.events.TEXT_TRACKS_ADDED, handleTextTracksAdded); 191 | 192 | // When the player can play, remove the initialization events. We might not have received 193 | // TEXT_TRACKS_ADDED` so we have to stop listening for it or we'll get errors when we load new 194 | // videos and are listening for the same event in multiple places, including cleaned up 195 | // mediaPlayers. 196 | mediaPlayer.on(dashjs.MediaPlayer.events.CAN_PLAY, () => { 197 | mediaPlayer.off(dashjs.MediaPlayer.events.TEXT_TRACKS_ADDED, handleTextTracksAdded); 198 | }); 199 | } 200 | -------------------------------------------------------------------------------- /src/js/ttml-text-track-display.js: -------------------------------------------------------------------------------- 1 | import videojs from 'video.js'; 2 | import dashjs from 'dashjs'; 3 | import window from 'global/window'; 4 | 5 | const Component = videojs.getComponent('Component'); 6 | 7 | const darkGray = '#222'; 8 | const lightGray = '#ccc'; 9 | const fontMap = { 10 | monospace: 'monospace', 11 | sansSerif: 'sans-serif', 12 | serif: 'serif', 13 | monospaceSansSerif: '"Andale Mono", "Lucida Console", monospace', 14 | monospaceSerif: '"Courier New", monospace', 15 | proportionalSansSerif: 'sans-serif', 16 | proportionalSerif: 'serif', 17 | casual: '"Comic Sans MS", Impact, fantasy', 18 | script: '"Monotype Corsiva", cursive', 19 | smallcaps: '"Andale Mono", "Lucida Console", monospace, sans-serif' 20 | }; 21 | 22 | /** 23 | * Try to update the style of a DOM element. Some style changes will throw an error, 24 | * particularly in IE8. Those should be noops. 25 | * 26 | * @param {Element} el 27 | * The DOM element to be styled. 28 | * 29 | * @param {string} style 30 | * The CSS property on the element that should be styled. 31 | * 32 | * @param {string} rule 33 | * The style rule that should be applied to the property. 34 | * 35 | * @private 36 | */ 37 | function tryUpdateStyle(el, style, rule) { 38 | try { 39 | el.style[style] = rule; 40 | } catch (e) { 41 | 42 | // Satisfies linter. 43 | return; 44 | } 45 | } 46 | 47 | function removeStyle(el) { 48 | if (el.style) { 49 | el.style.left = null; 50 | el.style.width = '100%'; 51 | } 52 | for (const i in el.children) { 53 | removeStyle(el.children[i]); 54 | } 55 | } 56 | 57 | /** 58 | * Construct an rgba color from a given hex color code. 59 | * 60 | * @param {number} color 61 | * Hex number for color, like #f0e or #f604e2. 62 | * 63 | * @param {number} opacity 64 | * Value for opacity, 0.0 - 1.0. 65 | * 66 | * @return {string} 67 | * The rgba color that was created, like 'rgba(255, 0, 0, 0.3)'. 68 | */ 69 | export function constructColor(color, opacity) { 70 | let hex; 71 | 72 | if (color.length === 4) { 73 | // color looks like "#f0e" 74 | hex = color[1] + color[1] + color[2] + color[2] + color[3] + color[3]; 75 | } else if (color.length === 7) { 76 | // color looks like "#f604e2" 77 | hex = color.slice(1); 78 | } else { 79 | throw new Error('Invalid color code provided, ' + color + '; must be formatted as e.g. #f0e or #f604e2.'); 80 | } 81 | return 'rgba(' + 82 | parseInt(hex.slice(0, 2), 16) + ',' + 83 | parseInt(hex.slice(2, 4), 16) + ',' + 84 | parseInt(hex.slice(4, 6), 16) + ',' + 85 | opacity + ')'; 86 | } 87 | 88 | /** 89 | * The component for displaying text track cues. 90 | * 91 | * @extends Component 92 | */ 93 | class TTMLTextTrackDisplay extends Component { 94 | 95 | /** 96 | * Creates an instance of this class. 97 | * 98 | * @param {Player} player 99 | * The `Player` that this class should be attached to. 100 | * 101 | * @param {Object} [options] 102 | * The key/value store of player options. 103 | * 104 | * @param {Component~ReadyCallback} [ready] 105 | * The function to call when `TextTrackDisplay` is ready. 106 | */ 107 | constructor(player, options, ready) { 108 | super(player, videojs.mergeOptions(options, {playerOptions: {}}), ready); 109 | const selects = player.getChild('TextTrackSettings').$$('select'); 110 | 111 | for (let i = 0; i < selects.length; i++) { 112 | this.on(selects[i], 'change', this.updateStyle.bind(this)); 113 | } 114 | player.dash.mediaPlayer.on(dashjs.MediaPlayer.events.CAPTION_RENDERED, 115 | this.updateStyle.bind(this)); 116 | } 117 | 118 | /** 119 | * Create the {@link Component}'s DOM element. 120 | * 121 | * @return {Element} 122 | * The element that was created. 123 | */ 124 | createEl() { 125 | const newEl = super.createEl('div', { 126 | className: 'vjs-text-track-display-ttml' 127 | }, { 128 | 'aria-live': 'off', 129 | 'aria-atomic': 'true' 130 | }); 131 | 132 | newEl.style.position = 'absolute'; 133 | newEl.style.left = '0'; 134 | newEl.style.right = '0'; 135 | newEl.style.top = '0'; 136 | newEl.style.bottom = '0'; 137 | newEl.style.margin = '1.5%'; 138 | return newEl; 139 | } 140 | 141 | updateStyle({captionDiv}) { 142 | if (!this.player_.textTrackSettings) { 143 | return; 144 | } 145 | 146 | const overrides = this.player_.textTrackSettings.getValues(); 147 | 148 | captionDiv = captionDiv || this.player_.getChild('TTMLTextTrackDisplay') 149 | .el().firstChild; 150 | if (!captionDiv) { 151 | return; 152 | } 153 | 154 | removeStyle(captionDiv); 155 | const spans = captionDiv.getElementsByTagName('span'); 156 | 157 | for (let i = 0; i < spans.length; i++) { 158 | const span = spans[i]; 159 | 160 | span.parentNode.style.textAlign = 'center'; 161 | if (overrides.color) { 162 | span.style.color = overrides.color; 163 | } 164 | if (overrides.textOpacity) { 165 | tryUpdateStyle( 166 | span, 167 | 'color', 168 | constructColor( 169 | overrides.color || '#fff', 170 | overrides.textOpacity 171 | ) 172 | ); 173 | } 174 | if (overrides.backgroundColor) { 175 | span.style.backgroundColor = overrides.backgroundColor; 176 | } 177 | if (overrides.backgroundOpacity) { 178 | tryUpdateStyle( 179 | span, 180 | 'backgroundColor', 181 | constructColor( 182 | overrides.backgroundColor || '#000', 183 | overrides.backgroundOpacity 184 | ) 185 | ); 186 | } 187 | if (overrides.windowColor) { 188 | if (overrides.windowOpacity) { 189 | tryUpdateStyle( 190 | span.parentNode, 191 | 'backgroundColor', 192 | constructColor(overrides.windowColor, overrides.windowOpacity) 193 | ); 194 | } else { 195 | span.parent.style.backgroundColor = overrides.windowColor; 196 | } 197 | } 198 | if (overrides.edgeStyle) { 199 | if (overrides.edgeStyle === 'dropshadow') { 200 | span.style.textShadow = `2px 2px 3px ${darkGray}, 2px 2px 4px ${darkGray}, 2px 2px 5px ${darkGray}`; 201 | } else if (overrides.edgeStyle === 'raised') { 202 | span.style.textShadow = `1px 1px ${darkGray}, 2px 2px ${darkGray}, 3px 3px ${darkGray}`; 203 | } else if (overrides.edgeStyle === 'depressed') { 204 | span.style.textShadow = `1px 1px ${lightGray}, 0 1px ${lightGray}, -1px -1px ${darkGray}, 0 -1px ${darkGray}`; 205 | } else if (overrides.edgeStyle === 'uniform') { 206 | span.style.textShadow = `0 0 4px ${darkGray}, 0 0 4px ${darkGray}, 0 0 4px ${darkGray}, 0 0 4px ${darkGray}`; 207 | } 208 | } 209 | if (overrides.fontPercent && overrides.fontPercent !== 1) { 210 | const fontSize = window.parseFloat(span.style.fontSize); 211 | 212 | span.style.fontSize = (fontSize * overrides.fontPercent) + 'px'; 213 | span.style.height = 'auto'; 214 | span.style.top = 'auto'; 215 | span.style.bottom = '2px'; 216 | } 217 | if (overrides.fontFamily && overrides.fontFamily !== 'default') { 218 | if (overrides.fontFamily === 'small-caps') { 219 | span.style.fontVariant = 'small-caps'; 220 | } else { 221 | span.style.fontFamily = fontMap[overrides.fontFamily]; 222 | } 223 | } 224 | } 225 | } 226 | 227 | } 228 | videojs.registerComponent('TTMLTextTrackDisplay', TTMLTextTrackDisplay); 229 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /src/js/videojs-dash.js: -------------------------------------------------------------------------------- 1 | import window from 'global/window'; 2 | import videojs from 'video.js'; 3 | import dashjs from 'dashjs'; 4 | import setupAudioTracks from './setup-audio-tracks'; 5 | import setupTextTracks from './setup-text-tracks'; 6 | import document from 'global/document'; 7 | import './ttml-text-track-display'; 8 | 9 | /** 10 | * videojs-contrib-dash 11 | * 12 | * Use Dash.js to playback DASH content inside of Video.js via a SourceHandler 13 | */ 14 | class Html5DashJS { 15 | constructor(source, tech, options) { 16 | // Get options from tech if not provided for backwards compatibility 17 | options = options || tech.options_; 18 | 19 | this.player = videojs(options.playerId); 20 | this.player.dash = this.player.dash || {}; 21 | 22 | this.tech_ = tech; 23 | this.el_ = tech.el(); 24 | this.elParent_ = this.el_.parentNode; 25 | this.hasFiniteDuration_ = false; 26 | 27 | // Do nothing if the src is falsey 28 | if (!source.src) { 29 | return; 30 | } 31 | 32 | // While the manifest is loading and Dash.js has not finished initializing 33 | // we must defer events and functions calls with isReady_ and then `triggerReady` 34 | // again later once everything is setup 35 | tech.isReady_ = false; 36 | 37 | if (Html5DashJS.updateSourceData) { 38 | videojs.log.warn('updateSourceData has been deprecated.' + 39 | ' Please switch to using hook("updatesource", callback).'); 40 | source = Html5DashJS.updateSourceData(source); 41 | } 42 | 43 | // call updatesource hooks 44 | Html5DashJS.hooks('updatesource').forEach((hook) => { 45 | source = hook(source); 46 | }); 47 | 48 | const manifestSource = source.src; 49 | 50 | this.keySystemOptions_ = Html5DashJS.buildDashJSProtData(source.keySystemOptions); 51 | 52 | this.player.dash.mediaPlayer = dashjs.MediaPlayer().create(); 53 | 54 | this.mediaPlayer_ = this.player.dash.mediaPlayer; 55 | 56 | // Log MedaPlayer messages through video.js 57 | if (Html5DashJS.useVideoJSDebug) { 58 | videojs.log.warn('useVideoJSDebug has been deprecated.' + 59 | ' Please switch to using hook("beforeinitialize", callback).'); 60 | Html5DashJS.useVideoJSDebug(this.mediaPlayer_); 61 | } 62 | 63 | if (Html5DashJS.beforeInitialize) { 64 | videojs.log.warn('beforeInitialize has been deprecated.' + 65 | ' Please switch to using hook("beforeinitialize", callback).'); 66 | Html5DashJS.beforeInitialize(this.player, this.mediaPlayer_); 67 | } 68 | 69 | Html5DashJS.hooks('beforeinitialize').forEach((hook) => { 70 | hook(this.player, this.mediaPlayer_); 71 | }); 72 | 73 | // Must run controller before these two lines or else there is no 74 | // element to bind to. 75 | this.mediaPlayer_.initialize(); 76 | 77 | // Retrigger a dash.js-specific error event as a player error 78 | // See src/streaming/utils/ErrorHandler.js in dash.js code 79 | // Handled with error (playback is stopped): 80 | // - capabilityError 81 | // - downloadError 82 | // - manifestError 83 | // - mediaSourceError 84 | // - mediaKeySessionError 85 | // Not handled: 86 | // - timedTextError (video can still play) 87 | // - mediaKeyMessageError (only fires under 'might not work' circumstances) 88 | this.retriggerError_ = (event) => { 89 | if (event.error === 'capability' && event.event === 'mediasource') { 90 | // No support for MSE 91 | this.player.error({ 92 | code: 4, 93 | message: 'The media cannot be played because it requires a feature ' + 94 | 'that your browser does not support.' 95 | }); 96 | 97 | } else if (event.error === 'manifestError' && ( 98 | // Manifest type not supported 99 | (event.event.id === 'createParser') || 100 | // Codec(s) not supported 101 | (event.event.id === 'codec') || 102 | // No streams available to stream 103 | (event.event.id === 'nostreams') || 104 | // Error creating Stream object 105 | (event.event.id === 'nostreamscomposed') || 106 | // syntax error parsing the manifest 107 | (event.event.id === 'parse') || 108 | // a stream has multiplexed audio+video 109 | (event.event.id === 'multiplexedrep') 110 | )) { 111 | // These errors have useful error messages, so we forward it on 112 | this.player.error({code: 4, message: event.event.message}); 113 | 114 | } else if (event.error === 'mediasource') { 115 | // This error happens when dash.js fails to allocate a SourceBuffer 116 | // OR the underlying video element throws a `MediaError`. 117 | // If it's a buffer allocation fail, the message states which buffer 118 | // (audio/video/text) failed allocation. 119 | // If it's a `MediaError`, dash.js inspects the error object for 120 | // additional information to append to the error type. 121 | if (event.event.match('MEDIA_ERR_ABORTED')) { 122 | this.player.error({code: 1, message: event.event}); 123 | } else if (event.event.match('MEDIA_ERR_NETWORK')) { 124 | this.player.error({code: 2, message: event.event}); 125 | } else if (event.event.match('MEDIA_ERR_DECODE')) { 126 | this.player.error({code: 3, message: event.event}); 127 | } else if (event.event.match('MEDIA_ERR_SRC_NOT_SUPPORTED')) { 128 | this.player.error({code: 4, message: event.event}); 129 | } else if (event.event.match('MEDIA_ERR_ENCRYPTED')) { 130 | this.player.error({code: 5, message: event.event}); 131 | } else if (event.event.match('UNKNOWN')) { 132 | // We shouldn't ever end up here, since this would mean a 133 | // `MediaError` thrown by the video element that doesn't comply 134 | // with the W3C spec. But, since we should handle the error, 135 | // throwing a MEDIA_ERR_SRC_NOT_SUPPORTED is probably the 136 | // most reasonable thing to do. 137 | this.player.error({code: 4, message: event.event}); 138 | } else { 139 | // Buffer allocation error 140 | this.player.error({code: 4, message: event.event}); 141 | } 142 | 143 | } else if (event.error === 'capability' && event.event === 'encryptedmedia') { 144 | // Browser doesn't support EME 145 | this.player.error({ 146 | code: 5, 147 | message: 'The media cannot be played because it requires encryption ' + 148 | 'features that your browser does not support.' 149 | }); 150 | 151 | } else if (event.error === 'key_session') { 152 | // This block handles pretty much all errors thrown by the 153 | // encryption subsystem 154 | this.player.error({ 155 | code: 5, 156 | message: event.event 157 | }); 158 | 159 | } else if (event.error === 'download') { 160 | this.player.error({ 161 | code: 2, 162 | message: 'The media playback was aborted because too many consecutive ' + 163 | 'download errors occurred.' 164 | }); 165 | 166 | } else if (event.error === 'mssError') { 167 | this.player.error({ 168 | code: 3, 169 | message: event.event 170 | }); 171 | 172 | } else { 173 | // ignore the error 174 | return; 175 | } 176 | 177 | // only reset the dash player in 10ms async, so that the rest of the 178 | // calling function finishes 179 | setTimeout(() => { 180 | this.mediaPlayer_.reset(); 181 | }, 10); 182 | }; 183 | 184 | this.mediaPlayer_.on(dashjs.MediaPlayer.events.ERROR, this.retriggerError_); 185 | 186 | this.getDuration_ = (event) => { 187 | const periods = event.data.Period_asArray; 188 | const oldHasFiniteDuration = this.hasFiniteDuration_; 189 | 190 | if (event.data.mediaPresentationDuration || periods[periods.length - 1].duration) { 191 | this.hasFiniteDuration_ = true; 192 | } else { 193 | // in case we run into a weird situation where we're VOD but then 194 | // switch to live 195 | this.hasFiniteDuration_ = false; 196 | } 197 | 198 | if (this.hasFiniteDuration_ !== oldHasFiniteDuration) { 199 | this.player.trigger('durationchange'); 200 | } 201 | }; 202 | 203 | this.mediaPlayer_.on(dashjs.MediaPlayer.events.MANIFEST_LOADED, this.getDuration_); 204 | 205 | // Apply all dash options that are set 206 | if (options.dash) { 207 | Object.keys(options.dash).forEach((key) => { 208 | if (key === 'useTTML') { 209 | return; 210 | } 211 | 212 | const dashOptionsKey = 'set' + key.charAt(0).toUpperCase() + key.slice(1); 213 | let value = options.dash[key]; 214 | 215 | if (this.mediaPlayer_.hasOwnProperty(dashOptionsKey)) { 216 | // Providing a key without `set` prefix is now deprecated. 217 | videojs.log.warn('Using dash options in videojs-contrib-dash without the set prefix ' + 218 | `has been deprecated. Change '${key}' to '${dashOptionsKey}'`); 219 | 220 | // Set key so it will still work 221 | key = dashOptionsKey; 222 | } 223 | 224 | if (!this.mediaPlayer_.hasOwnProperty(key)) { 225 | videojs.log.warn( 226 | `Warning: dash configuration option unrecognized: ${key}` 227 | ); 228 | 229 | return; 230 | } 231 | 232 | // Guarantee `value` is an array 233 | if (!Array.isArray(value)) { 234 | value = [value]; 235 | } 236 | 237 | this.mediaPlayer_[key](...value); 238 | }); 239 | } 240 | 241 | this.mediaPlayer_.attachView(this.el_); 242 | 243 | if (options.dash && options.dash.useTTML) { 244 | this.ttmlContainer_ = this.player.addChild('TTMLTextTrackDisplay'); 245 | this.mediaPlayer_.attachTTMLRenderingDiv(this.ttmlContainer_.el()); 246 | } 247 | 248 | // Dash.js autoplays by default, video.js will handle autoplay 249 | this.mediaPlayer_.setAutoPlay(false); 250 | 251 | // Setup audio tracks 252 | setupAudioTracks.call(null, this.player, tech); 253 | 254 | // Setup text tracks 255 | setupTextTracks.call(null, this.player, tech, options); 256 | 257 | // Attach the source with any protection data 258 | this.mediaPlayer_.setProtectionData(this.keySystemOptions_); 259 | this.mediaPlayer_.attachSource(manifestSource); 260 | 261 | this.tech_.triggerReady(); 262 | } 263 | 264 | /* 265 | * Iterate over the `keySystemOptions` array and convert each object into 266 | * the type of object Dash.js expects in the `protData` argument. 267 | * 268 | * Also rename 'licenseUrl' property in the options to an 'serverURL' property 269 | */ 270 | static buildDashJSProtData(keySystemOptions) { 271 | const output = {}; 272 | 273 | if (!keySystemOptions || !Array.isArray(keySystemOptions)) { 274 | return null; 275 | } 276 | 277 | for (let i = 0; i < keySystemOptions.length; i++) { 278 | const keySystem = keySystemOptions[i]; 279 | const options = videojs.mergeOptions({}, keySystem.options); 280 | 281 | if (options.licenseUrl) { 282 | options.serverURL = options.licenseUrl; 283 | delete options.licenseUrl; 284 | } 285 | 286 | output[keySystem.name] = options; 287 | } 288 | 289 | return output; 290 | } 291 | 292 | dispose() { 293 | if (this.mediaPlayer_) { 294 | this.mediaPlayer_.off(dashjs.MediaPlayer.events.ERROR, this.retriggerError_); 295 | this.mediaPlayer_.off(dashjs.MediaPlayer.events.MANIFEST_LOADED, this.getDuration_); 296 | this.mediaPlayer_.reset(); 297 | } 298 | 299 | if (this.player.dash) { 300 | delete this.player.dash; 301 | } 302 | 303 | if (this.ttmlContainer_) { 304 | this.ttmlContainer_.dispose(); 305 | this.player.removeChild('TTMLTextTrackDisplay'); 306 | } 307 | } 308 | 309 | duration() { 310 | if (this.mediaPlayer_.isDynamic() && !this.hasFiniteDuration_) { 311 | return Infinity; 312 | } 313 | return this.mediaPlayer_.duration(); 314 | 315 | } 316 | 317 | /** 318 | * Get a list of hooks for a specific lifecycle 319 | * 320 | * @param {string} type the lifecycle to get hooks from 321 | * @param {Function|Function[]} [hook] Optionally add a hook tothe lifecycle 322 | * @return {Array} an array of hooks or epty if none 323 | * @method hooks 324 | */ 325 | static hooks(type, hook) { 326 | Html5DashJS.hooks_[type] = Html5DashJS.hooks_[type] || []; 327 | 328 | if (hook) { 329 | Html5DashJS.hooks_[type] = Html5DashJS.hooks_[type].concat(hook); 330 | } 331 | 332 | return Html5DashJS.hooks_[type]; 333 | } 334 | 335 | /** 336 | * Add a function hook to a specific dash lifecycle 337 | * 338 | * @param {string} type the lifecycle to hook the function to 339 | * @param {Function|Function[]} hook the function or array of functions to attach 340 | * @method hook 341 | */ 342 | static hook(type, hook) { 343 | Html5DashJS.hooks(type, hook); 344 | } 345 | 346 | /** 347 | * Remove a hook from a specific dash lifecycle. 348 | * 349 | * @param {string} type the lifecycle that the function hooked to 350 | * @param {Function} hook The hooked function to remove 351 | * @return {boolean} True if the function was removed, false if not found 352 | * @method removeHook 353 | */ 354 | static removeHook(type, hook) { 355 | const index = Html5DashJS.hooks(type).indexOf(hook); 356 | 357 | if (index === -1) { 358 | return false; 359 | } 360 | 361 | Html5DashJS.hooks_[type] = Html5DashJS.hooks_[type].slice(); 362 | Html5DashJS.hooks_[type].splice(index, 1); 363 | 364 | return true; 365 | } 366 | } 367 | 368 | Html5DashJS.hooks_ = {}; 369 | 370 | const canHandleKeySystems = function(source) { 371 | // copy the source 372 | source = JSON.parse(JSON.stringify(source)); 373 | 374 | if (Html5DashJS.updateSourceData) { 375 | videojs.log.warn('updateSourceData has been deprecated.' + 376 | ' Please switch to using hook("updatesource", callback).'); 377 | source = Html5DashJS.updateSourceData(source); 378 | } 379 | 380 | // call updatesource hooks 381 | Html5DashJS.hooks('updatesource').forEach((hook) => { 382 | source = hook(source); 383 | }); 384 | 385 | const videoEl = document.createElement('video'); 386 | 387 | if (source.keySystemOptions && 388 | !(window.navigator.requestMediaKeySystemAccess || 389 | // IE11 Win 8.1 390 | videoEl.msSetMediaKeys)) { 391 | return false; 392 | } 393 | 394 | return true; 395 | }; 396 | 397 | videojs.DashSourceHandler = function() { 398 | return { 399 | canHandleSource(source) { 400 | const dashExtRE = /\.mpd/i; 401 | 402 | if (!canHandleKeySystems(source)) { 403 | return ''; 404 | } 405 | 406 | if (videojs.DashSourceHandler.canPlayType(source.type)) { 407 | return 'probably'; 408 | } else if (dashExtRE.test(source.src)) { 409 | return 'maybe'; 410 | } 411 | return ''; 412 | 413 | }, 414 | 415 | handleSource(source, tech, options) { 416 | return new Html5DashJS(source, tech, options); 417 | }, 418 | 419 | canPlayType(type) { 420 | return videojs.DashSourceHandler.canPlayType(type); 421 | } 422 | }; 423 | }; 424 | 425 | videojs.DashSourceHandler.canPlayType = function(type) { 426 | const dashTypeRE = /^application\/dash\+xml/i; 427 | 428 | if (dashTypeRE.test(type)) { 429 | return 'probably'; 430 | } 431 | 432 | return ''; 433 | }; 434 | 435 | // Only add the SourceHandler if the browser supports MediaSourceExtensions 436 | if (window.MediaSource) { 437 | videojs.getTech('Html5').registerSourceHandler(videojs.DashSourceHandler(), 0); 438 | } 439 | 440 | videojs.Html5DashJS = Html5DashJS; 441 | export default Html5DashJS; 442 | -------------------------------------------------------------------------------- /test/dashjs.test.js: -------------------------------------------------------------------------------- 1 | import videojs from 'video.js'; 2 | import QUnit from 'qunit'; 3 | import window from 'global/window'; 4 | import document from 'global/document'; 5 | 6 | const dashjs = window.dashjs; 7 | 8 | let sampleSrc = { 9 | src: 'movie.mpd', 10 | type: 'application/dash+xml', 11 | keySystemOptions: [{ 12 | name: 'com.widevine.alpha', 13 | options: { 14 | extra: 'data', 15 | licenseUrl: 'https://example.com/license' 16 | } 17 | }] 18 | }; 19 | 20 | const sampleSrcNoDRM = { 21 | src: 'movie.mpd', 22 | type: 'application/dash+xml' 23 | }; 24 | 25 | const testHandleSource = function(assert, source, expectedKeySystemOptions, config) { 26 | if (config === undefined) { 27 | config = {}; 28 | } 29 | const eventHandlers = config.eventHandlers ? config.eventHandlers : {}; 30 | let startupCalled = false; 31 | let attachViewCalled = false; 32 | let setLimitBitrateByPortalCalled = false; 33 | let setLimitBitrateByPortalValue = null; 34 | const el = document.createElement('div'); 35 | const fixture = document.querySelector('#qunit-fixture'); 36 | 37 | // stubs 38 | 39 | const origMediaPlayer = dashjs.MediaPlayer; 40 | 41 | const origVJSXHR = videojs.xhr; 42 | 43 | assert.expect(7); 44 | 45 | // Default limitBitrateByPortal to false 46 | const limitBitrateByPortal = config.limitBitrateByPortal || false; 47 | 48 | el.setAttribute('id', 'test-vid'); 49 | fixture.appendChild(el); 50 | 51 | const Html5 = videojs.getTech('Html5'); 52 | const tech = new Html5({ 53 | playerId: el.getAttribute('id') 54 | }); 55 | const options = { 56 | playerId: el.getAttribute('id'), 57 | dash: { 58 | limitBitrateByPortal 59 | } 60 | }; 61 | 62 | tech.el = function() { 63 | return el; 64 | }; 65 | tech.triggerReady = function() { }; 66 | 67 | dashjs.MediaPlayer = function() { 68 | return { 69 | create() { 70 | return { 71 | initialize() { 72 | startupCalled = true; 73 | }, 74 | 75 | attachTTMLRenderingDiv() { 76 | }, 77 | attachView() { 78 | attachViewCalled = true; 79 | }, 80 | setAutoPlay(autoplay) { 81 | assert.strictEqual(autoplay, false, 'autoplay is set to false by default'); 82 | }, 83 | setProtectionData(keySystemOptions) { 84 | assert.deepEqual(keySystemOptions, expectedKeySystemOptions, 85 | 'src and manifest key system options are merged'); 86 | }, 87 | attachSource(manifest) { 88 | assert.deepEqual(manifest, source.src, 'manifest url is sent to attachSource'); 89 | 90 | assert.strictEqual(setLimitBitrateByPortalCalled, true, 91 | 'MediaPlayer.setLimitBitrateByPortal was called'); 92 | assert.strictEqual(setLimitBitrateByPortalValue, limitBitrateByPortal, 93 | 'MediaPlayer.setLimitBitrateByPortal was called with the correct value'); 94 | assert.strictEqual(startupCalled, true, 'MediaPlayer.startup was called'); 95 | assert.strictEqual(attachViewCalled, true, 'MediaPlayer.attachView was called'); 96 | 97 | tech.dispose(); 98 | 99 | // Restore 100 | dashjs.MediaPlayer = origMediaPlayer; 101 | videojs.xhr = origVJSXHR; 102 | }, 103 | 104 | setLimitBitrateByPortal(value) { 105 | setLimitBitrateByPortalCalled = true; 106 | setLimitBitrateByPortalValue = value; 107 | }, 108 | 109 | on(event, fn) { 110 | if (!eventHandlers[event]) { 111 | eventHandlers[event] = []; 112 | } 113 | eventHandlers[event].push(fn); 114 | }, 115 | 116 | reset: config.resetCallback, 117 | 118 | trigger(event, data) { 119 | if (!eventHandlers[event]) { 120 | return; 121 | } 122 | eventHandlers[event].forEach(function(handler) { 123 | handler(data); 124 | }); 125 | } 126 | }; 127 | } 128 | }; 129 | }; 130 | 131 | dashjs.MediaPlayer.events = origMediaPlayer.events; 132 | 133 | const dashSourceHandler = Html5.selectSourceHandler(source); 134 | 135 | return dashSourceHandler.handleSource(source, tech, options); 136 | }; 137 | 138 | QUnit.module('videojs-dash dash.js SourceHandler', { 139 | afterEach() { 140 | videojs.Html5DashJS.hooks_ = {}; 141 | sampleSrc = { 142 | src: 'movie.mpd', 143 | type: 'application/dash+xml', 144 | keySystemOptions: [{ 145 | name: 'com.widevine.alpha', 146 | options: { 147 | extra: 'data', 148 | licenseUrl: 'https://example.com/license' 149 | } 150 | }] 151 | }; 152 | } 153 | }); 154 | 155 | QUnit.test('validate the Dash.js SourceHandler in Html5', function(assert) { 156 | const dashSource = { 157 | src: 'some.mpd', 158 | type: 'application/dash+xml' 159 | }; 160 | 161 | const maybeDashSource = { 162 | src: 'some.mpd' 163 | }; 164 | 165 | const nonDashSource = { 166 | src: 'some.mp4', 167 | type: 'video/mp4' 168 | }; 169 | 170 | const dashSourceHandler = videojs.getTech('Html5').selectSourceHandler(dashSource); 171 | 172 | assert.ok(dashSourceHandler, 'A DASH handler was found'); 173 | 174 | assert.strictEqual(dashSourceHandler.canHandleSource(dashSource), 'probably', 175 | 'canHandleSource with proper mime-type returns "probably"'); 176 | assert.strictEqual(dashSourceHandler.canHandleSource(maybeDashSource), 'maybe', 177 | 'canHandleSource with expected extension returns "maybe"'); 178 | assert.strictEqual(dashSourceHandler.canHandleSource(nonDashSource), '', 179 | 'canHandleSource with anything else returns ""'); 180 | 181 | assert.strictEqual(dashSourceHandler.canPlayType(dashSource.type), 'probably', 182 | 'canPlayType with proper mime-type returns "probably"'); 183 | assert.strictEqual(dashSourceHandler.canPlayType(nonDashSource.type), '', 184 | 'canPlayType with anything else returns ""'); 185 | }); 186 | 187 | QUnit.test('validate buildDashJSProtData function', function(assert) { 188 | const output = videojs.Html5DashJS.buildDashJSProtData(sampleSrc.keySystemOptions); 189 | 190 | const empty = videojs.Html5DashJS.buildDashJSProtData(undefined); 191 | 192 | assert.strictEqual(output['com.widevine.alpha'].serverURL, 'https://example.com/license', 193 | 'licenceUrl converted to serverURL'); 194 | assert.equal(empty, null, 'undefined keySystemOptions returns null'); 195 | }); 196 | 197 | QUnit.test('validate handleSource function with src-provided key options', function(assert) { 198 | const mergedKeySystemOptions = { 199 | 'com.widevine.alpha': { 200 | extra: 'data', 201 | serverURL: 'https://example.com/license' 202 | } 203 | }; 204 | 205 | testHandleSource(assert, sampleSrc, mergedKeySystemOptions); 206 | }); 207 | 208 | QUnit.test('validate handleSource function with "limit bitrate by portal" option', function(assert) { 209 | const mergedKeySystemOptions = { 210 | 'com.widevine.alpha': { 211 | extra: 'data', 212 | serverURL: 'https://example.com/license' 213 | } 214 | }; 215 | 216 | testHandleSource(assert, sampleSrc, mergedKeySystemOptions, {limitBitrateByPortal: true}); 217 | }); 218 | 219 | QUnit.test('validate handleSource function with invalid manifest', function(assert) { 220 | const mergedKeySystemOptions = null; 221 | 222 | testHandleSource(assert, sampleSrcNoDRM, mergedKeySystemOptions); 223 | }); 224 | 225 | QUnit.test('update the source keySystemOptions', function(assert) { 226 | const mergedKeySystemOptions = { 227 | 'com.widevine.alpha': { 228 | extra: 'data', 229 | serverURL: 'https://example.com/license' 230 | }, 231 | 'com.widevine.alpha1': { 232 | serverURL: 'https://example.com/anotherlicense' 233 | } 234 | }; 235 | 236 | const updateSourceData = function(source) { 237 | const numOfKeySystems = source.keySystemOptions.length; 238 | 239 | source.keySystemOptions.push({ 240 | name: 'com.widevine.alpha' + numOfKeySystems, 241 | options: { 242 | serverURL: 'https://example.com/anotherlicense' 243 | } 244 | }); 245 | return source; 246 | }; 247 | 248 | videojs.Html5DashJS.hook('updatesource', updateSourceData); 249 | 250 | testHandleSource(assert, sampleSrc, mergedKeySystemOptions); 251 | }); 252 | 253 | QUnit.test('registers hook callbacks correctly', function(assert) { 254 | let cb1Count = 0; 255 | let cb2Count = 0; 256 | const cb1 = function(source) { 257 | cb1Count++; 258 | return source; 259 | }; 260 | const cb2 = function() { 261 | cb2Count++; 262 | }; 263 | const mergedKeySystemOptions = { 264 | 'com.widevine.alpha': { 265 | extra: 'data', 266 | serverURL: 'https://example.com/license' 267 | } 268 | }; 269 | 270 | videojs.Html5DashJS.hook('updatesource', cb1); 271 | videojs.Html5DashJS.hook('beforeinitialize', cb2); 272 | 273 | testHandleSource(assert, sampleSrc, mergedKeySystemOptions, {limitBitrateByPortal: true}); 274 | 275 | assert.expect(9); 276 | 277 | assert.equal(cb1Count, 2, 278 | 'registered first callback and called'); 279 | assert.equal(cb2Count, 1, 280 | 'registered second callback and called'); 281 | }); 282 | 283 | QUnit.test('removes callbacks with removeInitializationHook correctly', function(assert) { 284 | let cb1Count = 0; 285 | let cb2Count = 0; 286 | let cb3Count = 0; 287 | let cb4Count = 0; 288 | const cb1 = function() { 289 | cb1Count++; 290 | }; 291 | const cb2 = function() { 292 | cb2Count++; 293 | assert.ok(videojs.Html5DashJS.removeHook('beforeinitialize', cb2), 294 | 'removed hook cb2'); 295 | }; 296 | const cb3 = function(source) { 297 | cb3Count++; 298 | return source; 299 | }; 300 | const cb4 = function(source) { 301 | cb4Count++; 302 | return source; 303 | }; 304 | const mergedKeySystemOptions = { 305 | 'com.widevine.alpha': { 306 | extra: 'data', 307 | serverURL: 'https://example.com/license' 308 | } 309 | }; 310 | 311 | videojs.Html5DashJS.hook('beforeinitialize', [cb1, cb2]); 312 | videojs.Html5DashJS.hook('updatesource', [cb3, cb4]); 313 | 314 | assert.equal(videojs.Html5DashJS.hooks('beforeinitialize').length, 2, 315 | 'added 2 hooks to beforeinitialize'); 316 | assert.equal(videojs.Html5DashJS.hooks('updatesource').length, 2, 317 | 'added 2 hooks to updatesource'); 318 | 319 | assert.ok(!videojs.Html5DashJS.removeHook('beforeinitialize', cb3), 320 | 'nothing removed if callback not found'); 321 | assert.ok(videojs.Html5DashJS.removeHook('updatesource', cb3), 'removed cb3'); 322 | 323 | assert.equal(videojs.Html5DashJS.hooks('updatesource').length, 1, 'removed hook cb3'); 324 | 325 | testHandleSource(assert, sampleSrc, mergedKeySystemOptions, {limitBitrateByPortal: true}); 326 | 327 | assert.expect(18); 328 | 329 | assert.equal(cb1Count, 1, 'called cb1'); 330 | assert.equal(cb2Count, 1, 'called cb2'); 331 | assert.equal(cb3Count, 0, 'did not call cb3'); 332 | assert.equal(cb4Count, 2, 'called cb4'); 333 | assert.equal(videojs.Html5DashJS.hooks('beforeinitialize').length, 1, 334 | 'cb2 removed itself'); 335 | }); 336 | 337 | QUnit.test('attaches dash.js error handler', function(assert) { 338 | const eventHandlers = {}; 339 | const sourceHandler = testHandleSource(assert, sampleSrcNoDRM, null, {eventHandlers}); 340 | 341 | assert.expect(8); 342 | assert.equal(eventHandlers[dashjs.MediaPlayer.events.ERROR][0], 343 | sourceHandler.retriggerError_); 344 | }); 345 | 346 | QUnit.test('handles various errors', function(assert) { 347 | const errors = [ 348 | { 349 | receive: {error: 'capability', event: 'mediasource'}, 350 | trigger: {code: 4, message: 'The media cannot be played because it requires a feature ' + 351 | 'that your browser does not support.'} 352 | }, 353 | { 354 | receive: {error: 'manifestError', 355 | event: {id: 'createParser', message: 'manifest type unsupported'}}, 356 | trigger: {code: 4, message: 'manifest type unsupported'} 357 | }, 358 | { 359 | receive: {error: 'manifestError', 360 | event: {id: 'codec', message: 'Codec (h264) is not supported'}}, 361 | trigger: {code: 4, message: 'Codec (h264) is not supported'} 362 | }, 363 | { 364 | receive: {error: 'manifestError', 365 | event: {id: 'nostreams', message: 'No streams to play.'}}, 366 | trigger: {code: 4, message: 'No streams to play.'} 367 | }, 368 | { 369 | receive: {error: 'manifestError', 370 | event: {id: 'nostreamscomposed', message: 'Error creating stream.'}}, 371 | trigger: {code: 4, message: 'Error creating stream.'} 372 | }, 373 | { 374 | receive: {error: 'manifestError', 375 | event: {id: 'parse', message: 'parsing the manifest failed'}}, 376 | trigger: {code: 4, message: 'parsing the manifest failed'} 377 | }, 378 | { 379 | receive: {error: 'manifestError', 380 | event: {id: 'nostreams', message: 'Multiplexed representations are intentionally not ' + 381 | 'supported, as they are not compliant with the DASH-AVC/264 guidelines'}}, 382 | trigger: {code: 4, message: 'Multiplexed representations are intentionally not ' + 383 | 'supported, as they are not compliant with the DASH-AVC/264 guidelines'} 384 | }, 385 | { 386 | receive: {error: 'mediasource', event: 'MEDIA_ERR_ABORTED: Some context'}, 387 | trigger: {code: 1, message: 'MEDIA_ERR_ABORTED: Some context'} 388 | }, 389 | { 390 | receive: {error: 'mediasource', event: 'MEDIA_ERR_NETWORK: Some context'}, 391 | trigger: {code: 2, message: 'MEDIA_ERR_NETWORK: Some context'} 392 | }, 393 | { 394 | receive: {error: 'mediasource', event: 'MEDIA_ERR_DECODE: Some context'}, 395 | trigger: {code: 3, message: 'MEDIA_ERR_DECODE: Some context'} 396 | }, 397 | { 398 | receive: {error: 'mediasource', event: 'MEDIA_ERR_SRC_NOT_SUPPORTED: Some context'}, 399 | trigger: {code: 4, message: 'MEDIA_ERR_SRC_NOT_SUPPORTED: Some context'} 400 | }, 401 | { 402 | receive: {error: 'mediasource', event: 'MEDIA_ERR_ENCRYPTED: Some context'}, 403 | trigger: {code: 5, message: 'MEDIA_ERR_ENCRYPTED: Some context'} 404 | }, 405 | { 406 | receive: {error: 'mediasource', event: 'UNKNOWN: Some context'}, 407 | trigger: {code: 4, message: 'UNKNOWN: Some context'} 408 | }, 409 | { 410 | receive: {error: 'mediasource', event: 'Error creating video source buffer'}, 411 | trigger: {code: 4, message: 'Error creating video source buffer'} 412 | }, 413 | { 414 | receive: {error: 'capability', event: 'encryptedmedia'}, 415 | trigger: {code: 5, message: 'The media cannot be played because it requires encryption ' + 416 | 'features that your browser does not support.'} 417 | }, 418 | { 419 | receive: {error: 'key_session', event: 'Some encryption error'}, 420 | trigger: {code: 5, message: 'Some encryption error'} 421 | }, 422 | { 423 | receive: {error: 'download', event: { id: 'someId', url: 'http://some/url', request: {} }}, 424 | trigger: {code: 2, message: 'The media playback was aborted because too many ' + 425 | 'consecutive download errors occurred.'} 426 | }, 427 | { 428 | receive: {error: 'mssError', event: 'MSS_NO_TFRF : Missing tfrf in live media segment'}, 429 | trigger: {code: 3, message: 'MSS_NO_TFRF : Missing tfrf in live media segment'} 430 | } 431 | ]; 432 | 433 | // Make sure the MediaPlayer gets reset enough times 434 | const done = assert.async(errors.length); 435 | const resetCallback = function() { 436 | done(); 437 | }; 438 | 439 | const eventHandlers = {}; 440 | const sourceHandler = testHandleSource(assert, sampleSrcNoDRM, null, 441 | {eventHandlers, resetCallback}); 442 | 443 | assert.expect(7 + (errors.length * 2)); 444 | 445 | let i; 446 | 447 | sourceHandler.player.on('error', function() { 448 | assert.equal(sourceHandler.player.error().code, errors[i].trigger.code, 'error code matches'); 449 | assert.equal(sourceHandler.player.error().message, errors[i].trigger.message, 450 | 'error message matches'); 451 | }); 452 | 453 | // dispatch all handled errors and see if they throw the correct details 454 | for (i = 0; i < errors.length; i++) { 455 | sourceHandler.mediaPlayer_.trigger(dashjs.MediaPlayer.events.ERROR, errors[i].receive); 456 | } 457 | 458 | }); 459 | 460 | QUnit.test('ignores unknown errors', function(assert) { 461 | let resetCalled = false; 462 | const resetCallback = () => { 463 | resetCalled = true; 464 | }; 465 | 466 | const sourceHandler = testHandleSource(assert, sampleSrcNoDRM, null, {resetCallback}); 467 | 468 | const done = assert.async(1); 469 | 470 | sourceHandler.mediaPlayer_.trigger(dashjs.MediaPlayer.events.ERROR, {error: 'unknown'}); 471 | assert.equal(sourceHandler.player.error(), null, 'No error dispatched'); 472 | // The error handler waits 10ms before firing reset, so we wait for 473 | // 20ms here to make sure it doesn't fire 474 | setTimeout(function() { 475 | assert.notOk(resetCalled, 'MediaPlayer has not been reset'); 476 | done(); 477 | }, 20); 478 | assert.expect(9); 479 | }); 480 | 481 | --------------------------------------------------------------------------------