diff --git a/.circleci/config.yml b/.circleci/config.yml index 9ba5c96357..0eb4198b62 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -49,8 +49,34 @@ commands: steps: - run: name: Build and run unit tests - command: npm run build + command: | + npm run build + npm run test + + functional_steps: + steps: + - functional_precondition + - checkout + - dependencies_setup + - build_unit_test_steps + - functional_test_setup + run_test_suite: + parameters: + browser: + default: "chrome" + type: string + protocol: + default: "https" + type: string + groupname: + default: "" + type: string + steps: + - run: + name: Run functional tests (<> / <>) <> + command: + node test/functional/runTests.js --selenium=remote --reporters=junit --app=remote --browsers=<> --protocol=<> --groupname="<>" jobs: build-and-unit-test: executor: dashjs-executor @@ -62,8 +88,7 @@ jobs: name: Deploy command: | if [ "${CIRCLE_BRANCH}" = "development" ] && [ -n "$DEPLOY_HOST" ] && [ -n "$DEPLOY_USER" ] && [ -n "$DEPLOY_PASSWORD" ]; then - sudo npm install -g grunt-cli - grunt deploy --git-commit=$CIRCLE_SHA1 --ftp-host=$DEPLOY_HOST --ftp-user=$DEPLOY_USER --ftp-pass=$DEPLOY_PASSWORD + node deploy.js --host=$DEPLOY_HOST --user=$DEPLOY_USER --password=$DEPLOY_PASSWORD else echo "Not on development branch or deploy not configured. Dry run only, nothing will be deployed." fi @@ -112,23 +137,52 @@ jobs: - store_test_results: path: test/functional/reports - functional-tests-all: + functional-tests-VOD_LIVE: executor: dashjs-executor steps: - - functional_precondition - - checkout - - dependencies_setup - - build_unit_test_steps - - functional_test_setup - - run: - name: Run functional tests (chrome / https) - command: - node test/functional/runTests.js --selenium=remote --reporters=junit --app=remote --browsers=chrome --protocol=https - - run: - name: Run functional tests (chrome / http) - when: always # run tests even if some previous tests failed - command: - node test/functional/runTests.js --selenium=remote --reporters=junit --app=remote --browsers=chrome --protocol=http + - functional_steps + - run_test_suite: + groupname: VOD (Static MPD) + - run_test_suite: + groupname: LIVE (Dynamic MPD) + - run_test_suite: + groupname: Live Low Latency + - store_test_results: + path: test/functional/reports + + functional-tests-DRM: + executor: dashjs-executor + steps: + - functional_steps + - run_test_suite: + groupname: DRM (modern) + - run_test_suite: + groupname: DRM Content (conservative/legacy) + - store_test_results: + path: test/functional/reports + + functional-tests-Subtitles_Thumbnails_Audio_Smooth: + executor: dashjs-executor + steps: + - functional_steps + - run_test_suite: + groupname: Subtitles and Captions + - run_test_suite: + groupname: Thumbnails + - run_test_suite: + groupname: Audio-only + - run_test_suite: + groupname: Smooth Streaming + + - store_test_results: + path: test/functional/reports + + functional-tests-only-http: + executor: dashjs-executor + steps: + - functional_steps + - run_test_suite: + protocol: http - store_test_results: path: test/functional/reports @@ -151,11 +205,14 @@ workflows: scheduled-workflow: triggers: - schedule: - cron: "0 0 * * 0" + cron: "0 0 * * 0,3" filters: branches: only: - development jobs: - - functional-tests-all + - functional-tests-VOD_LIVE + - functional-tests-DRM + - functional-tests-Subtitles_Thumbnails_Audio_Smooth + - functional-tests-only-http diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000000..ed1ceb0af8 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,39 @@ +{ + "env": { + "browser": true, + "es6": true, + "mocha": true, + "node": true + }, + "globals": { + "dashjs": true, + "WebKitMediaSource": true, + "MediaSource": true, + "WebKitMediaKeys": true, + "MSMediaKeys": true, + "MediaKeys": true + }, + "parser": "babel-eslint", + "rules": { + "no-caller": 2, + "no-undef": 2, + "no-unused-vars": 2, + "no-use-before-define": 0, + "strict": 0, + "no-loop-func": 0, + "quotes": [ + "error", + "single", + { + "allowTemplateLiterals": true + } + ], + "indent": [ + "error", + 4, + { + "SwitchCase": 1 + } + ] + } +} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000000..129a80c71a --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,53 @@ +name: deploy + +on: + workflow_call: + inputs: + envname: + required: true + type: string + deploy_path: + required: true + type: string + secrets: + host: + required: true + user: + required: true + private_key: + required: true + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: "14.x" + - name: Install dependencies + run: npm install + - name: Run build + run: npm run build + - name: Find and Replace + uses: jacobtomlinson/gha-find-replace@v2 + env: + replacement_string: ${{ format('{0} - commit {2}', github.ref_name, github.sha, github.sha)}} + with: + find: "" + replace: ${{env.replacement_string}} + include: "samples/dash-if-reference-player/index.html" + - name: Copy to deploy directory for deployment + run: | + mkdir ${{inputs.envname}} + cp -R contrib dist samples test/functional/config test/functional/tests ${{inputs.envname}} + - name: Install SSH Key + uses: shimataro/ssh-key-action@v2 + with: + key: ${{ secrets.private_key }} + known_hosts: unnecessary + + - name: Deploy with rsync + run: rsync -av -c -e "ssh -o StrictHostKeyChecking=no -oHostKeyAlgorithms=+ssh-dss" ${{inputs.envname}} ${{ secrets.user }}@${{ secrets.host }}:${{ inputs.deploy_path }} diff --git a/.github/workflows/deploy_latest.yml b/.github/workflows/deploy_latest.yml new file mode 100644 index 0000000000..3c21a3af2d --- /dev/null +++ b/.github/workflows/deploy_latest.yml @@ -0,0 +1,17 @@ +name: deploy + +on: + push: + branches: + - 'master' + +jobs: + deploy_staging: + uses: ./.github/workflows/deploy.yml + with: + envname: latest + deploy_path: '/377335/dash.js' + secrets: + host: ${{secrets.HOST}} + user: ${{secrets.USER}} + private_key: ${{secrets.PRIVATE_KEY}} diff --git a/.github/workflows/deploy_nightly.yml b/.github/workflows/deploy_nightly.yml new file mode 100644 index 0000000000..7335744be1 --- /dev/null +++ b/.github/workflows/deploy_nightly.yml @@ -0,0 +1,17 @@ +name: deploy + +on: + push: + branches: + - 'development' + +jobs: + deploy_staging: + uses: ./.github/workflows/deploy.yml + with: + envname: nightly + deploy_path: '/377335/dash.js' + secrets: + host: ${{secrets.HOST}} + user: ${{secrets.USER}} + private_key: ${{secrets.PRIVATE_KEY}} diff --git a/.gitignore b/.gitignore index 36f0e7354f..061c80a33f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /simple.html +samples/mpds ################# ## Node.js ################# @@ -187,3 +188,5 @@ build/typings/ # Vim .vimrc + +dashjs-*.tgz \ No newline at end of file diff --git a/.jscsrc b/.jscsrc deleted file mode 100644 index 850c7a5c50..0000000000 --- a/.jscsrc +++ /dev/null @@ -1,39 +0,0 @@ -{ - "esnext": true, - "verbose": true, - - "validateIndentation": 4, - "validateQuoteMarks": "'", - "disallowMixedSpacesAndTabs": true, - "disallowTrailingWhitespace": true, - "disallowTrailingComma": true, - "requireSpaceAfterKeywords": true, - "requireSpaceBeforeBinaryOperators": true, - "requireSpaceAfterBinaryOperators": true, - "requireSpacesInConditionalExpression": true, - "requireSpaceBeforeBlockStatements": true, - "requireSpacesInForStatement": true, - "requireSpacesInFunctionExpression": { - "beforeOpeningCurlyBrace": true - }, - "disallowSpaceAfterObjectKeys": true, - "requireSpaceBeforeObjectValues": true, - "requireSemicolons": true, - "validateParameterSeparator": ", ", - "disallowMultipleVarDecl": { - "allExcept": ["undefined"] - }, - "disallowEmptyBlocks": { - "allExcept": ["comments"] - }, - "jsDoc": { - "checkParamExistence": true, - "checkParamNames": true, - "requireParamTypes": true, - "checkRedundantParams": true, - "checkReturnTypes": true, - "checkRedundantReturns": true, - "requireReturnTypes": true, - "checkTypes": true - } -} diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 4753bb722b..0000000000 --- a/.jshintrc +++ /dev/null @@ -1,34 +0,0 @@ -{ - "esnext": true, - "noarg": true, - "latedef": "nofunc", - "undef": true, - "unused": true, - "strict": false, - "node": true, - "jquery" : true, - "devel" : true, - "browser": true, - "loopfunc" : true, - "predef" : [ - "it", - "describe", - "beforeEach", - "before", - "afterEach", - "after", - "kendo", - "utils", - "MediaPlayer", - "Dash", - "WebKitMediaSource", - "MediaSource", - "WebKitMediaKeys", - "MSMediaKeys", - "MediaKeys", - "Caster", - "TextTrackCue", - "HTMLMediaElement", - "MediaError", - "cea608parser"] -} diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index ff06b9c826..0000000000 --- a/Gruntfile.js +++ /dev/null @@ -1,442 +0,0 @@ -module.exports = function (grunt) { - require('time-grunt')(grunt); - - grunt.initConfig({ - pkg: grunt.file.readJSON('package.json'), - githash: { - options: { - fail: false - }, - dist: { - } - }, - - clean: { - build: ['build/temp'], - dist: ['dist/*'] - }, - jshint: { - src: { - src: ['src/**/*.js', 'test/unit/mocks/*.js', 'test/unit/*.js', 'Gruntfile.js'], - options: { - jshintrc: '.jshintrc' - } - } - }, - - uglify: { - options: { - banner: '/*! v<%= pkg.version %>-<%= githash.dist.short %>, <%= grunt.template.today("isoUtcDateTime") %> */', - sourceMap: { - includeSources: true, - root: './src/' - }, - preserveComments: 'some', - mangle: true, - compress: { - sequences: true, - dead_code: true, - conditionals: true, - booleans: true, - unused: true, - if_return: true, - join_vars: true, - drop_console: false - } - }, - - build_core: { - options: { - sourceMapIn: 'build/temp/dash.mediaplayer.debug.js.map' - }, - files: { - 'build/temp/dash.mediaplayer.min.js': 'build/temp/dash.mediaplayer.debug.js' - } - }, - build_protection: { - options: { - sourceMapIn: 'build/temp/dash.protection.debug.js.map' - }, - files: { - 'build/temp/dash.protection.min.js': 'build/temp/dash.protection.debug.js' - } - }, - - build_reporting: { - options: { - sourceMapIn: 'build/temp/dash.reporting.debug.js.map' - }, - files: { - 'build/temp/dash.reporting.min.js': 'build/temp/dash.reporting.debug.js' - } - }, - - build_mss: { - options: { - sourceMapIn: 'build/temp/dash.mss.debug.js.map' - }, - files: { - 'build/temp/dash.mss.min.js': 'build/temp/dash.mss.debug.js' - } - }, - - build_offline: { - options: { - sourceMapIn: 'build/temp/dash.offline.debug.js.map' - }, - files: { - 'build/temp/dash.offline.min.js': 'build/temp/dash.offline.debug.js' - } - }, - - build_all: { - options: { - sourceMapIn: 'build/temp/dash.all.debug.js.map' - }, - files: { - 'build/temp/dash.all.min.js': 'build/temp/dash.all.debug.js' - } - } - }, - browserSync: { - bsFiles: { - src: ['dist/*.js', 'samples/**/*', 'contrib/**/*', 'externals/**/*.js'] - }, - options: { - watchTask: true, - host: 'localhost', - server: { - baseDir: './', - directory: true - }, - startPath: '/samples/index.html', - plugins: [ - { - module: 'bs-html-injector', - options: { - files: 'samples/**/*.html' - } - } - ] - } - }, - copy: { - dist: { - files: [{ - expand: true, - cwd: 'build/temp/', - src: [ - 'dash.all.min.js', 'dash.all.min.js.map', - 'dash.mediaplayer.min.js', 'dash.mediaplayer.min.js.map', - 'dash.protection.min.js', 'dash.protection.min.js.map', - 'dash.all.debug.js', 'dash.all.debug.js.map', - 'dash.reporting.min.js', 'dash.reporting.min.js.map', - 'dash.mss.min.js', 'dash.mss.min.js.map', - 'dash.mediaplayer.debug.js', 'dash.mediaplayer.debug.js.map', - 'dash.protection.debug.js', 'dash.protection.debug.js.map', - 'dash.reporting.debug.js', 'dash.reporting.debug.js.map', - 'dash.mss.debug.js', 'dash.mss.debug.js.map', - 'dash.offline.debug.js', 'dash.offline.debug.js.map', - 'dash.offline.min.js', 'dash.offline.min.js.map' - ], - dest: 'dist/', - filter: 'isFile' - }, { - expand: true, - cwd: '.', - src: 'index.d.ts', - dest: 'dist/', - rename: function (dest) { - return dest + 'dash.d.ts'; - } - }, { - expand: true, - cwd: '.', - src: 'index.d.ts', - dest: 'build/typings/' - }] - } - }, - exorcise: { - mediaplayer: { - options: { - base: './src' - }, - files: { - 'build/temp/dash.mediaplayer.debug.js.map': ['build/temp/dash.mediaplayer.debug.js'] - } - }, - protection: { - options: { - base: './src' - }, - files: { - 'build/temp/dash.protection.debug.js.map': ['build/temp/dash.protection.debug.js'] - } - }, - all: { - options: { - base: './src' - }, - files: { - 'build/temp/dash.all.debug.js.map': ['build/temp/dash.all.debug.js'] - } - }, - reporting: { - options: { - base: './src' - }, - files: { - 'build/temp/dash.reporting.debug.js.map': ['build/temp/dash.reporting.debug.js'] - } - }, - mss: { - options: { - base: './src' - }, - files: { - 'build/temp/dash.mss.debug.js.map': ['build/temp/dash.mss.debug.js'] - } - }, - offline: { - options: { - base: './src' - }, - files: { - 'build/temp/dash.offline.debug.js.map': ['build/temp/dash.offline.debug.js'] - } - } - }, - - babel: { - options: { - sourceMap: true, - compact: true, - presets: ['env'] - }, - es5: { - files: [{ - expand: true, - src: ['index.js', 'index_mediaplayerOnly.js', 'src/**/*.js', 'externals/**/*.js'], - dest: 'build/es5/' - }] - } - }, - - browserify: { - mediaplayer: { - files: { - 'build/temp/dash.mediaplayer.debug.js': ['index_mediaplayerOnly.js'] - }, - options: { - browserifyOptions: { - debug: true - }, - plugin: [ - 'browserify-derequire', 'bundle-collapser/plugin' - ], - transform: [['babelify', {compact: false}]] - } - }, - protection: { - files: { - 'build/temp/dash.protection.debug.js': ['src/streaming/protection/Protection.js'] - }, - options: { - browserifyOptions: { - debug: true, - standalone: 'dashjs.Protection' - }, - plugin: [ - 'browserify-derequire', 'bundle-collapser/plugin' - ], - transform: [['babelify', {compact: false}]] - } - }, - reporting: { - files: { - 'build/temp/dash.reporting.debug.js': ['src/streaming/metrics/MetricsReporting.js'] - }, - options: { - browserifyOptions: { - debug: true, - standalone: 'dashjs.MetricsReporting' - }, - plugin: [ - 'browserify-derequire', 'bundle-collapser/plugin' - ], - transform: [['babelify', {compact: false}]] - } - }, - all: { - files: { - 'build/temp/dash.all.debug.js': ['index.js'] - }, - options: { - browserifyOptions: { - debug: true - }, - plugin: [ - 'browserify-derequire', 'bundle-collapser/plugin' - ], - transform: [['babelify', {compact: false}]] - } - }, - mss: { - files: { - 'build/temp/dash.mss.debug.js': ['src/mss/index.js'] - }, - options: { - browserifyOptions: { - debug: true - }, - plugin: [ - 'browserify-derequire', 'bundle-collapser/plugin' - ], - transform: [['babelify', {compact: false}]] - } - }, - offline: { - files: { - 'build/temp/dash.offline.debug.js': ['src/offline/index.js'] - }, - options: { - browserifyOptions: { - debug: true - }, - plugin: [ - 'browserify-derequire', 'bundle-collapser/plugin' - ], - transform: [['babelify', {compact: false}]] - } - }, - - watch: { - files: { - 'build/temp/dash.all.debug.js': ['index.js'], - 'build/temp/dash.mss.debug.js': ['src/mss/index.js'], - 'build/temp/dash.offline.debug.js': ['src/offline/index.js'] - }, - options: { - watch: true, - keepAlive: true, - browserifyOptions: { - debug: true - }, - plugin: [ - 'browserify-derequire' - ], - transform: [['babelify', {compact: false}]] - } - }, - watch_dev: { - files: { - 'dist/dash.all.debug.js': ['index.js'], - 'dist/dash.mss.debug.js': ['src/mss/index.js'], - 'dist/dash.offline.debug.js': ['src/offline/index.js'] - }, - options: { - watch: true, - keepAlive: true, - browserifyOptions: { - debug: true - }, - plugin: [ - ['browserify-derequire'] - ], - transform: [['babelify', {compact: false}]] - } - } - }, - jsdoc: { - dist: { - options: { - destination: 'docs/jsdoc', - configure: 'build/jsdoc/jsdoc_conf.json' - } - } - }, - mocha_istanbul: { - test: { - src: './test/unit', - options: { - mask: '*.js', - coverageFolder: './reports', - mochaOptions: ['--compilers', 'js:babel/register'], - print: 'both', - reportFormats: ['lcov'], - root: './src' - } - } - }, - jscs: { - src: ['./src/**/*.js', 'test/unit/mocks/*.js', 'test/unit/*.js', 'Gruntfile.js'], - options: { - config: '.jscsrc' - } - }, - githooks: { - all: { - 'pre-commit': 'lint' - } - }, - 'string-replace': { - dist: { - files: { - './samples/dash-if-reference-player/index.html': './samples/dash-if-reference-player/index.html' - }, - options: { - replacements: [{ - pattern: '', - replacement: !grunt.option('git-commit') ? '' : '(development, commit: ' + grunt.option('git-commit').toString().substring(0, 8) + ')' - }] - } - } - }, - ftp_push: { - deployment: { - options: { - host: grunt.option('ftp-host'), - dest: '/', - username: grunt.option('ftp-user'), - password: grunt.option('ftp-pass'), - hideCredentials: true, - // disabling incrementalUpdates because this option is not working fine - incrementalUpdates: false, - debug: true, - port: 21 - }, - files: [ - { - expand: true, - cwd: '.', - src: [ - 'contrib/**', - 'dist/**', - 'test/functional/tests.html', - 'test/functional/testsCommon.js', - 'test/functional/config/**', - 'test/functional/tests/**', - 'samples/**' - ] - } - ] - } - } - }); - - require('load-grunt-tasks')(grunt); - grunt.loadNpmTasks('grunt-string-replace'); - grunt.registerTask('default', ['dist', 'test']); - grunt.registerTask('dist', ['clean', 'jshint', 'jscs', 'browserify:mediaplayer', 'browserify:protection', 'browserify:reporting', 'browserify:mss','browserify:offline', 'browserify:all', 'babel:es5', 'minimize', 'copy:dist']); - grunt.registerTask('minimize', ['exorcise', 'githash', 'uglify']); - grunt.registerTask('test', ['mocha_istanbul:test']); - grunt.registerTask('watch', ['browserify:watch']); - grunt.registerTask('watch-dev', ['browserify:watch_dev']); - grunt.registerTask('release', ['default', 'jsdoc']); - grunt.registerTask('debug', ['clean', 'browserify:all', 'exorcise:all', 'copy:dist']); - grunt.registerTask('lint', ['jshint', 'jscs']); - grunt.registerTask('prepublish', ['githooks', 'dist']); - grunt.registerTask('dev', ['browserSync', 'watch-dev']); - grunt.registerTask('deploy', ['string-replace', 'ftp_push']); -}; diff --git a/README.md b/README.md index 8b1184b540..ed805abb63 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,8 @@ Build status (CircleCI): [![CircleCI](https://circleci.com/gh/Dash-Industry-Foru [Join #dashjs on Slack!](https://join.slack.com/t/dashif/shared_invite/zt-egme869x-JH~UPUuLoKJB26fw7wj3Gg) -## dash.js awards 2021 -The DASH Industry Forum (DASH-IF) is proud to announce its second dash.js award. Again, we are looking for developers who made contributions of significant benefit to the advancement of the dash.js project. -All information can be found [here](https://github.com/Dash-Industry-Forum/dash.js/wiki/dash.js-awards-2021). +## Migration from v3.x to v4.0 +If you are migrating from dash.js v3.x to dash.js v4.x please read the migration document found [here](https://github.com/Dash-Industry-Forum/dash.js/wiki/Migration-to-dash.js-4.0). ## Overview A reference client implementation for the playback of MPEG DASH via JavaScript and [compliant browsers](http://caniuse.com/#feat=mediasource). Learn more about DASH IF Reference Client on our [wiki](https://github.com/Dash-Industry-Forum/dash.js/wiki). @@ -48,8 +47,6 @@ Full [API Documentation](http://cdn.dashjs.org/latest/jsdoc/module-MediaPlayer.h For help, join our [Slack channel](https://dashif-slack.azurewebsites.net), our [email list](https://groups.google.com/d/forum/dashjs) and read our [wiki](https://github.com/Dash-Industry-Forum/dash.js/wiki). -If you are migrating from dash.js v2.x to dash.js v3.x please read the migration document found [here](https://github.com/Dash-Industry-Forum/dash.js/wiki/Migration-3.0). - ## Tutorials Detailed information on specific topics can be found in our tutorials: @@ -165,31 +162,29 @@ When it is all done, it should look similar to this: 1. Install Core Dependencies * [install nodejs](http://nodejs.org/) - * [install grunt](http://gruntjs.com/getting-started) - * ```npm install -g grunt-cli``` 2. Checkout project repository (default branch: development) * ```git clone https://github.com/Dash-Industry-Forum/dash.js.git``` 3. Install dependencies * ```npm install``` 4. Build, watch file changes and launch samples page, which has links that point to reference player and to other examples (basic examples, captioning, ads, live, etc). - * ```grunt dev``` - - -### Other Grunt Tasks to Build / Run Tests on Commandline. - -1. Individual tasks: - * Quickest build - * ```grunt debug``` - * Lint - * ```grunt lint``` - * Run unit tests - * ```grunt test``` - * Build distribution files (minification included) - * ```grunt dist``` - * Build distribution files, lint, run unit tests and generate documentation - * ```grunt release``` -2. GruntFile.js default task (equivalent to ```grunt dist && grunt test```) - * ```grunt``` + * ```npm run start``` + + +### Other Tasks to Build / Run Tests on Commandline. + +* Build distribution files (minification included) + * ```npm run build``` +* Build and watch distribution files + * ```npm run dev``` +* Run linter on source files (linter is also applied when building files) + * ```npm run lint``` +* Run unit tests + * ```npm run test``` +* Generate API jsdoc + * ```npm run doc``` + +### Troubleshooting +* In case the build process is failing make sure to use an up-to-date node.js version. The build process was successfully tested with node.js version 14.16.1. ### License dash.js is released under [BSD license](https://github.com/Dash-Industry-Forum/dash.js/blob/development/LICENSE.md) diff --git a/build/helpers/LLsegment.js b/build/helpers/LLsegment.js new file mode 100644 index 0000000000..dd7790b83e --- /dev/null +++ b/build/helpers/LLsegment.js @@ -0,0 +1,100 @@ +const fs = require("fs"); +const path = require("path"); + +const segmentData = fs.readFileSync(path.join(__dirname, 'data', 'chunk', 'chunk-stream_0-00001.m4s')); +const segmentInfo = fs.readFileSync(path.join(__dirname, 'data', 'chunk', 'chunk-stream_0-00001.json')).toString(); + +function sendChunks(res, segmentInfoData, param, interval, previousTs, chunkDistances) { + let chunksInBurst = 0; + if (param.chunksAvailableAtReqTime) { + chunksInBurst = param.chunksAvailableAtReqTime; + param.chunksAvailableAtReqTime = 0; + } else { + chunksInBurst = param.chunkCount; + } + + for (let index = 0; index < chunksInBurst; index++) { + let mdatFound = false; + let chunkSize = 0; + while (!mdatFound && segmentInfoData.length) { + let box = segmentInfoData.shift(); + mdatFound = box.name === 'mdat'; + chunkSize += box.size; + } + if (chunkSize) { + res.write(segmentData.slice(param.pos, param.pos + chunkSize)); + param.pos = param.pos + chunkSize; + } + } + if (!segmentInfoData.length) { + if (chunkDistances && chunkDistances.length) { + // console.log('all sent with average chunk (burst) distance', chunkDistances.reduce((prev, curr) => prev + curr, 0) / chunkDistances.length) + } + return res.end(); + } + + let processingDuration = 0; + const chunkDistance = Date.now() - previousTs; + // console.log('ms since last call', chunkDistance) + + if (chunkDistance > interval) { + chunkDistances.push(chunkDistance) + processingDuration = chunkDistance - interval; + } + if (interval - processingDuration > 0) { + setTimeout((previousTs) => { + sendChunks(res, segmentInfoData, param, interval, previousTs, chunkDistances); + }, interval - processingDuration, Date.now()); // minus processing duration on producer side + } +} + +function streamWithPattern(res, interval, chunkCount, chunksAvailableAtReqTime) { + const segmentInfoData = JSON.parse(segmentInfo); + + res.statusCode = 200; + + const param = { + chunkCount, + pos: 0, + chunksAvailableAtReqTime + } + + sendChunks(res, segmentInfoData, param, interval, Date.now(), []); +} + + +module.exports = function (req, res) { + switch (req.url) { + case '/ll/pattern0': + streamWithPattern(res, 0, 0, 60); + break; + case '/ll/pattern1': + streamWithPattern(res, 33, 1, 0); + break; + case '/ll/pattern2': + streamWithPattern(res, 133, 4, 0); + break; + case '/ll/pattern3': + streamWithPattern(res, 333, 10, 0); + break; + case '/ll/pattern4': + streamWithPattern(res, 1000, 30, 0); + break; + case '/ll/pattern5': + streamWithPattern(res, 33, 1, 30); + break; + case '/ll/pattern6': + streamWithPattern(res, 133, 4, 30); + break; + case '/ll/pattern7': + streamWithPattern(res, 333, 10, 30); + break; + case '/ll/pattern8': + streamWithPattern(res, 1000, 30, 30); + break; + default: + console.log('unknown', req.url); + res.statusCode = 404; + return res.end(); + } +}; \ No newline at end of file diff --git a/build/helpers/data/chunk/chunk-stream_0-00001.json b/build/helpers/data/chunk/chunk-stream_0-00001.json new file mode 100644 index 0000000000..cb301dfd9c --- /dev/null +++ b/build/helpers/data/chunk/chunk-stream_0-00001.json @@ -0,0 +1,2709 @@ +[ +{ + "name":"styp", + "header_size":8, + "size":24 +}, +{ + "name":"moof", + "header_size":8, + "size":104, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":1 + }, + { + "name":"traf", + "header_size":8, + "size":80, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":4314, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":0 + }, + { + "name":"trun", + "header_size":12, + "size":24, + "sample count":1, + "data offset":112, + "first sample flags":33554432 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":4322 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":2 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":155, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":1001 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":163 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":3 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":350, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":2002 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":358 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":4 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":1042, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":3003 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":1050 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":5 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":709, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":4004 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":717 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":6 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":712, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":5005 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":720 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":7 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":738, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":6006 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":746 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":8 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":617, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":7007 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":625 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":9 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":1004, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":8008 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":1012 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":10 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":891, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":9009 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":899 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":11 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":900, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":10010 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":908 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":12 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":856, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":11011 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":864 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":13 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":943, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":12012 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":951 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":14 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":702, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":13013 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":710 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":15 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":802, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":14014 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":810 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":16 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":888, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":15015 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":896 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":17 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":802, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":16016 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":810 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":18 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":1029, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":17017 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":1037 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":19 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":800, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":18018 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":808 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":20 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":878, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":19019 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":886 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":21 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":961, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":20020 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":969 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":22 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":882, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":21021 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":890 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":23 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":939, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":22022 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":947 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":24 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":871, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":23023 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":879 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":25 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":1009, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":24024 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":1017 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":26 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":779, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":25025 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":787 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":27 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":1000, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":26026 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":1008 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":28 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":823, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":27027 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":831 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":29 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":1049, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":28028 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":1057 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":30 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":1021, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":29029 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":1029 +}, +{ + "name":"moof", + "header_size":8, + "size":104, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":31 + }, + { + "name":"traf", + "header_size":8, + "size":80, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":4447, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":30030 + }, + { + "name":"trun", + "header_size":12, + "size":24, + "sample count":1, + "data offset":112, + "first sample flags":33554432 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":4455 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":32 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":2107, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":31031 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":2115 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":33 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":396, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":32032 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":404 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":34 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":573, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":33033 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":581 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":35 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":558, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":34034 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":566 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":36 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":525, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":35035 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":533 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":37 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":632, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":36036 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":640 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":38 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":748, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":37037 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":756 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":39 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":1137, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":38038 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":1145 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":40 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":767, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":39039 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":775 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":41 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":1109, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":40040 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":1117 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":42 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":953, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":41041 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":961 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":43 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":880, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":42042 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":888 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":44 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":1026, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":43043 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":1034 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":45 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":790, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":44044 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":798 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":46 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":1011, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":45045 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":1019 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":47 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":1127, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":46046 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":1135 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":48 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":899, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":47047 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":907 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":49 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":1187, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":48048 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":1195 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":50 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":929, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":49049 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":937 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":51 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":1225, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":50050 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":1233 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":52 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":778, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":51051 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":786 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":53 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":1345, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":52052 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":1353 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":54 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":871, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":53053 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":879 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":55 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":1112, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":54054 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":1120 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":56 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":866, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":55055 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":874 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":57 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":1268, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":56056 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":1276 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":58 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":842, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":57057 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":850 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":59 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":1397, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":58058 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":1405 +}, +{ + "name":"moof", + "header_size":8, + "size":100, + "children":[ + { + "name":"mfhd", + "header_size":12, + "size":16, + "sequence number":60 + }, + { + "name":"traf", + "header_size":8, + "size":76, + "children":[ + { + "name":"tfhd", + "header_size":12, + "size":28, + "track ID":1, + "default sample duration":1001, + "default sample size":749, + "default sample flags":16842752 + }, + { + "name":"tfdt", + "header_size":12, + "size":20, + "base media decode time":59059 + }, + { + "name":"trun", + "header_size":12, + "size":20, + "sample count":1, + "data offset":108 + }] + }] +}, +{ + "name":"mdat", + "header_size":8, + "size":757 +} +] diff --git a/build/helpers/data/chunk/chunk-stream_0-00001.m4s b/build/helpers/data/chunk/chunk-stream_0-00001.m4s new file mode 100644 index 0000000000..4264628d11 Binary files /dev/null and b/build/helpers/data/chunk/chunk-stream_0-00001.m4s differ diff --git a/build/jsdoc/README.md b/build/jsdoc/README.md index bf057cfdef..ea50668d65 100644 --- a/build/jsdoc/README.md +++ b/build/jsdoc/README.md @@ -5,11 +5,11 @@ ### Install Dependencies 1. [install grunt-jsdoc](https://www.npmjs.org/package/grunt-jsdoc-plugin) - * npm install grunt-jsdoc-plugin + * `npm install jsdoc` ### Build JSDocs only ``` -grunt jsdoc +npm run doc ``` diff --git a/build/jsdoc/jsdoc_conf.json b/build/jsdoc/jsdoc_conf.json index 757bb66c1e..794b426281 100644 --- a/build/jsdoc/jsdoc_conf.json +++ b/build/jsdoc/jsdoc_conf.json @@ -3,27 +3,32 @@ "allowUnknownTags": true }, "source": { - "include": ["./src/", "README.md"] + "include": [ + "./src/", + "README.md" + ] }, - "plugins": [ "plugins/markdown" ], + "plugins": [ + "plugins/markdown" + ], "markdown": { "hardwrap": true }, "templates": { - "cleverLinks" : true, - "monospaceLinks" : true, - "dateFormat" : "ddd MMM Do YYYY", - "outputSourceFiles" : true, - "outputSourcePath" : true, - "systemName" : "Dash JS", - "copyright" : "

Dash.js \"Built

", - "footer" : "", - "navType" : "horizontal", - "theme" : "spacelab", - "linenums" : true, - "collapseSymbols" : false, - "inverseNav" : true, - "highlightTutorialCode" : true + "cleverLinks": true, + "monospaceLinks": true, + "dateFormat": "ddd MMM Do YYYY", + "outputSourceFiles": true, + "outputSourcePath": true, + "systemName": "dash.js", + "copyright": "

DASH Industry Forum

", + "footer": "", + "navType": "horizontal", + "theme": "spacelab", + "linenums": true, + "collapseSymbols": true, + "inverseNav": true, + "highlightTutorialCode": true }, "opts": { "template": "../../node_modules/ink-docstrap/template", diff --git a/build/karma.conf.js b/build/karma.conf.js new file mode 100644 index 0000000000..3ce93cfb12 --- /dev/null +++ b/build/karma.conf.js @@ -0,0 +1,93 @@ + +const llSegmentMiddleware = require('./helpers/LLsegment.js'); + +function CustomMiddlewareFactory(/*config*/) { + return function (request, response, next ) { + if (`${request.url}`.startsWith('/ll')) { + return llSegmentMiddleware(request, response); + } + next(); + } +} + +module.exports = function(config) { + config.set({ + + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '', + + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha', 'chai', 'webpack'], + + plugins: [ + 'karma-webpack', + 'karma-mocha', + 'karma-chai', + 'karma-mocha-reporter', + 'karma-chrome-launcher', + { 'middleware:custom': ['factory', CustomMiddlewareFactory] } + ], + + middleware: ['custom'], + + // list of files / patterns to load in the browser + // https://github.com/webpack-contrib/karma-webpack#alternative-usage + files: [ + { pattern: '../test/browserunit/**/*.js', watched: false }, + { pattern: '../src/**/*.js', watched: false, included: false }, + ], + + // list of files / patterns to exclude + exclude: [ + ], + + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + // add webpack as preprocessor + '../test/browserunit/**/*.js': ['webpack'] + }, + + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['mocha'], + + webpack: { + + }, + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, + + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['ChromeHeadless'], + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: true, + + // Concurrency level + // how many browser should be started simultaneous + concurrency: Infinity + }) +} diff --git a/build/webpack.base.js b/build/webpack.base.js new file mode 100644 index 0000000000..2e34cdb3ab --- /dev/null +++ b/build/webpack.base.js @@ -0,0 +1,39 @@ +const path = require('path'); +const pkg = require('../package.json'); + +const out_dir = '../dist'; + +const config = { + devtool: 'source-map', + output: { + path: path.resolve(__dirname, out_dir), + publicPath: '/dist/', + library: 'dashjs', + libraryTarget: 'umd', + libraryExport: 'default' + }, + module: { + rules: [ + { + test: /\.(js)$/, + loader: 'string-replace-loader', + options: { + search: '__VERSION__', + replace: pkg.version, + } + }, + { + test: /\.(js)$/, + exclude: /node_modules/, + use: [ + { + loader: `babel-loader`, + options: {presets: ['@babel/env']} + } + ] + } + ] + } +} + +module.exports = {config}; diff --git a/build/webpack.dev.js b/build/webpack.dev.js new file mode 100644 index 0000000000..27c8f788fe --- /dev/null +++ b/build/webpack.dev.js @@ -0,0 +1,33 @@ +const { merge } = require('webpack-merge'); +const common = require('./webpack.base.js').config; +const webpack = require("webpack"); +const path = require('path'); + +const config = merge(common, { + mode: 'development', + entry: { + 'dash.all': './index.js', + 'dash.mss': './src/mss/index.js', + 'dash.offline': './src/offline/index.js' + }, + output: { + filename: '[name].debug.js', + }, + devServer: { + contentBase: path.join(__dirname, '../'), + open: true, + openPage: 'samples/index.html', + hot: true, + compress: true, + port: 3000, + watchOptions: { + aggregateTimeout: 300, + poll: 1000 + } + }, + plugins: [ + new webpack.HotModuleReplacementPlugin(), + ] +}); + +module.exports = config; diff --git a/build/webpack.prod.js b/build/webpack.prod.js new file mode 100644 index 0000000000..8e1dd9296f --- /dev/null +++ b/build/webpack.prod.js @@ -0,0 +1,40 @@ +const { merge } = require('webpack-merge'); +const ESLintPlugin = require('eslint-webpack-plugin'); +const common = require('./webpack.base.js').config; + +const entries = { + 'dash.all': './index.js', + 'dash.mss': './src/mss/index.js', + 'dash.mediaplayer': './index_mediaplayerOnly.js', + 'dash.protection': './src/streaming/protection/Protection.js', + 'dash.reporting': './src/streaming/metrics/MetricsReporting.js', + 'dash.offline': './src/offline/index.js' +} + +const configDev = merge(common, { + mode: 'development', + entry: entries, + output: { + filename: '[name].debug.js' + } +}); + +const configProd = merge(common, { + mode: 'production', + entry: entries, + output: { + filename: '[name].min.js' + }, + performance: { hints: false }, + plugins: [ + new ESLintPlugin({ + files: [ + 'src/**/*.js', + 'test/unit/mocks/*.js', + 'test/unit/*.js' + ] + }) + ] +}); + +module.exports = [configDev, configProd]; diff --git a/contrib/akamai/controlbar/ControlBar.js b/contrib/akamai/controlbar/ControlBar.js index b9c09c4b9d..3864dbdeea 100644 --- a/contrib/akamai/controlbar/ControlBar.js +++ b/contrib/akamai/controlbar/ControlBar.js @@ -34,6 +34,7 @@ * @param {object=} dashjsMediaPlayer - dashjs reference * @param {boolean=} displayUTCTimeCodes - true if time is displayed in UTC format, false otherwise */ +// eslint-disable-next-line no-unused-vars var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) { var player = this.player = dashjsMediaPlayer; @@ -42,11 +43,17 @@ var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) { var captionMenu = null; var bitrateListMenu = null; var trackSwitchMenu = null; - var menuHandlersList = []; + var menuHandlersList = { + bitrate: null, + caption: null, + track: null + }; var lastVolumeLevel = NaN; var seeking = false; var videoControllerVisibleTimeout = 0; - var liveThresholdSecs = 12; + var liveThresholdSecs = 1; + var textTrackList = {}; + var forceQuality = false; var video, videoContainer, videoController, @@ -66,7 +73,7 @@ var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) { thumbnailElem, thumbnailTimeLabel, idSuffix, - startedPlaying; + seekbarBufferInterval; //************************************************************************************ // THUMBNAIL CONSTANTS @@ -99,24 +106,26 @@ var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) { }; var addPlayerEventsListeners = function () { - self.player.on(dashjs.MediaPlayer.events.PLAYBACK_STARTED, onPlayStart, this); - self.player.on(dashjs.MediaPlayer.events.PLAYBACK_PAUSED, onPlaybackPaused, this); - self.player.on(dashjs.MediaPlayer.events.PLAYBACK_TIME_UPDATED, onPlayTimeUpdate, this); - self.player.on(dashjs.MediaPlayer.events.TEXT_TRACKS_ADDED, onTracksAdded, this); - self.player.on(dashjs.MediaPlayer.events.STREAM_INITIALIZED, onStreamInitialized, this); - self.player.on(dashjs.MediaPlayer.events.STREAM_TEARDOWN_COMPLETE, onStreamTeardownComplete, this); - self.player.on(dashjs.MediaPlayer.events.SOURCE_INITIALIZED, onSourceInitialized, this); - } + self.player.on(dashjs.MediaPlayer.events.PLAYBACK_STARTED, _onPlayStart, this); + self.player.on(dashjs.MediaPlayer.events.PLAYBACK_PAUSED, _onPlaybackPaused, this); + self.player.on(dashjs.MediaPlayer.events.PLAYBACK_TIME_UPDATED, _onPlayTimeUpdate, this); + self.player.on(dashjs.MediaPlayer.events.STREAM_ACTIVATED, _onStreamActivated, this); + self.player.on(dashjs.MediaPlayer.events.STREAM_DEACTIVATED, _onStreamDeactivated, this); + self.player.on(dashjs.MediaPlayer.events.STREAM_TEARDOWN_COMPLETE, _onStreamTeardownComplete, this); + self.player.on(dashjs.MediaPlayer.events.TEXT_TRACKS_ADDED, _onTracksAdded, this); + self.player.on(dashjs.MediaPlayer.events.BUFFER_LEVEL_UPDATED, _onBufferLevelUpdated, this); + }; var removePlayerEventsListeners = function () { - self.player.off(dashjs.MediaPlayer.events.PLAYBACK_STARTED, onPlayStart, this); - self.player.off(dashjs.MediaPlayer.events.PLAYBACK_PAUSED, onPlaybackPaused, this); - self.player.off(dashjs.MediaPlayer.events.PLAYBACK_TIME_UPDATED, onPlayTimeUpdate, this); - self.player.off(dashjs.MediaPlayer.events.TEXT_TRACKS_ADDED, onTracksAdded, this); - self.player.off(dashjs.MediaPlayer.events.STREAM_INITIALIZED, onStreamInitialized, this); - self.player.off(dashjs.MediaPlayer.events.STREAM_TEARDOWN_COMPLETE, onStreamTeardownComplete, this); - self.player.off(dashjs.MediaPlayer.events.SOURCE_INITIALIZED, onSourceInitialized, this); - } + self.player.off(dashjs.MediaPlayer.events.PLAYBACK_STARTED, _onPlayStart, this); + self.player.off(dashjs.MediaPlayer.events.PLAYBACK_PAUSED, _onPlaybackPaused, this); + self.player.off(dashjs.MediaPlayer.events.PLAYBACK_TIME_UPDATED, _onPlayTimeUpdate, this); + self.player.off(dashjs.MediaPlayer.events.STREAM_ACTIVATED, _onStreamActivated, this); + self.player.off(dashjs.MediaPlayer.events.STREAM_DEACTIVATED, _onStreamDeactivated, this); + self.player.off(dashjs.MediaPlayer.events.STREAM_TEARDOWN_COMPLETE, _onStreamTeardownComplete, this); + self.player.off(dashjs.MediaPlayer.events.TEXT_TRACKS_ADDED, _onTracksAdded, this); + self.player.off(dashjs.MediaPlayer.events.BUFFER_LEVEL_UPDATED, _onBufferLevelUpdated, this); + }; var getControlId = function (id) { return id + (idSuffix ? idSuffix : ''); @@ -158,38 +167,34 @@ var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) { } }; - var onPlayPauseClick = function (/*e*/) { + var _onPlayPauseClick = function (/*e*/) { togglePlayPauseBtnState.call(this); self.player.isPaused() ? self.player.play() : self.player.pause(); }; - var onPlaybackPaused = function (/*e*/) { + var _onPlaybackPaused = function (/*e*/) { togglePlayPauseBtnState(); }; - var onPlayStart = function (/*e*/) { + var _onPlayStart = function (/*e*/) { setTime(displayUTCTimeCodes ? self.player.timeAsUTC() : self.player.time()); updateDuration(); togglePlayPauseBtnState(); + if (seekbarBufferInterval) { + clearInterval(seekbarBufferInterval); + } }; - var onPlayTimeUpdate = function (/*e*/) { + var _onPlayTimeUpdate = function (/*e*/) { updateDuration(); if (!seeking) { - setTime(displayUTCTimeCodes ? self.player.timeAsUTC() : self.player.time()); + setTime(displayUTCTimeCodes ? player.timeAsUTC() : player.time()); if (seekbarPlay) { - if (self.player.isDynamic() && (self.player.duration() - self.player.time() < liveThresholdSecs)) { - seekbarPlay.style.width = '100%'; - } else { - seekbarPlay.style.width = (self.player.time() / self.player.duration() * 100) + '%'; - } - } - if (seekbarBuffer) { - seekbarBuffer.style.width = ((self.player.time() + getBufferLevel()) / self.player.duration() * 100) + '%'; + seekbarPlay.style.width = (player.time() / player.duration() * 100) + '%'; } if (seekbar.getAttribute('type') === 'range') { - seekbar.value = self.player.time(); + seekbar.value = player.time(); } } @@ -353,21 +358,8 @@ var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) { thumbnailContainer.style.display = 'none'; }; - var getScrollOffset = function () { - if (window.pageXOffset) { - return { - x: window.pageXOffset, - y: window.pageYOffset - }; - } - return { - x: document.documentElement.scrollLeft, - y: document.documentElement.scrollTop - }; - }; - var seekLive = function () { - self.player.seek(self.player.duration()); + self.player.seekToOriginalLive(); }; //************************************************************************************ @@ -392,13 +384,14 @@ var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) { } if (self.player.isDynamic() && self.player.duration()) { var liveDelay = self.player.duration() - value; - if (liveDelay < liveThresholdSecs) { + var targetLiveDelay = self.player.getTargetLiveDelay(); + + if (liveDelay < targetLiveDelay + liveThresholdSecs) { durationDisplay.classList.add('live'); - timeDisplay.textContent = ''; } else { durationDisplay.classList.remove('live'); - timeDisplay.textContent = '- ' + self.player.convertToTimeCode(liveDelay); } + timeDisplay.textContent = '- ' + self.player.convertToTimeCode(liveDelay); } else if (!isNaN(value)) { timeDisplay.textContent = displayUTCTimeCodes ? self.player.formatUTC(value) : self.player.convertToTimeCode(value); } @@ -437,16 +430,18 @@ var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) { var enterFullscreen = function () { var element = videoContainer || video; - - if (element.requestFullscreen) { - element.requestFullscreen(); - } else if (element.msRequestFullscreen) { - element.msRequestFullscreen(); - } else if (element.mozRequestFullScreen) { - element.mozRequestFullScreen(); - } else { - element.webkitRequestFullScreen(); + if (!document.fullscreenElement) { + if (element.requestFullscreen) { + element.requestFullscreen(); + } else if (element.msRequestFullscreen) { + element.msRequestFullscreen(); + } else if (element.mozRequestFullScreen) { + element.mozRequestFullScreen(); + } else { + element.webkitRequestFullScreen(); + } } + videoController.classList.add('video-controller-fullscreen'); window.addEventListener('mousemove', onFullScreenMouseMove); onFullScreenMouseMove(); @@ -469,16 +464,18 @@ var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) { clearFullscreenState(); if (document.fullscreenElement) { - document.exitFullscreen(); - } else if (document.exitFullscreen) { - document.exitFullscreen(); - } else if (document.mozCancelFullScreen) { - document.mozCancelFullScreen(); - } else if (document.msExitFullscreen) { - document.msExitFullscreen(); - } else { - document.webkitCancelFullScreen(); + + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } else { + document.webkitCancelFullScreen(); + } } + videoController.classList.remove('video-controller-fullscreen'); }; @@ -503,49 +500,39 @@ var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) { // Audio Video MENU //************************************************************************************ - var onTracksAdded = function (e) { - // Subtitles/Captions Menu //XXX we need to add two layers for captions & subtitles if present. - if (!captionMenu) { - var contentFunc = function (element, index) { - if (isNaN(index)) { - return 'OFF'; - } + var _onStreamDeactivated = function (e) { + if (e.streamInfo && textTrackList[e.streamInfo.id]) { + delete textTrackList[e.streamInfo.id]; + } + }; - var label = getLabelForLocale(element.labels); - if (label) { - return label + ' : ' + element.kind; - } + var _onStreamActivated = function (e) { + var streamInfo = e.streamInfo; - return element.lang + ' : ' + element.kind; - }; - captionMenu = createMenu({ menuType: 'caption', arr: e.tracks }, contentFunc); + updateDuration(); - var func = function () { - onMenuClick(captionMenu, captionBtn); - } - menuHandlersList.push(func); - captionBtn.addEventListener('click', func); - captionBtn.classList.remove('hide'); - } else if (e.index !== undefined) { - setMenuItemsState(e.index + 1, 'caption-list'); - } - }; + //Bitrate Menu + createBitrateSwitchMenu(); - var onSourceInitialized = function () { - startedPlaying = false; + //Track Switch Menu + createTrackSwitchMenu(); + + //Text Switch Menu + createCaptionSwitchMenu(streamInfo); }; - var onStreamInitialized = function (/*e*/) { - updateDuration(); + var createBitrateSwitchMenu = function () { var contentFunc; - //Bitrate Menu + if (bitrateListBtn) { - destroyBitrateMenu(); + destroyMenu(bitrateListMenu, bitrateListBtn, menuHandlersList.bitrate); + bitrateListMenu = null; var availableBitrates = { menuType: 'bitrate' }; availableBitrates.audio = self.player.getBitrateInfoListFor && self.player.getBitrateInfoListFor('audio') || []; availableBitrates.video = self.player.getBitrateInfoListFor && self.player.getBitrateInfoListFor('video') || []; availableBitrates.images = self.player.getBitrateInfoListFor && self.player.getBitrateInfoListFor('image') || []; - if (availableBitrates.audio.length > 1 || availableBitrates.video.length > 1 || availableBitrates.images.length > 1) { + + if (availableBitrates.audio.length >= 1 || availableBitrates.video.length >= 1 || availableBitrates.images.length >= 1) { contentFunc = function (element, index) { var result = isNaN(index) ? ' Auto Switch' : Math.floor(element.bitrate / 1000) + ' kbps'; result += element && element.width && element.height ? ' (' + element.width + 'x' + element.height + ')' : ''; @@ -556,7 +543,7 @@ var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) { var func = function () { onMenuClick(bitrateListMenu, bitrateListBtn); }; - menuHandlersList.push(func); + menuHandlersList.bitrate = func; bitrateListBtn.addEventListener('click', func); bitrateListBtn.classList.remove('hide'); @@ -564,28 +551,102 @@ var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) { bitrateListBtn.classList.add('hide'); } } - //Track Switch Menu - if (!trackSwitchMenu && trackSwitchBtn) { + }; + + var createTrackSwitchMenu = function () { + var contentFunc; + + if (trackSwitchBtn) { + + destroyMenu(trackSwitchMenu, trackSwitchBtn, menuHandlersList.track); + trackSwitchMenu = null; + var availableTracks = { menuType: 'track' }; availableTracks.audio = self.player.getTracksFor('audio'); availableTracks.video = self.player.getTracksFor('video'); // these return empty arrays so no need to check for null if (availableTracks.audio.length > 1 || availableTracks.video.length > 1) { contentFunc = function (element) { - return getLabelForLocale(element.labels) || 'Language: ' + element.lang + ' - Role: ' + element.roles[0]; + var label = getLabelForLocale(element.labels); + var info = ''; + + if (element.lang) { + info += 'Language - ' + element.lang + ' '; + } + + if (element.roles[0]) { + info += '- Role: ' + element.roles[0] + ' '; + } + + if (element.codec) { + info += '- Codec: ' + element.codec + ' '; + } + + return label || info }; trackSwitchMenu = createMenu(availableTracks, contentFunc); var func = function () { onMenuClick(trackSwitchMenu, trackSwitchBtn); }; - menuHandlersList.push(func); + menuHandlersList.track = func; trackSwitchBtn.addEventListener('click', func); trackSwitchBtn.classList.remove('hide'); } } }; - var onStreamTeardownComplete = function (/*e*/) { + var createCaptionSwitchMenu = function (streamId) { + // Subtitles/Captions Menu //XXX we need to add two layers for captions & subtitles if present. + var activeStreamInfo = player.getActiveStream().getStreamInfo(); + + if (captionBtn && (!activeStreamInfo.id || activeStreamInfo.id === streamId)) { + + destroyMenu(captionMenu, captionBtn, menuHandlersList.caption); + captionMenu = null; + + var tracks = textTrackList[streamId] || []; + var contentFunc = function (element, index) { + if (isNaN(index)) { + return 'OFF'; + } + + var label = getLabelForLocale(element.labels); + if (label) { + return label + ' : ' + element.type; + } + + return element.lang + ' : ' + element.kind; + }; + captionMenu = createMenu({ menuType: 'caption', arr: tracks }, contentFunc); + + var func = function () { + onMenuClick(captionMenu, captionBtn); + }; + + menuHandlersList.caption = func; + captionBtn.addEventListener('click', func); + captionBtn.classList.remove('hide'); + } + + }; + + var _onTracksAdded = function (e) { + // Subtitles/Captions Menu //XXX we need to add two layers for captions & subtitles if present. + if (!textTrackList[e.streamId]) { + textTrackList[e.streamId] = []; + } + + textTrackList[e.streamId] = textTrackList[e.streamId].concat(e.tracks); + createCaptionSwitchMenu(e.streamId); + }; + + var _onBufferLevelUpdated = function () { + if (seekbarBuffer) { + seekbarBuffer.style.width = ((player.time() + getBufferLevel()) / player.duration() * 100) + '%'; + } + }; + + var _onStreamTeardownComplete = function (/*e*/) { setPlayBtn(); timeDisplay.textContent = '00:00'; }; @@ -608,17 +669,17 @@ var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) { break; case 'track': case 'bitrate': - if (info.video.length > 1) { + if (info.video.length >= 1) { el.appendChild(createMediaTypeMenu('video')); el = createMenuContent(el, getMenuContent(menuType, info.video, contentFunc), 'video', 'video-' + menuType + '-list'); setMenuItemsState(getMenuInitialIndex(info.video, menuType, 'video'), 'video-' + menuType + '-list'); } - if (info.audio.length > 1) { + if (info.audio.length >= 1) { el.appendChild(createMediaTypeMenu('audio')); el = createMenuContent(el, getMenuContent(menuType, info.audio, contentFunc), 'audio', 'audio-' + menuType + '-list'); setMenuItemsState(getMenuInitialIndex(info.audio, menuType, 'audio'), 'audio-' + menuType + '-list'); } - if (info.images && info.images.length > 1) { + if (info.images && info.images.length >= 1) { el.appendChild(createMediaTypeMenu('image')); el = createMenuContent(el, getMenuContent(menuType, info.images, contentFunc, false), 'image', 'image-' + menuType + '-list'); setMenuItemsState(getMenuInitialIndex(info.images, menuType, 'image'), 'image-' + menuType + '-list'); @@ -761,52 +822,57 @@ var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) { }; var setMenuItemsState = function (value, type) { - var item = typeof value === 'number' ? document.getElementById(type + 'Item_' + value) : this; - var nodes = item.parentElement.children; - - for (var i = 0; i < nodes.length; i++) { - nodes[i].selected = false; - nodes[i].classList.remove('menu-item-selected'); - nodes[i].classList.add('menu-item-unselected'); - } - item.selected = true; - item.classList.remove('menu-item-over'); - item.classList.remove('menu-item-unselected'); - item.classList.add('menu-item-selected'); - - if (type === undefined) { // User clicked so type is part of item binding. - switch (item.name) { - case 'video-bitrate-list': - case 'audio-bitrate-list': - var cfg = { - 'streaming': { - 'abr': { - 'autoSwitchBitrate': { + try { + var item = typeof value === 'number' ? document.getElementById(type + 'Item_' + value) : this; + if (item) { + var nodes = item.parentElement.children; + + for (var i = 0; i < nodes.length; i++) { + nodes[i].selected = false; + nodes[i].classList.remove('menu-item-selected'); + nodes[i].classList.add('menu-item-unselected'); + } + item.selected = true; + item.classList.remove('menu-item-over'); + item.classList.remove('menu-item-unselected'); + item.classList.add('menu-item-selected'); + + if (type === undefined) { // User clicked so type is part of item binding. + switch (item.name) { + case 'video-bitrate-list': + case 'audio-bitrate-list': + var cfg = { + 'streaming': { + 'abr': { + 'autoSwitchBitrate': {} + } } + }; + + if (item.index > 0) { + cfg.streaming.abr.autoSwitchBitrate[item.mediaType] = false; + self.player.updateSettings(cfg); + self.player.setQualityFor(item.mediaType, item.index - 1, forceQuality); + } else { + cfg.streaming.abr.autoSwitchBitrate[item.mediaType] = true; + self.player.updateSettings(cfg); } - } - }; - - if (item.index > 0) { - cfg.streaming.abr.autoSwitchBitrate[item.mediaType] = false; - self.player.updateSettings(cfg); - self.player.setQualityFor(item.mediaType, item.index - 1); - } else { - cfg.streaming.abr.autoSwitchBitrate[item.mediaType] = true; - self.player.updateSettings(cfg); + break; + case 'image-bitrate-list': + player.setQualityFor(item.mediaType, item.index); + break; + case 'caption-list': + self.player.setTextTrack(item.index - 1); + break; + case 'video-track-list': + case 'audio-track-list': + self.player.setCurrentTrack(self.player.getTracksFor(item.mediaType)[item.index]); + break; } - break; - case 'image-bitrate-list': - player.setQualityFor(self.mediaType, self.index); - break; - case 'caption-list': - self.player.setTextTrack(item.index - 1); - break; - case 'video-track-list': - case 'audio-track-list': - self.player.setCurrentTrack(self.player.getTracksFor(item.mediaType)[item.index]); - break; + } } + } catch (e) { + console.error(e); } }; @@ -833,13 +899,24 @@ var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) { menu.style.top = menu_y + 'px'; }; - var destroyBitrateMenu = function () { - if (bitrateListMenu) { - menuHandlersList.forEach(function (item) { - bitrateListBtn.removeEventListener('click', item); - }); - videoController.removeChild(bitrateListMenu); - bitrateListMenu = null; + var destroyMenu = function (menu, btn, handler) { + try { + if (menu && videoController) { + btn.removeEventListener('click', handler); + videoController.removeChild(menu); + } + } catch (e) { + } + }; + + var removeMenu = function (menu, btn) { + try { + if (menu) { + videoController.removeChild(menu); + menu = null; + btn.classList.add('hide'); + } + } catch (e) { } }; @@ -875,7 +952,6 @@ var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) { return !!navigator.userAgent.match(/Trident.*rv[ :]*11\./); }; - //************************************************************************************ // PUBLIC API //************************************************************************************ @@ -885,6 +961,7 @@ var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) { setDuration: setDuration, setTime: setTime, setPlayer: setPlayer, + removeMenu: removeMenu, initialize: function (suffix) { @@ -906,7 +983,7 @@ var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) { trackSwitchBtn.classList.add('hide'); } addPlayerEventsListeners(); - playPauseBtn.addEventListener('click', onPlayPauseClick); + playPauseBtn.addEventListener('click', _onPlayPauseClick); muteBtn.addEventListener('click', onMuteClick); fullscreenBtn.addEventListener('click', onFullscreenClick); seekbar.addEventListener('mousedown', onSeeking, true); @@ -945,23 +1022,36 @@ var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) { videoController.classList.remove('disable'); }, - reset: function () { - window.removeEventListener('resize', handleMenuPositionOnResize); - destroyBitrateMenu(); - menuHandlersList.forEach(function (item) { - if (trackSwitchBtn) trackSwitchBtn.removeEventListener('click', item); - if (captionBtn) captionBtn.removeEventListener('click', item); - }); + forceQualitySwitch: function (value) { + forceQuality = value; + }, + + resetSelectionMenus: function () { + if (menuHandlersList.bitrate) { + bitrateListBtn.removeEventListener('click', menuHandlersList.bitrate); + } + if (menuHandlersList.track) { + trackSwitchBtn.removeEventListener('click', menuHandlersList.track); + } + if (menuHandlersList.caption) { + captionBtn.removeEventListener('click', menuHandlersList.caption); + } if (captionMenu) { - videoController.removeChild(captionMenu); - captionMenu = null; - captionBtn.classList.add('hide'); + this.removeMenu(captionMenu, captionBtn); } if (trackSwitchMenu) { - videoController.removeChild(trackSwitchMenu); - trackSwitchMenu = null; - trackSwitchBtn.classList.add('hide'); + this.removeMenu(trackSwitchMenu, trackSwitchBtn); } + if (bitrateListMenu) { + this.removeMenu(bitrateListMenu, bitrateListBtn); + } + }, + + reset: function () { + window.removeEventListener('resize', handleMenuPositionOnResize); + + this.resetSelectionMenus(); + menuHandlersList = []; seeking = false; @@ -977,7 +1067,7 @@ var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) { destroy: function () { this.reset(); - playPauseBtn.removeEventListener('click', onPlayPauseClick); + playPauseBtn.removeEventListener('click', _onPlayPauseClick); muteBtn.removeEventListener('click', onMuteClick); fullscreenBtn.removeEventListener('click', onFullscreenClick); seekbar.removeEventListener('mousedown', onSeeking); diff --git a/deploy.js b/deploy.js new file mode 100644 index 0000000000..e63ced632e --- /dev/null +++ b/deploy.js @@ -0,0 +1,66 @@ +const argv = require('yargs').argv; +const path = require('path'); +const child = require('child_process'); +const replace = require('replace-in-file'); +const FtpDeploy = require("ftp-deploy"); +const ftpDeploy = new FtpDeploy(); + +function execSync(command) { + // console.info('Exec command: ' + command); + var res = child.execSync(command); + res = String(res).trim(); + return res; +} + +// Replace in samples/dash-if-reference-player/index.html with git branch and commit +var branch = execSync('git rev-parse --abbrev-ref HEAD'); +var hash = execSync('git rev-parse HEAD'); + +var str = `(${branch}, commit: ${hash.substring(0, 8)})`; + +try { + replace.sync({ + files: 'samples/dash-if-reference-player/index.html', + from: '', + to: str + }); +} catch (e) { + console.error('Failed to replace git info: ', e); + process.exit(1); +} + +// Push files ont ftp server +var config = { + user: argv.user, + password: argv.password, + host: argv.host, + port: argv.port, + localRoot: __dirname, + remoteRoot: '/', + include: [ + 'contrib/**', + 'dist/**', + 'test/functional/tests.html', + 'test/functional/testsCommon.js', + 'test/functional/config/**', + 'test/functional/tests/**', + 'samples/**' + ], + // delete ALL existing files at destination before uploading, if true + deleteRemote: false, + // Passive mode is forced (EPSV command is not sent) + forcePasv: true, + // use sftp or ftp + sftp: false +}; + +ftpDeploy + .deploy(config) + .then(res => console.log("finished:", res)) + .catch(err => { + console.log(err); + process.exit(1); + }); + + + diff --git a/dist/dash.all.debug.js b/dist/dash.all.debug.js deleted file mode 100644 index b48266265c..0000000000 --- a/dist/dash.all.debug.js +++ /dev/null @@ -1,65610 +0,0 @@ -(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i> 6); - u.push(0x80 | 63 & c); - } else if (c < 0x10000) { - u.push(0xE0 | c >> 12); - u.push(0x80 | 63 & c >> 6); - u.push(0x80 | 63 & c); - } else { - u.push(0xF0 | c >> 18); - u.push(0x80 | 63 & c >> 12); - u.push(0x80 | 63 & c >> 6); - u.push(0x80 | 63 & c); - } - } - return u; -}; -UTF8.decode = function (u) { - var a = []; - var i = 0; - while (i < u.length) { - var v = u[i++]; - if (v < 0x80) { - // no need to mask byte - } else if (v < 0xE0) { - v = (31 & v) << 6; - v |= 63 & u[i++]; - } else if (v < 0xF0) { - v = (15 & v) << 12; - v |= (63 & u[i++]) << 6; - v |= 63 & u[i++]; - } else { - v = (7 & v) << 18; - v |= (63 & u[i++]) << 12; - v |= (63 & u[i++]) << 6; - v |= 63 & u[i++]; - } - a.push(String.fromCharCode(v)); - } - return a.join(''); -}; - -var BASE64 = {}; -(function (T) { - var encodeArray = function encodeArray(u) { - var i = 0; - var a = []; - var n = 0 | u.length / 3; - while (0 < n--) { - var v = (u[i] << 16) + (u[i + 1] << 8) + u[i + 2]; - i += 3; - a.push(T.charAt(63 & v >> 18)); - a.push(T.charAt(63 & v >> 12)); - a.push(T.charAt(63 & v >> 6)); - a.push(T.charAt(63 & v)); - } - if (2 == u.length - i) { - var v = (u[i] << 16) + (u[i + 1] << 8); - a.push(T.charAt(63 & v >> 18)); - a.push(T.charAt(63 & v >> 12)); - a.push(T.charAt(63 & v >> 6)); - a.push('='); - } else if (1 == u.length - i) { - var v = u[i] << 16; - a.push(T.charAt(63 & v >> 18)); - a.push(T.charAt(63 & v >> 12)); - a.push('=='); - } - return a.join(''); - }; - var R = (function () { - var a = []; - for (var i = 0; i < T.length; ++i) { - a[T.charCodeAt(i)] = i; - } - a['='.charCodeAt(0)] = 0; - return a; - })(); - var decodeArray = function decodeArray(s) { - var i = 0; - var u = []; - var n = 0 | s.length / 4; - while (0 < n--) { - var v = (R[s.charCodeAt(i)] << 18) + (R[s.charCodeAt(i + 1)] << 12) + (R[s.charCodeAt(i + 2)] << 6) + R[s.charCodeAt(i + 3)]; - u.push(255 & v >> 16); - u.push(255 & v >> 8); - u.push(255 & v); - i += 4; - } - if (u) { - if ('=' == s.charAt(i - 2)) { - u.pop(); - u.pop(); - } else if ('=' == s.charAt(i - 1)) { - u.pop(); - } - } - return u; - }; - var ASCII = {}; - ASCII.encode = function (s) { - var u = []; - for (var i = 0; i < s.length; ++i) { - u.push(s.charCodeAt(i)); - } - return u; - }; - ASCII.decode = function (u) { - for (var i = 0; i < s.length; ++i) { - a[i] = String.fromCharCode(a[i]); - } - return a.join(''); - }; - BASE64.decodeArray = function (s) { - var u = decodeArray(s); - return new Uint8Array(u); - }; - BASE64.encodeASCII = function (s) { - var u = ASCII.encode(s); - return encodeArray(u); - }; - BASE64.decodeASCII = function (s) { - var a = decodeArray(s); - return ASCII.decode(a); - }; - BASE64.encode = function (s) { - var u = UTF8.encode(s); - return encodeArray(u); - }; - BASE64.decode = function (s) { - var u = decodeArray(s); - return UTF8.decode(u); - }; -})("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"); - -/*The following polyfills are not used in dash.js but have caused multiplayer integration issues. - Therefore commenting them out. -if (undefined === btoa) { - var btoa = BASE64.encode; -} -if (undefined === atob) { - var atob = BASE64.decode; -} -*/ - -if (typeof exports !== 'undefined') { - exports.decode = BASE64.decode; - exports.decodeArray = BASE64.decodeArray; - exports.encode = BASE64.encode; - exports.encodeASCII = BASE64.encodeASCII; -} - -},{}],2:[function(_dereq_,module,exports){ -/** - * The copyright in this software is being made available under the BSD License, - * included below. This software may be subject to other third party and contributor - * rights, including patent rights, and no such rights are granted under this license. - * - * Copyright (c) 2015-2016, DASH Industry Forum. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * 2. Neither the name of Dash Industry Forum nor the names of its - * contributors may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -'use strict'; - -(function (exports) { - - "use strict"; - - /** - * Exceptions from regular ASCII. CodePoints are mapped to UTF-16 codes - */ - - var specialCea608CharsCodes = { - 0x2a: 0xe1, // lowercase a, acute accent - 0x5c: 0xe9, // lowercase e, acute accent - 0x5e: 0xed, // lowercase i, acute accent - 0x5f: 0xf3, // lowercase o, acute accent - 0x60: 0xfa, // lowercase u, acute accent - 0x7b: 0xe7, // lowercase c with cedilla - 0x7c: 0xf7, // division symbol - 0x7d: 0xd1, // uppercase N tilde - 0x7e: 0xf1, // lowercase n tilde - 0x7f: 0x2588, // Full block - // THIS BLOCK INCLUDES THE 16 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS - // THAT COME FROM HI BYTE=0x11 AND LOW BETWEEN 0x30 AND 0x3F - // THIS MEANS THAT \x50 MUST BE ADDED TO THE VALUES - 0x80: 0xae, // Registered symbol (R) - 0x81: 0xb0, // degree sign - 0x82: 0xbd, // 1/2 symbol - 0x83: 0xbf, // Inverted (open) question mark - 0x84: 0x2122, // Trademark symbol (TM) - 0x85: 0xa2, // Cents symbol - 0x86: 0xa3, // Pounds sterling - 0x87: 0x266a, // Music 8'th note - 0x88: 0xe0, // lowercase a, grave accent - 0x89: 0x20, // transparent space (regular) - 0x8a: 0xe8, // lowercase e, grave accent - 0x8b: 0xe2, // lowercase a, circumflex accent - 0x8c: 0xea, // lowercase e, circumflex accent - 0x8d: 0xee, // lowercase i, circumflex accent - 0x8e: 0xf4, // lowercase o, circumflex accent - 0x8f: 0xfb, // lowercase u, circumflex accent - // THIS BLOCK INCLUDES THE 32 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS - // THAT COME FROM HI BYTE=0x12 AND LOW BETWEEN 0x20 AND 0x3F - 0x90: 0xc1, // capital letter A with acute - 0x91: 0xc9, // capital letter E with acute - 0x92: 0xd3, // capital letter O with acute - 0x93: 0xda, // capital letter U with acute - 0x94: 0xdc, // capital letter U with diaresis - 0x95: 0xfc, // lowercase letter U with diaeresis - 0x96: 0x2018, // opening single quote - 0x97: 0xa1, // inverted exclamation mark - 0x98: 0x2a, // asterisk - 0x99: 0x2019, // closing single quote - 0x9a: 0x2501, // box drawings heavy horizontal - 0x9b: 0xa9, // copyright sign - 0x9c: 0x2120, // Service mark - 0x9d: 0x2022, // (round) bullet - 0x9e: 0x201c, // Left double quotation mark - 0x9f: 0x201d, // Right double quotation mark - 0xa0: 0xc0, // uppercase A, grave accent - 0xa1: 0xc2, // uppercase A, circumflex - 0xa2: 0xc7, // uppercase C with cedilla - 0xa3: 0xc8, // uppercase E, grave accent - 0xa4: 0xca, // uppercase E, circumflex - 0xa5: 0xcb, // capital letter E with diaresis - 0xa6: 0xeb, // lowercase letter e with diaresis - 0xa7: 0xce, // uppercase I, circumflex - 0xa8: 0xcf, // uppercase I, with diaresis - 0xa9: 0xef, // lowercase i, with diaresis - 0xaa: 0xd4, // uppercase O, circumflex - 0xab: 0xd9, // uppercase U, grave accent - 0xac: 0xf9, // lowercase u, grave accent - 0xad: 0xdb, // uppercase U, circumflex - 0xae: 0xab, // left-pointing double angle quotation mark - 0xaf: 0xbb, // right-pointing double angle quotation mark - // THIS BLOCK INCLUDES THE 32 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS - // THAT COME FROM HI BYTE=0x13 AND LOW BETWEEN 0x20 AND 0x3F - 0xb0: 0xc3, // Uppercase A, tilde - 0xb1: 0xe3, // Lowercase a, tilde - 0xb2: 0xcd, // Uppercase I, acute accent - 0xb3: 0xcc, // Uppercase I, grave accent - 0xb4: 0xec, // Lowercase i, grave accent - 0xb5: 0xd2, // Uppercase O, grave accent - 0xb6: 0xf2, // Lowercase o, grave accent - 0xb7: 0xd5, // Uppercase O, tilde - 0xb8: 0xf5, // Lowercase o, tilde - 0xb9: 0x7b, // Open curly brace - 0xba: 0x7d, // Closing curly brace - 0xbb: 0x5c, // Backslash - 0xbc: 0x5e, // Caret - 0xbd: 0x5f, // Underscore - 0xbe: 0x7c, // Pipe (vertical line) - 0xbf: 0x223c, // Tilde operator - 0xc0: 0xc4, // Uppercase A, umlaut - 0xc1: 0xe4, // Lowercase A, umlaut - 0xc2: 0xd6, // Uppercase O, umlaut - 0xc3: 0xf6, // Lowercase o, umlaut - 0xc4: 0xdf, // Esszett (sharp S) - 0xc5: 0xa5, // Yen symbol - 0xc6: 0xa4, // Generic currency sign - 0xc7: 0x2503, // Box drawings heavy vertical - 0xc8: 0xc5, // Uppercase A, ring - 0xc9: 0xe5, // Lowercase A, ring - 0xca: 0xd8, // Uppercase O, stroke - 0xcb: 0xf8, // Lowercase o, strok - 0xcc: 0x250f, // Box drawings heavy down and right - 0xcd: 0x2513, // Box drawings heavy down and left - 0xce: 0x2517, // Box drawings heavy up and right - 0xcf: 0x251b // Box drawings heavy up and left - }; - - /** - * Get Unicode Character from CEA-608 byte code - */ - var getCharForByte = function getCharForByte(byte) { - var charCode = byte; - if (specialCea608CharsCodes.hasOwnProperty(byte)) { - charCode = specialCea608CharsCodes[byte]; - } - return String.fromCharCode(charCode); - }; - - var NR_ROWS = 15, - NR_COLS = 32; - // Tables to look up row from PAC data - var rowsLowCh1 = { 0x11: 1, 0x12: 3, 0x15: 5, 0x16: 7, 0x17: 9, 0x10: 11, 0x13: 12, 0x14: 14 }; - var rowsHighCh1 = { 0x11: 2, 0x12: 4, 0x15: 6, 0x16: 8, 0x17: 10, 0x13: 13, 0x14: 15 }; - var rowsLowCh2 = { 0x19: 1, 0x1A: 3, 0x1D: 5, 0x1E: 7, 0x1F: 9, 0x18: 11, 0x1B: 12, 0x1C: 14 }; - var rowsHighCh2 = { 0x19: 2, 0x1A: 4, 0x1D: 6, 0x1E: 8, 0x1F: 10, 0x1B: 13, 0x1C: 15 }; - - var backgroundColors = ['white', 'green', 'blue', 'cyan', 'red', 'yellow', 'magenta', 'black', 'transparent']; - - /** - * Simple logger class to be able to write with time-stamps and filter on level. - */ - var logger = { - verboseFilter: { 'DATA': 3, 'DEBUG': 3, 'INFO': 2, 'WARNING': 2, 'TEXT': 1, 'ERROR': 0 }, - time: null, - verboseLevel: 0, // Only write errors - setTime: function setTime(newTime) { - this.time = newTime; - }, - log: function log(severity, msg) { - var minLevel = this.verboseFilter[severity]; - if (this.verboseLevel >= minLevel) { - console.log(this.time + " [" + severity + "] " + msg); - } - } - }; - - var numArrayToHexArray = function numArrayToHexArray(numArray) { - var hexArray = []; - for (var j = 0; j < numArray.length; j++) { - hexArray.push(numArray[j].toString(16)); - } - return hexArray; - }; - - /** - * State of CEA-608 pen or character - * @constructor - */ - var PenState = function PenState(foreground, underline, italics, background, flash) { - this.foreground = foreground || "white"; - this.underline = underline || false; - this.italics = italics || false; - this.background = background || "black"; - this.flash = flash || false; - }; - - PenState.prototype = { - - reset: function reset() { - this.foreground = "white"; - this.underline = false; - this.italics = false; - this.background = "black"; - this.flash = false; - }, - - setStyles: function setStyles(styles) { - var attribs = ["foreground", "underline", "italics", "background", "flash"]; - for (var i = 0; i < attribs.length; i++) { - var style = attribs[i]; - if (styles.hasOwnProperty(style)) { - this[style] = styles[style]; - } - } - }, - - isDefault: function isDefault() { - return this.foreground === "white" && !this.underline && !this.italics && this.background === "black" && !this.flash; - }, - - equals: function equals(other) { - return this.foreground === other.foreground && this.underline === other.underline && this.italics === other.italics && this.background === other.background && this.flash === other.flash; - }, - - copy: function copy(newPenState) { - this.foreground = newPenState.foreground; - this.underline = newPenState.underline; - this.italics = newPenState.italics; - this.background = newPenState.background; - this.flash = newPenState.flash; - }, - - toString: function toString() { - return "color=" + this.foreground + ", underline=" + this.underline + ", italics=" + this.italics + ", background=" + this.background + ", flash=" + this.flash; - } - }; - - /** - * Unicode character with styling and background. - * @constructor - */ - var StyledUnicodeChar = function StyledUnicodeChar(uchar, foreground, underline, italics, background, flash) { - this.uchar = uchar || ' '; // unicode character - this.penState = new PenState(foreground, underline, italics, background, flash); - }; - - StyledUnicodeChar.prototype = { - - reset: function reset() { - this.uchar = ' '; - this.penState.reset(); - }, - - setChar: function setChar(uchar, newPenState) { - this.uchar = uchar; - this.penState.copy(newPenState); - }, - - setPenState: function setPenState(newPenState) { - this.penState.copy(newPenState); - }, - - equals: function equals(other) { - return this.uchar === other.uchar && this.penState.equals(other.penState); - }, - - copy: function copy(newChar) { - this.uchar = newChar.uchar; - this.penState.copy(newChar.penState); - }, - - isEmpty: function isEmpty() { - return this.uchar === ' ' && this.penState.isDefault(); - } - }; - - /** - * CEA-608 row consisting of NR_COLS instances of StyledUnicodeChar. - * @constructor - */ - var Row = function Row() { - this.chars = []; - for (var i = 0; i < NR_COLS; i++) { - this.chars.push(new StyledUnicodeChar()); - } - this.pos = 0; - this.currPenState = new PenState(); - }; - - Row.prototype = { - - equals: function equals(other) { - var equal = true; - for (var i = 0; i < NR_COLS; i++) { - if (!this.chars[i].equals(other.chars[i])) { - equal = false; - break; - } - } - return equal; - }, - - copy: function copy(other) { - for (var i = 0; i < NR_COLS; i++) { - this.chars[i].copy(other.chars[i]); - } - }, - - isEmpty: function isEmpty() { - var empty = true; - for (var i = 0; i < NR_COLS; i++) { - if (!this.chars[i].isEmpty()) { - empty = false; - break; - } - } - return empty; - }, - - /** - * Set the cursor to a valid column. - */ - setCursor: function setCursor(absPos) { - if (this.pos !== absPos) { - this.pos = absPos; - } - if (this.pos < 0) { - logger.log("ERROR", "Negative cursor position " + this.pos); - this.pos = 0; - } else if (this.pos > NR_COLS) { - logger.log("ERROR", "Too large cursor position " + this.pos); - this.pos = NR_COLS; - } - }, - - /** - * Move the cursor relative to current position. - */ - moveCursor: function moveCursor(relPos) { - var newPos = this.pos + relPos; - if (relPos > 1) { - for (var i = this.pos + 1; i < newPos + 1; i++) { - this.chars[i].setPenState(this.currPenState); - } - } - this.setCursor(newPos); - }, - - /** - * Backspace, move one step back and clear character. - */ - backSpace: function backSpace() { - this.moveCursor(-1); - this.chars[this.pos].setChar(' ', this.currPenState); - }, - - insertChar: function insertChar(byte) { - if (byte >= 0x90) { - //Extended char - this.backSpace(); - } - var char = getCharForByte(byte); - if (this.pos >= NR_COLS) { - logger.log("ERROR", "Cannot insert " + byte.toString(16) + " (" + char + ") at position " + this.pos + ". Skipping it!"); - return; - } - this.chars[this.pos].setChar(char, this.currPenState); - this.moveCursor(1); - }, - - clearFromPos: function clearFromPos(startPos) { - var i; - for (i = startPos; i < NR_COLS; i++) { - this.chars[i].reset(); - } - }, - - clear: function clear() { - this.clearFromPos(0); - this.pos = 0; - this.currPenState.reset(); - }, - - clearToEndOfRow: function clearToEndOfRow() { - this.clearFromPos(this.pos); - }, - - getTextString: function getTextString() { - var chars = []; - var empty = true; - for (var i = 0; i < NR_COLS; i++) { - var char = this.chars[i].uchar; - if (char !== " ") { - empty = false; - } - chars.push(char); - } - if (empty) { - return ""; - } else { - return chars.join(""); - } - }, - - setPenStyles: function setPenStyles(styles) { - this.currPenState.setStyles(styles); - var currChar = this.chars[this.pos]; - currChar.setPenState(this.currPenState); - } - }; - - /** - * Keep a CEA-608 screen of 32x15 styled characters - * @constructor - */ - var CaptionScreen = function CaptionScreen() { - - this.rows = []; - for (var i = 0; i < NR_ROWS; i++) { - this.rows.push(new Row()); // Note that we use zero-based numbering (0-14) - } - this.currRow = NR_ROWS - 1; - this.nrRollUpRows = null; - this.reset(); - }; - - CaptionScreen.prototype = { - - reset: function reset() { - for (var i = 0; i < NR_ROWS; i++) { - this.rows[i].clear(); - } - this.currRow = NR_ROWS - 1; - }, - - equals: function equals(other) { - var equal = true; - for (var i = 0; i < NR_ROWS; i++) { - if (!this.rows[i].equals(other.rows[i])) { - equal = false; - break; - } - } - return equal; - }, - - copy: function copy(other) { - for (var i = 0; i < NR_ROWS; i++) { - this.rows[i].copy(other.rows[i]); - } - }, - - isEmpty: function isEmpty() { - var empty = true; - for (var i = 0; i < NR_ROWS; i++) { - if (!this.rows[i].isEmpty()) { - empty = false; - break; - } - } - return empty; - }, - - backSpace: function backSpace() { - var row = this.rows[this.currRow]; - row.backSpace(); - }, - - clearToEndOfRow: function clearToEndOfRow() { - var row = this.rows[this.currRow]; - row.clearToEndOfRow(); - }, - - /** - * Insert a character (without styling) in the current row. - */ - insertChar: function insertChar(char) { - var row = this.rows[this.currRow]; - row.insertChar(char); - }, - - setPen: function setPen(styles) { - var row = this.rows[this.currRow]; - row.setPenStyles(styles); - }, - - moveCursor: function moveCursor(relPos) { - var row = this.rows[this.currRow]; - row.moveCursor(relPos); - }, - - setCursor: function setCursor(absPos) { - logger.log("INFO", "setCursor: " + absPos); - var row = this.rows[this.currRow]; - row.setCursor(absPos); - }, - - setPAC: function setPAC(pacData) { - logger.log("INFO", "pacData = " + JSON.stringify(pacData)); - var newRow = pacData.row - 1; - if (this.nrRollUpRows && newRow < this.nrRollUpRows - 1) { - newRow = this.nrRollUpRows - 1; - } - this.currRow = newRow; - var row = this.rows[this.currRow]; - if (pacData.indent !== null) { - var indent = pacData.indent; - var prevPos = Math.max(indent - 1, 0); - row.setCursor(pacData.indent); - pacData.color = row.chars[prevPos].penState.foreground; - } - var styles = { foreground: pacData.color, underline: pacData.underline, italics: pacData.italics, background: 'black', flash: false }; - this.setPen(styles); - }, - - /** - * Set background/extra foreground, but first do back_space, and then insert space (backwards compatibility). - */ - setBkgData: function setBkgData(bkgData) { - - logger.log("INFO", "bkgData = " + JSON.stringify(bkgData)); - this.backSpace(); - this.setPen(bkgData); - this.insertChar(0x20); //Space - }, - - setRollUpRows: function setRollUpRows(nrRows) { - this.nrRollUpRows = nrRows; - }, - - rollUp: function rollUp() { - if (this.nrRollUpRows === null) { - logger.log("DEBUG", "roll_up but nrRollUpRows not set yet"); - return; //Not properly setup - } - logger.log("TEXT", this.getDisplayText()); - var topRowIndex = this.currRow + 1 - this.nrRollUpRows; - var topRow = this.rows.splice(topRowIndex, 1)[0]; - topRow.clear(); - this.rows.splice(this.currRow, 0, topRow); - logger.log("INFO", "Rolling up"); - //logger.log("TEXT", this.get_display_text()) - }, - - /** - * Get all non-empty rows with as unicode text. - */ - getDisplayText: function getDisplayText(asOneRow) { - asOneRow = asOneRow || false; - var displayText = []; - var text = ""; - var rowNr = -1; - for (var i = 0; i < NR_ROWS; i++) { - var rowText = this.rows[i].getTextString(); - if (rowText) { - rowNr = i + 1; - if (asOneRow) { - displayText.push("Row " + rowNr + ': "' + rowText + '"'); - } else { - displayText.push(rowText.trim()); - } - } - } - if (displayText.length > 0) { - if (asOneRow) { - text = "[" + displayText.join(" | ") + "]"; - } else { - text = displayText.join("\n"); - } - } - return text; - }, - - getTextAndFormat: function getTextAndFormat() { - return this.rows; - } - }; - - /** - * Handle a CEA-608 channel and send decoded data to outputFilter - * @constructor - * @param {Number} channelNumber (1 or 2) - * @param {CueHandler} outputFilter Output from channel1 newCue(startTime, endTime, captionScreen) - */ - var Cea608Channel = function Cea608Channel(channelNumber, outputFilter) { - - this.chNr = channelNumber; - this.outputFilter = outputFilter; - this.mode = null; - this.verbose = 0; - this.displayedMemory = new CaptionScreen(); - this.nonDisplayedMemory = new CaptionScreen(); - this.lastOutputScreen = new CaptionScreen(); - this.currRollUpRow = this.displayedMemory.rows[NR_ROWS - 1]; - this.writeScreen = this.displayedMemory; - this.mode = null; - this.cueStartTime = null; // Keeps track of where a cue started. - }; - - Cea608Channel.prototype = { - - modes: ["MODE_ROLL-UP", "MODE_POP-ON", "MODE_PAINT-ON", "MODE_TEXT"], - - reset: function reset() { - this.mode = null; - this.displayedMemory.reset(); - this.nonDisplayedMemory.reset(); - this.lastOutputScreen.reset(); - this.currRollUpRow = this.displayedMemory.rows[NR_ROWS - 1]; - this.writeScreen = this.displayedMemory; - this.mode = null; - this.cueStartTime = null; - this.lastCueEndTime = null; - }, - - getHandler: function getHandler() { - return this.outputFilter; - }, - - setHandler: function setHandler(newHandler) { - this.outputFilter = newHandler; - }, - - setPAC: function setPAC(pacData) { - this.writeScreen.setPAC(pacData); - }, - - setBkgData: function setBkgData(bkgData) { - this.writeScreen.setBkgData(bkgData); - }, - - setMode: function setMode(newMode) { - if (newMode === this.mode) { - return; - } - this.mode = newMode; - logger.log("INFO", "MODE=" + newMode); - if (this.mode == "MODE_POP-ON") { - this.writeScreen = this.nonDisplayedMemory; - } else { - this.writeScreen = this.displayedMemory; - this.writeScreen.reset(); - } - if (this.mode !== "MODE_ROLL-UP") { - this.displayedMemory.nrRollUpRows = null; - this.nonDisplayedMemory.nrRollUpRows = null; - } - this.mode = newMode; - }, - - insertChars: function insertChars(chars) { - for (var i = 0; i < chars.length; i++) { - this.writeScreen.insertChar(chars[i]); - } - var screen = this.writeScreen === this.displayedMemory ? "DISP" : "NON_DISP"; - logger.log("INFO", screen + ": " + this.writeScreen.getDisplayText(true)); - if (this.mode === "MODE_PAINT-ON" || this.mode === "MODE_ROLL-UP") { - logger.log("TEXT", "DISPLAYED: " + this.displayedMemory.getDisplayText(true)); - this.outputDataUpdate(); - } - }, - - cc_RCL: function cc_RCL() { - // Resume Caption Loading (switch mode to Pop On) - logger.log("INFO", "RCL - Resume Caption Loading"); - this.setMode("MODE_POP-ON"); - }, - cc_BS: function cc_BS() { - // BackSpace - logger.log("INFO", "BS - BackSpace"); - if (this.mode === "MODE_TEXT") { - return; - } - this.writeScreen.backSpace(); - if (this.writeScreen === this.displayedMemory) { - this.outputDataUpdate(); - } - }, - cc_AOF: function cc_AOF() { - // Reserved (formerly Alarm Off) - return; - }, - cc_AON: function cc_AON() { - // Reserved (formerly Alarm On) - return; - }, - cc_DER: function cc_DER() { - // Delete to End of Row - logger.log("INFO", "DER- Delete to End of Row"); - this.writeScreen.clearToEndOfRow(); - this.outputDataUpdate(); - }, - cc_RU: function cc_RU(nrRows) { - //Roll-Up Captions-2,3,or 4 Rows - logger.log("INFO", "RU(" + nrRows + ") - Roll Up"); - this.writeScreen = this.displayedMemory; - this.setMode("MODE_ROLL-UP"); - this.writeScreen.setRollUpRows(nrRows); - }, - cc_FON: function cc_FON() { - //Flash On - logger.log("INFO", "FON - Flash On"); - this.writeScreen.setPen({ flash: true }); - }, - cc_RDC: function cc_RDC() { - // Resume Direct Captioning (switch mode to PaintOn) - logger.log("INFO", "RDC - Resume Direct Captioning"); - this.setMode("MODE_PAINT-ON"); - }, - cc_TR: function cc_TR() { - // Text Restart in text mode (not supported, however) - logger.log("INFO", "TR"); - this.setMode("MODE_TEXT"); - }, - cc_RTD: function cc_RTD() { - // Resume Text Display in Text mode (not supported, however) - logger.log("INFO", "RTD"); - this.setMode("MODE_TEXT"); - }, - cc_EDM: function cc_EDM() { - // Erase Displayed Memory - logger.log("INFO", "EDM - Erase Displayed Memory"); - this.displayedMemory.reset(); - this.outputDataUpdate(); - }, - cc_CR: function cc_CR() { - // Carriage Return - logger.log("CR - Carriage Return"); - this.writeScreen.rollUp(); - this.outputDataUpdate(); - }, - cc_ENM: function cc_ENM() { - //Erase Non-Displayed Memory - logger.log("INFO", "ENM - Erase Non-displayed Memory"); - this.nonDisplayedMemory.reset(); - }, - cc_EOC: function cc_EOC() { - //End of Caption (Flip Memories) - logger.log("INFO", "EOC - End Of Caption"); - if (this.mode === "MODE_POP-ON") { - var tmp = this.displayedMemory; - this.displayedMemory = this.nonDisplayedMemory; - this.nonDisplayedMemory = tmp; - this.writeScreen = this.nonDisplayedMemory; - logger.log("TEXT", "DISP: " + this.displayedMemory.getDisplayText()); - } - this.outputDataUpdate(); - }, - cc_TO: function cc_TO(nrCols) { - // Tab Offset 1,2, or 3 columns - logger.log("INFO", "TO(" + nrCols + ") - Tab Offset"); - this.writeScreen.moveCursor(nrCols); - }, - cc_MIDROW: function cc_MIDROW(secondByte) { - // Parse MIDROW command - var styles = { flash: false }; - styles.underline = secondByte % 2 === 1; - styles.italics = secondByte >= 0x2e; - if (!styles.italics) { - var colorIndex = Math.floor(secondByte / 2) - 0x10; - var colors = ["white", "green", "blue", "cyan", "red", "yellow", "magenta"]; - styles.foreground = colors[colorIndex]; - } else { - styles.foreground = "white"; - } - logger.log("INFO", "MIDROW: " + JSON.stringify(styles)); - this.writeScreen.setPen(styles); - }, - - outputDataUpdate: function outputDataUpdate() { - var t = logger.time; - if (t === null) { - return; - } - if (this.outputFilter) { - if (this.outputFilter.updateData) { - this.outputFilter.updateData(t, this.displayedMemory); - } - if (this.cueStartTime === null && !this.displayedMemory.isEmpty()) { - // Start of a new cue - this.cueStartTime = t; - } else { - if (!this.displayedMemory.equals(this.lastOutputScreen)) { - if (this.outputFilter.newCue) { - this.outputFilter.newCue(this.cueStartTime, t, this.lastOutputScreen); - } - this.cueStartTime = this.displayedMemory.isEmpty() ? null : t; - } - } - this.lastOutputScreen.copy(this.displayedMemory); - } - }, - - cueSplitAtTime: function cueSplitAtTime(t) { - if (this.outputFilter) { - if (!this.displayedMemory.isEmpty()) { - if (this.outputFilter.newCue) { - this.outputFilter.newCue(this.cueStartTime, t, this.displayedMemory); - } - this.cueStartTime = t; - } - } - } - }; - - /** - * Parse CEA-608 data and send decoded data to out1 and out2. - * @constructor - * @param {Number} field CEA-608 field (1 or 2) - * @param {CueHandler} out1 Output from channel1 newCue(startTime, endTime, captionScreen) - * @param {CueHandler} out2 Output from channel2 newCue(startTime, endTime, captionScreen) - */ - var Cea608Parser = function Cea608Parser(field, out1, out2) { - this.field = field || 1; - this.outputs = [out1, out2]; - this.channels = [new Cea608Channel(1, out1), new Cea608Channel(2, out2)]; - this.currChNr = -1; // Will be 1 or 2 - this.lastCmdA = null; // First byte of last command - this.lastCmdB = null; // Second byte of last command - this.bufferedData = []; - this.startTime = null; - this.lastTime = null; - this.dataCounters = { 'padding': 0, 'char': 0, 'cmd': 0, 'other': 0 }; - }; - - Cea608Parser.prototype = { - - getHandler: function getHandler(index) { - return this.channels[index].getHandler(); - }, - - setHandler: function setHandler(index, newHandler) { - this.channels[index].setHandler(newHandler); - }, - - /** - * Add data for time t in forms of list of bytes (unsigned ints). The bytes are treated as pairs. - */ - addData: function addData(t, byteList) { - var cmdFound, - a, - b, - charsFound = false; - - this.lastTime = t; - logger.setTime(t); - - for (var i = 0; i < byteList.length; i += 2) { - a = byteList[i] & 0x7f; - b = byteList[i + 1] & 0x7f; - - if (a >= 0x10 && a <= 0x1f && a === this.lastCmdA && b === this.lastCmdB) { - this.lastCmdA = null; - this.lastCmdB = null; - logger.log("DEBUG", "Repeated command (" + numArrayToHexArray([a, b]) + ") is dropped"); - continue; // Repeated commands are dropped (once) - } - - if (a === 0 && b === 0) { - this.dataCounters.padding += 2; - continue; - } else { - logger.log("DATA", "[" + numArrayToHexArray([byteList[i], byteList[i + 1]]) + "] -> (" + numArrayToHexArray([a, b]) + ")"); - } - cmdFound = this.parseCmd(a, b); - if (!cmdFound) { - cmdFound = this.parseMidrow(a, b); - } - if (!cmdFound) { - cmdFound = this.parsePAC(a, b); - } - if (!cmdFound) { - cmdFound = this.parseBackgroundAttributes(a, b); - } - if (!cmdFound) { - charsFound = this.parseChars(a, b); - if (charsFound) { - if (this.currChNr && this.currChNr >= 0) { - var channel = this.channels[this.currChNr - 1]; - channel.insertChars(charsFound); - } else { - logger.log("WARNING", "No channel found yet. TEXT-MODE?"); - } - } - } - if (cmdFound) { - this.dataCounters.cmd += 2; - } else if (charsFound) { - this.dataCounters.char += 2; - } else { - this.dataCounters.other += 2; - logger.log("WARNING", "Couldn't parse cleaned data " + numArrayToHexArray([a, b]) + " orig: " + numArrayToHexArray([byteList[i], byteList[i + 1]])); - } - } - }, - - /** - * Parse Command. - * @returns {Boolean} Tells if a command was found - */ - parseCmd: function parseCmd(a, b) { - var chNr = null; - - var cond1 = (a === 0x14 || a === 0x15 || a === 0x1C || a === 0x1D) && 0x20 <= b && b <= 0x2F; - var cond2 = (a === 0x17 || a === 0x1F) && 0x21 <= b && b <= 0x23; - if (!(cond1 || cond2)) { - return false; - } - - if (a === 0x14 || a === 0x15 || a === 0x17) { - chNr = 1; - } else { - chNr = 2; // (a === 0x1C || a === 0x1D || a=== 0x1f) - } - - var channel = this.channels[chNr - 1]; - - if (a === 0x14 || a === 0x15 || a === 0x1C || a === 0x1D) { - if (b === 0x20) { - channel.cc_RCL(); - } else if (b === 0x21) { - channel.cc_BS(); - } else if (b === 0x22) { - channel.cc_AOF(); - } else if (b === 0x23) { - channel.cc_AON(); - } else if (b === 0x24) { - channel.cc_DER(); - } else if (b === 0x25) { - channel.cc_RU(2); - } else if (b === 0x26) { - channel.cc_RU(3); - } else if (b === 0x27) { - channel.cc_RU(4); - } else if (b === 0x28) { - channel.cc_FON(); - } else if (b === 0x29) { - channel.cc_RDC(); - } else if (b === 0x2A) { - channel.cc_TR(); - } else if (b === 0x2B) { - channel.cc_RTD(); - } else if (b === 0x2C) { - channel.cc_EDM(); - } else if (b === 0x2D) { - channel.cc_CR(); - } else if (b === 0x2E) { - channel.cc_ENM(); - } else if (b === 0x2F) { - channel.cc_EOC(); - } - } else { - //a == 0x17 || a == 0x1F - channel.cc_TO(b - 0x20); - } - this.lastCmdA = a; - this.lastCmdB = b; - this.currChNr = chNr; - return true; - }, - - /** - * Parse midrow styling command - * @returns {Boolean} - */ - parseMidrow: function parseMidrow(a, b) { - var chNr = null; - - if ((a === 0x11 || a === 0x19) && 0x20 <= b && b <= 0x2f) { - if (a === 0x11) { - chNr = 1; - } else { - chNr = 2; - } - if (chNr !== this.currChNr) { - logger.log("ERROR", "Mismatch channel in midrow parsing"); - return false; - } - var channel = this.channels[chNr - 1]; - // cea608 spec says midrow codes should inject a space - channel.insertChars([0x20]); - channel.cc_MIDROW(b); - logger.log("DEBUG", "MIDROW (" + numArrayToHexArray([a, b]) + ")"); - this.lastCmdA = a; - this.lastCmdB = b; - return true; - } - return false; - }, - /** - * Parse Preable Access Codes (Table 53). - * @returns {Boolean} Tells if PAC found - */ - parsePAC: function parsePAC(a, b) { - - var chNr = null; - var row = null; - - var case1 = (0x11 <= a && a <= 0x17 || 0x19 <= a && a <= 0x1F) && 0x40 <= b && b <= 0x7F; - var case2 = (a === 0x10 || a === 0x18) && 0x40 <= b && b <= 0x5F; - if (!(case1 || case2)) { - return false; - } - - chNr = a <= 0x17 ? 1 : 2; - - if (0x40 <= b && b <= 0x5F) { - row = chNr === 1 ? rowsLowCh1[a] : rowsLowCh2[a]; - } else { - // 0x60 <= b <= 0x7F - row = chNr === 1 ? rowsHighCh1[a] : rowsHighCh2[a]; - } - var pacData = this.interpretPAC(row, b); - var channel = this.channels[chNr - 1]; - channel.setPAC(pacData); - this.lastCmdA = a; - this.lastCmdB = b; - this.currChNr = chNr; - return true; - }, - - /** - * Interpret the second byte of the pac, and return the information. - * @returns {Object} pacData with style parameters. - */ - interpretPAC: function interpretPAC(row, byte) { - var pacIndex = byte; - var pacData = { color: null, italics: false, indent: null, underline: false, row: row }; - - if (byte > 0x5F) { - pacIndex = byte - 0x60; - } else { - pacIndex = byte - 0x40; - } - pacData.underline = (pacIndex & 1) === 1; - if (pacIndex <= 0xd) { - pacData.color = ['white', 'green', 'blue', 'cyan', 'red', 'yellow', 'magenta', 'white'][Math.floor(pacIndex / 2)]; - } else if (pacIndex <= 0xf) { - pacData.italics = true; - pacData.color = 'white'; - } else { - pacData.indent = Math.floor((pacIndex - 0x10) / 2) * 4; - } - return pacData; // Note that row has zero offset. The spec uses 1. - }, - - /** - * Parse characters. - * @returns An array with 1 to 2 codes corresponding to chars, if found. null otherwise. - */ - parseChars: function parseChars(a, b) { - - var channelNr = null, - charCodes = null, - charCode1 = null, - charCode2 = null; - - if (a >= 0x19) { - channelNr = 2; - charCode1 = a - 8; - } else { - channelNr = 1; - charCode1 = a; - } - if (0x11 <= charCode1 && charCode1 <= 0x13) { - // Special character - var oneCode = b; - if (charCode1 === 0x11) { - oneCode = b + 0x50; - } else if (charCode1 === 0x12) { - oneCode = b + 0x70; - } else { - oneCode = b + 0x90; - } - logger.log("INFO", "Special char '" + getCharForByte(oneCode) + "' in channel " + channelNr); - charCodes = [oneCode]; - this.lastCmdA = a; - this.lastCmdB = b; - } else if (0x20 <= a && a <= 0x7f) { - charCodes = b === 0 ? [a] : [a, b]; - this.lastCmdA = null; - this.lastCmdB = null; - } - if (charCodes) { - var hexCodes = numArrayToHexArray(charCodes); - logger.log("DEBUG", "Char codes = " + hexCodes.join(",")); - } - return charCodes; - }, - - /** - * Parse extended background attributes as well as new foreground color black. - * @returns{Boolean} Tells if background attributes are found - */ - parseBackgroundAttributes: function parseBackgroundAttributes(a, b) { - var bkgData, index, chNr, channel; - - var case1 = (a === 0x10 || a === 0x18) && 0x20 <= b && b <= 0x2f; - var case2 = (a === 0x17 || a === 0x1f) && 0x2d <= b && b <= 0x2f; - if (!(case1 || case2)) { - return false; - } - bkgData = {}; - if (a === 0x10 || a === 0x18) { - index = Math.floor((b - 0x20) / 2); - bkgData.background = backgroundColors[index]; - if (b % 2 === 1) { - bkgData.background = bkgData.background + "_semi"; - } - } else if (b === 0x2d) { - bkgData.background = "transparent"; - } else { - bkgData.foreground = "black"; - if (b === 0x2f) { - bkgData.underline = true; - } - } - chNr = a < 0x18 ? 1 : 2; - channel = this.channels[chNr - 1]; - channel.setBkgData(bkgData); - this.lastCmdA = a; - this.lastCmdB = b; - return true; - }, - - /** - * Reset state of parser and its channels. - */ - reset: function reset() { - for (var i = 0; i < this.channels.length; i++) { - if (this.channels[i]) { - this.channels[i].reset(); - } - } - this.lastCmdA = null; - this.lastCmdB = null; - }, - - /** - * Trigger the generation of a cue, and the start of a new one if displayScreens are not empty. - */ - cueSplitAtTime: function cueSplitAtTime(t) { - for (var i = 0; i < this.channels.length; i++) { - if (this.channels[i]) { - this.channels[i].cueSplitAtTime(t); - } - } - } - }; - - /** - * Find ranges corresponding to SEA CEA-608 NALUS in sizeprepended NALU array. - * @param {raw} dataView of binary data - * @param {startPos} start position in raw - * @param {size} total size of data in raw to consider - * @returns - */ - var findCea608Nalus = function findCea608Nalus(raw, startPos, size) { - var nalSize = 0, - cursor = startPos, - nalType = 0, - cea608NaluRanges = [], - - // Check SEI data according to ANSI-SCTE 128 - isCEA608SEI = function isCEA608SEI(payloadType, payloadSize, raw, pos) { - if (payloadType !== 4 || payloadSize < 8) { - return null; - } - var countryCode = raw.getUint8(pos); - var providerCode = raw.getUint16(pos + 1); - var userIdentifier = raw.getUint32(pos + 3); - var userDataTypeCode = raw.getUint8(pos + 7); - return countryCode == 0xB5 && providerCode == 0x31 && userIdentifier == 0x47413934 && userDataTypeCode == 0x3; - }; - while (cursor < startPos + size) { - nalSize = raw.getUint32(cursor); - nalType = raw.getUint8(cursor + 4) & 0x1F; - //console.log(time + " NAL " + nalType); - if (nalType === 6) { - // SEI NAL Unit. The NAL header is the first byte - //console.log("SEI NALU of size " + nalSize + " at time " + time); - var pos = cursor + 5; - var payloadType = -1; - while (pos < cursor + 4 + nalSize - 1) { - // The last byte should be rbsp_trailing_bits - payloadType = 0; - var b = 0xFF; - while (b === 0xFF) { - b = raw.getUint8(pos); - payloadType += b; - pos++; - } - var payloadSize = 0; - b = 0xFF; - while (b === 0xFF) { - b = raw.getUint8(pos); - payloadSize += b; - pos++; - } - if (isCEA608SEI(payloadType, payloadSize, raw, pos)) { - //console.log("CEA608 SEI " + time + " " + payloadSize); - cea608NaluRanges.push([pos, payloadSize]); - } - pos += payloadSize; - } - } - cursor += nalSize + 4; - } - return cea608NaluRanges; - }; - - var extractCea608DataFromRange = function extractCea608DataFromRange(raw, cea608Range) { - var pos = cea608Range[0]; - var fieldData = [[], []]; - - pos += 8; // Skip the identifier up to userDataTypeCode - var ccCount = raw.getUint8(pos) & 0x1f; - pos += 2; // Advance 1 and skip reserved byte - - for (var i = 0; i < ccCount; i++) { - var byte = raw.getUint8(pos); - var ccValid = byte & 0x4; - var ccType = byte & 0x3; - pos++; - var ccData1 = raw.getUint8(pos); // Keep parity bit - pos++; - var ccData2 = raw.getUint8(pos); // Keep parity bit - pos++; - if (ccValid && (ccData1 & 0x7f) + (ccData2 & 0x7f) !== 0) { - //Check validity and non-empty data - if (ccType === 0) { - fieldData[0].push(ccData1); - fieldData[0].push(ccData2); - } else if (ccType === 1) { - fieldData[1].push(ccData1); - fieldData[1].push(ccData2); - } - } - } - return fieldData; - }; - - exports.logger = logger; - exports.PenState = PenState; - exports.CaptionScreen = CaptionScreen; - exports.Cea608Parser = Cea608Parser; - exports.findCea608Nalus = findCea608Nalus; - exports.extractCea608DataFromRange = extractCea608DataFromRange; -})(typeof exports === 'undefined' ? undefined.cea608parser = {} : exports); - -},{}],3:[function(_dereq_,module,exports){ -/* - Copyright 2011-2013 Abdulla Abdurakhmanov - Original sources are available at https://code.google.com/p/x2js/ - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -/* - Further modified for dashjs to: - - keep track of children nodes in order in attribute __children. - - add type conversion matchers - - re-add ignoreRoot - - allow zero-length attributePrefix - - don't add white-space text nodes - - remove explicit RequireJS support -*/ - -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -function X2JS(config) { - 'use strict'; - - var VERSION = "1.2.0"; - - config = config || {}; - initConfigDefaults(); - initRequiredPolyfills(); - - function initConfigDefaults() { - if (config.escapeMode === undefined) { - config.escapeMode = true; - } - - if (config.attributePrefix === undefined) { - config.attributePrefix = "_"; - } - - config.arrayAccessForm = config.arrayAccessForm || "none"; - config.emptyNodeForm = config.emptyNodeForm || "text"; - - if (config.enableToStringFunc === undefined) { - config.enableToStringFunc = true; - } - config.arrayAccessFormPaths = config.arrayAccessFormPaths || []; - if (config.skipEmptyTextNodesForObj === undefined) { - config.skipEmptyTextNodesForObj = true; - } - if (config.stripWhitespaces === undefined) { - config.stripWhitespaces = true; - } - config.datetimeAccessFormPaths = config.datetimeAccessFormPaths || []; - - if (config.useDoubleQuotes === undefined) { - config.useDoubleQuotes = false; - } - - config.xmlElementsFilter = config.xmlElementsFilter || []; - config.jsonPropertiesFilter = config.jsonPropertiesFilter || []; - - if (config.keepCData === undefined) { - config.keepCData = false; - } - - if (config.ignoreRoot === undefined) { - config.ignoreRoot = false; - } - } - - var DOMNodeTypes = { - ELEMENT_NODE: 1, - TEXT_NODE: 3, - CDATA_SECTION_NODE: 4, - COMMENT_NODE: 8, - DOCUMENT_NODE: 9 - }; - - function initRequiredPolyfills() {} - - function getNodeLocalName(node) { - var nodeLocalName = node.localName; - if (nodeLocalName == null) // Yeah, this is IE!! - nodeLocalName = node.baseName; - if (nodeLocalName == null || nodeLocalName == "") // =="" is IE too - nodeLocalName = node.nodeName; - return nodeLocalName; - } - - function getNodePrefix(node) { - return node.prefix; - } - - function escapeXmlChars(str) { - if (typeof str == "string") return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''');else return str; - } - - function unescapeXmlChars(str) { - return str.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, '&'); - } - - function checkInStdFiltersArrayForm(stdFiltersArrayForm, obj, name, path) { - var idx = 0; - for (; idx < stdFiltersArrayForm.length; idx++) { - var filterPath = stdFiltersArrayForm[idx]; - if (typeof filterPath === "string") { - if (filterPath == path) break; - } else if (filterPath instanceof RegExp) { - if (filterPath.test(path)) break; - } else if (typeof filterPath === "function") { - if (filterPath(obj, name, path)) break; - } - } - return idx != stdFiltersArrayForm.length; - } - - function toArrayAccessForm(obj, childName, path) { - switch (config.arrayAccessForm) { - case "property": - if (!(obj[childName] instanceof Array)) obj[childName + "_asArray"] = [obj[childName]];else obj[childName + "_asArray"] = obj[childName]; - break; - /*case "none": - break;*/ - } - - if (!(obj[childName] instanceof Array) && config.arrayAccessFormPaths.length > 0) { - if (checkInStdFiltersArrayForm(config.arrayAccessFormPaths, obj, childName, path)) { - obj[childName] = [obj[childName]]; - } - } - } - - function fromXmlDateTime(prop) { - // Implementation based up on http://stackoverflow.com/questions/8178598/xml-datetime-to-javascript-date-object - // Improved to support full spec and optional parts - var bits = prop.split(/[-T:+Z]/g); - - var d = new Date(bits[0], bits[1] - 1, bits[2]); - var secondBits = bits[5].split("\."); - d.setHours(bits[3], bits[4], secondBits[0]); - if (secondBits.length > 1) d.setMilliseconds(secondBits[1]); - - // Get supplied time zone offset in minutes - if (bits[6] && bits[7]) { - var offsetMinutes = bits[6] * 60 + Number(bits[7]); - var sign = /\d\d-\d\d:\d\d$/.test(prop) ? '-' : '+'; - - // Apply the sign - offsetMinutes = 0 + (sign == '-' ? -1 * offsetMinutes : offsetMinutes); - - // Apply offset and local timezone - d.setMinutes(d.getMinutes() - offsetMinutes - d.getTimezoneOffset()); - } else if (prop.indexOf("Z", prop.length - 1) !== -1) { - d = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds())); - } - - // d is now a local time equivalent to the supplied time - return d; - } - - function checkFromXmlDateTimePaths(value, childName, fullPath) { - if (config.datetimeAccessFormPaths.length > 0) { - var path = fullPath.split("\.#")[0]; - if (checkInStdFiltersArrayForm(config.datetimeAccessFormPaths, value, childName, path)) { - return fromXmlDateTime(value); - } else return value; - } else return value; - } - - function checkXmlElementsFilter(obj, childType, childName, childPath) { - if (childType == DOMNodeTypes.ELEMENT_NODE && config.xmlElementsFilter.length > 0) { - return checkInStdFiltersArrayForm(config.xmlElementsFilter, obj, childName, childPath); - } else return true; - } - - function parseDOMChildren(node, path) { - if (node.nodeType == DOMNodeTypes.DOCUMENT_NODE) { - var result = new Object(); - var nodeChildren = node.childNodes; - // Alternative for firstElementChild which is not supported in some environments - for (var cidx = 0; cidx < nodeChildren.length; cidx++) { - var child = nodeChildren[cidx]; - if (child.nodeType == DOMNodeTypes.ELEMENT_NODE) { - if (config.ignoreRoot) { - result = parseDOMChildren(child); - } else { - result = {}; - var childName = getNodeLocalName(child); - result[childName] = parseDOMChildren(child); - } - } - } - return result; - } else if (node.nodeType == DOMNodeTypes.ELEMENT_NODE) { - var result = new Object(); - result.__cnt = 0; - - var children = []; - var nodeChildren = node.childNodes; - - // Children nodes - for (var cidx = 0; cidx < nodeChildren.length; cidx++) { - var child = nodeChildren[cidx]; - var childName = getNodeLocalName(child); - - if (child.nodeType != DOMNodeTypes.COMMENT_NODE) { - var childPath = path + "." + childName; - if (checkXmlElementsFilter(result, child.nodeType, childName, childPath)) { - result.__cnt++; - if (result[childName] == null) { - var c = parseDOMChildren(child, childPath); - if (childName != "#text" || /[^\s]/.test(c)) { - var o = {}; - o[childName] = c; - children.push(o); - } - result[childName] = c; - toArrayAccessForm(result, childName, childPath); - } else { - if (result[childName] != null) { - if (!(result[childName] instanceof Array)) { - result[childName] = [result[childName]]; - toArrayAccessForm(result, childName, childPath); - } - } - - var c = parseDOMChildren(child, childPath); - if (childName != "#text" || /[^\s]/.test(c)) { - // Don't add white-space text nodes - var o = {}; - o[childName] = c; - children.push(o); - } - result[childName][result[childName].length] = c; - } - } - } - } - - result.__children = children; - - // Attributes - var nodeLocalName = getNodeLocalName(node); - for (var aidx = 0; aidx < node.attributes.length; aidx++) { - var attr = node.attributes[aidx]; - result.__cnt++; - - var value2 = attr.value; - for (var m = 0, ml = config.matchers.length; m < ml; m++) { - var matchobj = config.matchers[m]; - if (matchobj.test(attr, nodeLocalName)) value2 = matchobj.converter(attr.value); - } - - result[config.attributePrefix + attr.name] = value2; - } - - // Node namespace prefix - var nodePrefix = getNodePrefix(node); - if (nodePrefix != null && nodePrefix != "") { - result.__cnt++; - result.__prefix = nodePrefix; - } - - if (result["#text"] != null) { - result.__text = result["#text"]; - if (result.__text instanceof Array) { - result.__text = result.__text.join("\n"); - } - //if(config.escapeMode) - // result.__text = unescapeXmlChars(result.__text); - if (config.stripWhitespaces) result.__text = result.__text.trim(); - delete result["#text"]; - if (config.arrayAccessForm == "property") delete result["#text_asArray"]; - result.__text = checkFromXmlDateTimePaths(result.__text, childName, path + "." + childName); - } - if (result["#cdata-section"] != null) { - result.__cdata = result["#cdata-section"]; - delete result["#cdata-section"]; - if (config.arrayAccessForm == "property") delete result["#cdata-section_asArray"]; - } - - if (result.__cnt == 0 && config.emptyNodeForm == "text") { - result = ''; - } else if (result.__cnt == 1 && result.__text != null) { - result = result.__text; - } else if (result.__cnt == 1 && result.__cdata != null && !config.keepCData) { - result = result.__cdata; - } else if (result.__cnt > 1 && result.__text != null && config.skipEmptyTextNodesForObj) { - if (config.stripWhitespaces && result.__text == "" || result.__text.trim() == "") { - delete result.__text; - } - } - delete result.__cnt; - - if (config.enableToStringFunc && (result.__text != null || result.__cdata != null)) { - result.toString = function () { - return (this.__text != null ? this.__text : '') + (this.__cdata != null ? this.__cdata : ''); - }; - } - - return result; - } else if (node.nodeType == DOMNodeTypes.TEXT_NODE || node.nodeType == DOMNodeTypes.CDATA_SECTION_NODE) { - return node.nodeValue; - } - } - - function startTag(jsonObj, element, attrList, closed) { - var resultStr = "<" + (jsonObj != null && jsonObj.__prefix != null ? jsonObj.__prefix + ":" : "") + element; - if (attrList != null) { - for (var aidx = 0; aidx < attrList.length; aidx++) { - var attrName = attrList[aidx]; - var attrVal = jsonObj[attrName]; - if (config.escapeMode) attrVal = escapeXmlChars(attrVal); - resultStr += " " + attrName.substr(config.attributePrefix.length) + "="; - if (config.useDoubleQuotes) resultStr += '"' + attrVal + '"';else resultStr += "'" + attrVal + "'"; - } - } - if (!closed) resultStr += ">";else resultStr += "/>"; - return resultStr; - } - - function endTag(jsonObj, elementName) { - return ""; - } - - function endsWith(str, suffix) { - return str.indexOf(suffix, str.length - suffix.length) !== -1; - } - - function jsonXmlSpecialElem(jsonObj, jsonObjField) { - if (config.arrayAccessForm == "property" && endsWith(jsonObjField.toString(), "_asArray") || jsonObjField.toString().indexOf(config.attributePrefix) == 0 || jsonObjField.toString().indexOf("__") == 0 || jsonObj[jsonObjField] instanceof Function) return true;else return false; - } - - function jsonXmlElemCount(jsonObj) { - var elementsCnt = 0; - if (jsonObj instanceof Object) { - for (var it in jsonObj) { - if (jsonXmlSpecialElem(jsonObj, it)) continue; - elementsCnt++; - } - } - return elementsCnt; - } - - function checkJsonObjPropertiesFilter(jsonObj, propertyName, jsonObjPath) { - return config.jsonPropertiesFilter.length == 0 || jsonObjPath == "" || checkInStdFiltersArrayForm(config.jsonPropertiesFilter, jsonObj, propertyName, jsonObjPath); - } - - function parseJSONAttributes(jsonObj) { - var attrList = []; - if (jsonObj instanceof Object) { - for (var ait in jsonObj) { - if (ait.toString().indexOf("__") == -1 && ait.toString().indexOf(config.attributePrefix) == 0) { - attrList.push(ait); - } - } - } - return attrList; - } - - function parseJSONTextAttrs(jsonTxtObj) { - var result = ""; - - if (jsonTxtObj.__cdata != null) { - result += ""; - } - - if (jsonTxtObj.__text != null) { - if (config.escapeMode) result += escapeXmlChars(jsonTxtObj.__text);else result += jsonTxtObj.__text; - } - return result; - } - - function parseJSONTextObject(jsonTxtObj) { - var result = ""; - - if (jsonTxtObj instanceof Object) { - result += parseJSONTextAttrs(jsonTxtObj); - } else if (jsonTxtObj != null) { - if (config.escapeMode) result += escapeXmlChars(jsonTxtObj);else result += jsonTxtObj; - } - - return result; - } - - function getJsonPropertyPath(jsonObjPath, jsonPropName) { - if (jsonObjPath === "") { - return jsonPropName; - } else return jsonObjPath + "." + jsonPropName; - } - - function parseJSONArray(jsonArrRoot, jsonArrObj, attrList, jsonObjPath) { - var result = ""; - if (jsonArrRoot.length == 0) { - result += startTag(jsonArrRoot, jsonArrObj, attrList, true); - } else { - for (var arIdx = 0; arIdx < jsonArrRoot.length; arIdx++) { - result += startTag(jsonArrRoot[arIdx], jsonArrObj, parseJSONAttributes(jsonArrRoot[arIdx]), false); - result += parseJSONObject(jsonArrRoot[arIdx], getJsonPropertyPath(jsonObjPath, jsonArrObj)); - result += endTag(jsonArrRoot[arIdx], jsonArrObj); - } - } - return result; - } - - function parseJSONObject(jsonObj, jsonObjPath) { - var result = ""; - - var elementsCnt = jsonXmlElemCount(jsonObj); - - if (elementsCnt > 0) { - for (var it in jsonObj) { - - if (jsonXmlSpecialElem(jsonObj, it) || jsonObjPath != "" && !checkJsonObjPropertiesFilter(jsonObj, it, getJsonPropertyPath(jsonObjPath, it))) continue; - - var subObj = jsonObj[it]; - - var attrList = parseJSONAttributes(subObj); - - if (subObj == null || subObj == undefined) { - result += startTag(subObj, it, attrList, true); - } else if (subObj instanceof Object) { - - if (subObj instanceof Array) { - result += parseJSONArray(subObj, it, attrList, jsonObjPath); - } else if (subObj instanceof Date) { - result += startTag(subObj, it, attrList, false); - result += subObj.toISOString(); - result += endTag(subObj, it); - } else { - var subObjElementsCnt = jsonXmlElemCount(subObj); - if (subObjElementsCnt > 0 || subObj.__text != null || subObj.__cdata != null) { - result += startTag(subObj, it, attrList, false); - result += parseJSONObject(subObj, getJsonPropertyPath(jsonObjPath, it)); - result += endTag(subObj, it); - } else { - result += startTag(subObj, it, attrList, true); - } - } - } else { - result += startTag(subObj, it, attrList, false); - result += parseJSONTextObject(subObj); - result += endTag(subObj, it); - } - } - } - result += parseJSONTextObject(jsonObj); - - return result; - } - - this.parseXmlString = function (xmlDocStr) { - var isIEParser = window.ActiveXObject || "ActiveXObject" in window; - if (xmlDocStr === undefined) { - return null; - } - var xmlDoc; - if (window.DOMParser) { - var parser = new window.DOMParser(); - var parsererrorNS = null; - try { - xmlDoc = parser.parseFromString(xmlDocStr, "text/xml"); - if (xmlDoc.getElementsByTagNameNS("*", "parsererror").length > 0) { - xmlDoc = null; - } - } catch (err) { - xmlDoc = null; - } - } else { - // IE :( - if (xmlDocStr.indexOf("") + 2); - } - xmlDoc = new ActiveXObject("Microsoft.XMLDOM"); - xmlDoc.async = "false"; - xmlDoc.loadXML(xmlDocStr); - } - return xmlDoc; - }; - - this.asArray = function (prop) { - if (prop === undefined || prop == null) return [];else if (prop instanceof Array) return prop;else return [prop]; - }; - - this.toXmlDateTime = function (dt) { - if (dt instanceof Date) return dt.toISOString();else if (typeof dt === 'number') return new Date(dt).toISOString();else return null; - }; - - this.asDateTime = function (prop) { - if (typeof prop == "string") { - return fromXmlDateTime(prop); - } else return prop; - }; - - this.xml2json = function (xmlDoc) { - return parseDOMChildren(xmlDoc); - }; - - this.xml_str2json = function (xmlDocStr) { - var xmlDoc = this.parseXmlString(xmlDocStr); - if (xmlDoc != null) return this.xml2json(xmlDoc);else return null; - }; - - this.json2xml_str = function (jsonObj) { - return parseJSONObject(jsonObj, ""); - }; - - this.json2xml = function (jsonObj) { - var xmlDocStr = this.json2xml_str(jsonObj); - return this.parseXmlString(xmlDocStr); - }; - - this.getVersion = function () { - return VERSION; - }; -} - -exports["default"] = X2JS; -module.exports = exports["default"]; - -},{}],4:[function(_dereq_,module,exports){ -/** - * The copyright in this software is being made available under the BSD License, - * included below. This software may be subject to other third party and contributor - * rights, including patent rights, and no such rights are granted under this license. - * - * Copyright (c) 2013, Dash Industry Forum. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * * Neither the name of Dash Industry Forum nor the names of its - * contributors may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -'use strict'; - -Object.defineProperty(exports, '__esModule', { - value: true -}); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - -var _index_mediaplayerOnly = _dereq_(5); - -var _srcStreamingUtilsCapabilities = _dereq_(220); - -var _srcStreamingMetricsMetricsReporting = _dereq_(129); - -var _srcStreamingMetricsMetricsReporting2 = _interopRequireDefault(_srcStreamingMetricsMetricsReporting); - -var _srcStreamingProtectionProtection = _dereq_(166); - -var _srcStreamingProtectionProtection2 = _interopRequireDefault(_srcStreamingProtectionProtection); - -var _srcStreamingMediaPlayerFactory = _dereq_(105); - -var _srcStreamingMediaPlayerFactory2 = _interopRequireDefault(_srcStreamingMediaPlayerFactory); - -var _srcCoreDebug = _dereq_(45); - -var _srcCoreDebug2 = _interopRequireDefault(_srcCoreDebug); - -dashjs.Protection = _srcStreamingProtectionProtection2['default']; -dashjs.MetricsReporting = _srcStreamingMetricsMetricsReporting2['default']; -dashjs.MediaPlayerFactory = _srcStreamingMediaPlayerFactory2['default']; -dashjs.Debug = _srcCoreDebug2['default']; -dashjs.supportsMediaSource = _srcStreamingUtilsCapabilities.supportsMediaSource; - -exports['default'] = dashjs; -exports.MediaPlayer = _index_mediaplayerOnly.MediaPlayer; -exports.Protection = _srcStreamingProtectionProtection2['default']; -exports.MetricsReporting = _srcStreamingMetricsMetricsReporting2['default']; -exports.MediaPlayerFactory = _srcStreamingMediaPlayerFactory2['default']; -exports.Debug = _srcCoreDebug2['default']; -exports.supportsMediaSource = _srcStreamingUtilsCapabilities.supportsMediaSource; - -},{"105":105,"129":129,"166":166,"220":220,"45":45,"5":5}],5:[function(_dereq_,module,exports){ -(function (global){ -/** - * The copyright in this software is being made available under the BSD License, - * included below. This software may be subject to other third party and contributor - * rights, including patent rights, and no such rights are granted under this license. - * - * Copyright (c) 2013, Dash Industry Forum. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * * Neither the name of Dash Industry Forum nor the names of its - * contributors may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -'use strict'; - -Object.defineProperty(exports, '__esModule', { - value: true -}); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - -var _srcStreamingMediaPlayer = _dereq_(103); - -var _srcStreamingMediaPlayer2 = _interopRequireDefault(_srcStreamingMediaPlayer); - -var _srcCoreFactoryMaker = _dereq_(47); - -var _srcCoreFactoryMaker2 = _interopRequireDefault(_srcCoreFactoryMaker); - -var _srcCoreDebug = _dereq_(45); - -var _srcCoreDebug2 = _interopRequireDefault(_srcCoreDebug); - -var _srcCoreVersion = _dereq_(50); - -// Shove both of these into the global scope -var context = typeof window !== 'undefined' && window || global; - -var dashjs = context.dashjs; -if (!dashjs) { - dashjs = context.dashjs = {}; -} - -dashjs.MediaPlayer = _srcStreamingMediaPlayer2['default']; -dashjs.FactoryMaker = _srcCoreFactoryMaker2['default']; -dashjs.Debug = _srcCoreDebug2['default']; -dashjs.Version = (0, _srcCoreVersion.getVersionString)(); - -exports['default'] = dashjs; -exports.MediaPlayer = _srcStreamingMediaPlayer2['default']; -exports.FactoryMaker = _srcCoreFactoryMaker2['default']; -exports.Debug = _srcCoreDebug2['default']; - -}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) - -},{"103":103,"45":45,"47":47,"50":50}],6:[function(_dereq_,module,exports){ -'use strict' - -exports.byteLength = byteLength -exports.toByteArray = toByteArray -exports.fromByteArray = fromByteArray - -var lookup = [] -var revLookup = [] -var Arr = typeof Uint8Array !== 'undefined' ? Uint8Array : Array - -var code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' -for (var i = 0, len = code.length; i < len; ++i) { - lookup[i] = code[i] - revLookup[code.charCodeAt(i)] = i -} - -// Support decoding URL-safe base64 strings, as Node.js does. -// See: https://en.wikipedia.org/wiki/Base64#URL_applications -revLookup['-'.charCodeAt(0)] = 62 -revLookup['_'.charCodeAt(0)] = 63 - -function getLens (b64) { - var len = b64.length - - if (len % 4 > 0) { - throw new Error('Invalid string. Length must be a multiple of 4') - } - - // Trim off extra bytes after placeholder bytes are found - // See: https://github.com/beatgammit/base64-js/issues/42 - var validLen = b64.indexOf('=') - if (validLen === -1) validLen = len - - var placeHoldersLen = validLen === len - ? 0 - : 4 - (validLen % 4) - - return [validLen, placeHoldersLen] -} - -// base64 is 4/3 + up to two characters of the original data -function byteLength (b64) { - var lens = getLens(b64) - var validLen = lens[0] - var placeHoldersLen = lens[1] - return ((validLen + placeHoldersLen) * 3 / 4) - placeHoldersLen -} - -function _byteLength (b64, validLen, placeHoldersLen) { - return ((validLen + placeHoldersLen) * 3 / 4) - placeHoldersLen -} - -function toByteArray (b64) { - var tmp - var lens = getLens(b64) - var validLen = lens[0] - var placeHoldersLen = lens[1] - - var arr = new Arr(_byteLength(b64, validLen, placeHoldersLen)) - - var curByte = 0 - - // if there are placeholders, only get up to the last complete 4 chars - var len = placeHoldersLen > 0 - ? validLen - 4 - : validLen - - for (var i = 0; i < len; i += 4) { - tmp = - (revLookup[b64.charCodeAt(i)] << 18) | - (revLookup[b64.charCodeAt(i + 1)] << 12) | - (revLookup[b64.charCodeAt(i + 2)] << 6) | - revLookup[b64.charCodeAt(i + 3)] - arr[curByte++] = (tmp >> 16) & 0xFF - arr[curByte++] = (tmp >> 8) & 0xFF - arr[curByte++] = tmp & 0xFF - } - - if (placeHoldersLen === 2) { - tmp = - (revLookup[b64.charCodeAt(i)] << 2) | - (revLookup[b64.charCodeAt(i + 1)] >> 4) - arr[curByte++] = tmp & 0xFF - } - - if (placeHoldersLen === 1) { - tmp = - (revLookup[b64.charCodeAt(i)] << 10) | - (revLookup[b64.charCodeAt(i + 1)] << 4) | - (revLookup[b64.charCodeAt(i + 2)] >> 2) - arr[curByte++] = (tmp >> 8) & 0xFF - arr[curByte++] = tmp & 0xFF - } - - return arr -} - -function tripletToBase64 (num) { - return lookup[num >> 18 & 0x3F] + - lookup[num >> 12 & 0x3F] + - lookup[num >> 6 & 0x3F] + - lookup[num & 0x3F] -} - -function encodeChunk (uint8, start, end) { - var tmp - var output = [] - for (var i = start; i < end; i += 3) { - tmp = - ((uint8[i] << 16) & 0xFF0000) + - ((uint8[i + 1] << 8) & 0xFF00) + - (uint8[i + 2] & 0xFF) - output.push(tripletToBase64(tmp)) - } - return output.join('') -} - -function fromByteArray (uint8) { - var tmp - var len = uint8.length - var extraBytes = len % 3 // if we have 1 byte left, pad 2 bytes - var parts = [] - var maxChunkLength = 16383 // must be multiple of 3 - - // go through the array every three bytes, we'll deal with trailing stuff later - for (var i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) { - parts.push(encodeChunk( - uint8, i, (i + maxChunkLength) > len2 ? len2 : (i + maxChunkLength) - )) - } - - // pad the end with zeros, but make sure to not forget the extra bytes - if (extraBytes === 1) { - tmp = uint8[len - 1] - parts.push( - lookup[tmp >> 2] + - lookup[(tmp << 4) & 0x3F] + - '==' - ) - } else if (extraBytes === 2) { - tmp = (uint8[len - 2] << 8) + uint8[len - 1] - parts.push( - lookup[tmp >> 10] + - lookup[(tmp >> 4) & 0x3F] + - lookup[(tmp << 2) & 0x3F] + - '=' - ) - } - - return parts.join('') -} - -},{}],7:[function(_dereq_,module,exports){ - -},{}],8:[function(_dereq_,module,exports){ -/*! - * The buffer module from node.js, for the browser. - * - * @author Feross Aboukhadijeh - * @license MIT - */ -/* eslint-disable no-proto */ - -'use strict' - -var base64 = _dereq_(6) -var ieee754 = _dereq_(13) - -exports.Buffer = Buffer -exports.SlowBuffer = SlowBuffer -exports.INSPECT_MAX_BYTES = 50 - -var K_MAX_LENGTH = 0x7fffffff -exports.kMaxLength = K_MAX_LENGTH - -/** - * If `Buffer.TYPED_ARRAY_SUPPORT`: - * === true Use Uint8Array implementation (fastest) - * === false Print warning and recommend using `buffer` v4.x which has an Object - * implementation (most compatible, even IE6) - * - * Browsers that support typed arrays are IE 10+, Firefox 4+, Chrome 7+, Safari 5.1+, - * Opera 11.6+, iOS 4.2+. - * - * We report that the browser does not support typed arrays if the are not subclassable - * using __proto__. Firefox 4-29 lacks support for adding new properties to `Uint8Array` - * (See: https://bugzilla.mozilla.org/show_bug.cgi?id=695438). IE 10 lacks support - * for __proto__ and has a buggy typed array implementation. - */ -Buffer.TYPED_ARRAY_SUPPORT = typedArraySupport() - -if (!Buffer.TYPED_ARRAY_SUPPORT && typeof console !== 'undefined' && - typeof console.error === 'function') { - console.error( - 'This browser lacks typed array (Uint8Array) support which is required by ' + - '`buffer` v5.x. Use `buffer` v4.x if you require old browser support.' - ) -} - -function typedArraySupport () { - // Can typed array instances can be augmented? - try { - var arr = new Uint8Array(1) - arr.__proto__ = { __proto__: Uint8Array.prototype, foo: function () { return 42 } } - return arr.foo() === 42 - } catch (e) { - return false - } -} - -Object.defineProperty(Buffer.prototype, 'parent', { - enumerable: true, - get: function () { - if (!Buffer.isBuffer(this)) return undefined - return this.buffer - } -}) - -Object.defineProperty(Buffer.prototype, 'offset', { - enumerable: true, - get: function () { - if (!Buffer.isBuffer(this)) return undefined - return this.byteOffset - } -}) - -function createBuffer (length) { - if (length > K_MAX_LENGTH) { - throw new RangeError('The value "' + length + '" is invalid for option "size"') - } - // Return an augmented `Uint8Array` instance - var buf = new Uint8Array(length) - buf.__proto__ = Buffer.prototype - return buf -} - -/** - * The Buffer constructor returns instances of `Uint8Array` that have their - * prototype changed to `Buffer.prototype`. Furthermore, `Buffer` is a subclass of - * `Uint8Array`, so the returned instances will have all the node `Buffer` methods - * and the `Uint8Array` methods. Square bracket notation works as expected -- it - * returns a single octet. - * - * The `Uint8Array` prototype remains unmodified. - */ - -function Buffer (arg, encodingOrOffset, length) { - // Common case. - if (typeof arg === 'number') { - if (typeof encodingOrOffset === 'string') { - throw new TypeError( - 'The "string" argument must be of type string. Received type number' - ) - } - return allocUnsafe(arg) - } - return from(arg, encodingOrOffset, length) -} - -// Fix subarray() in ES2016. See: https://github.com/feross/buffer/pull/97 -if (typeof Symbol !== 'undefined' && Symbol.species != null && - Buffer[Symbol.species] === Buffer) { - Object.defineProperty(Buffer, Symbol.species, { - value: null, - configurable: true, - enumerable: false, - writable: false - }) -} - -Buffer.poolSize = 8192 // not used by this implementation - -function from (value, encodingOrOffset, length) { - if (typeof value === 'string') { - return fromString(value, encodingOrOffset) - } - - if (ArrayBuffer.isView(value)) { - return fromArrayLike(value) - } - - if (value == null) { - throw TypeError( - 'The first argument must be one of type string, Buffer, ArrayBuffer, Array, ' + - 'or Array-like Object. Received type ' + (typeof value) - ) - } - - if (isInstance(value, ArrayBuffer) || - (value && isInstance(value.buffer, ArrayBuffer))) { - return fromArrayBuffer(value, encodingOrOffset, length) - } - - if (typeof value === 'number') { - throw new TypeError( - 'The "value" argument must not be of type number. Received type number' - ) - } - - var valueOf = value.valueOf && value.valueOf() - if (valueOf != null && valueOf !== value) { - return Buffer.from(valueOf, encodingOrOffset, length) - } - - var b = fromObject(value) - if (b) return b - - if (typeof Symbol !== 'undefined' && Symbol.toPrimitive != null && - typeof value[Symbol.toPrimitive] === 'function') { - return Buffer.from( - value[Symbol.toPrimitive]('string'), encodingOrOffset, length - ) - } - - throw new TypeError( - 'The first argument must be one of type string, Buffer, ArrayBuffer, Array, ' + - 'or Array-like Object. Received type ' + (typeof value) - ) -} - -/** - * Functionally equivalent to Buffer(arg, encoding) but throws a TypeError - * if value is a number. - * Buffer.from(str[, encoding]) - * Buffer.from(array) - * Buffer.from(buffer) - * Buffer.from(arrayBuffer[, byteOffset[, length]]) - **/ -Buffer.from = function (value, encodingOrOffset, length) { - return from(value, encodingOrOffset, length) -} - -// Note: Change prototype *after* Buffer.from is defined to workaround Chrome bug: -// https://github.com/feross/buffer/pull/148 -Buffer.prototype.__proto__ = Uint8Array.prototype -Buffer.__proto__ = Uint8Array - -function assertSize (size) { - if (typeof size !== 'number') { - throw new TypeError('"size" argument must be of type number') - } else if (size < 0) { - throw new RangeError('The value "' + size + '" is invalid for option "size"') - } -} - -function alloc (size, fill, encoding) { - assertSize(size) - if (size <= 0) { - return createBuffer(size) - } - if (fill !== undefined) { - // Only pay attention to encoding if it's a string. This - // prevents accidentally sending in a number that would - // be interpretted as a start offset. - return typeof encoding === 'string' - ? createBuffer(size).fill(fill, encoding) - : createBuffer(size).fill(fill) - } - return createBuffer(size) -} - -/** - * Creates a new filled Buffer instance. - * alloc(size[, fill[, encoding]]) - **/ -Buffer.alloc = function (size, fill, encoding) { - return alloc(size, fill, encoding) -} - -function allocUnsafe (size) { - assertSize(size) - return createBuffer(size < 0 ? 0 : checked(size) | 0) -} - -/** - * Equivalent to Buffer(num), by default creates a non-zero-filled Buffer instance. - * */ -Buffer.allocUnsafe = function (size) { - return allocUnsafe(size) -} -/** - * Equivalent to SlowBuffer(num), by default creates a non-zero-filled Buffer instance. - */ -Buffer.allocUnsafeSlow = function (size) { - return allocUnsafe(size) -} - -function fromString (string, encoding) { - if (typeof encoding !== 'string' || encoding === '') { - encoding = 'utf8' - } - - if (!Buffer.isEncoding(encoding)) { - throw new TypeError('Unknown encoding: ' + encoding) - } - - var length = byteLength(string, encoding) | 0 - var buf = createBuffer(length) - - var actual = buf.write(string, encoding) - - if (actual !== length) { - // Writing a hex string, for example, that contains invalid characters will - // cause everything after the first invalid character to be ignored. (e.g. - // 'abxxcd' will be treated as 'ab') - buf = buf.slice(0, actual) - } - - return buf -} - -function fromArrayLike (array) { - var length = array.length < 0 ? 0 : checked(array.length) | 0 - var buf = createBuffer(length) - for (var i = 0; i < length; i += 1) { - buf[i] = array[i] & 255 - } - return buf -} - -function fromArrayBuffer (array, byteOffset, length) { - if (byteOffset < 0 || array.byteLength < byteOffset) { - throw new RangeError('"offset" is outside of buffer bounds') - } - - if (array.byteLength < byteOffset + (length || 0)) { - throw new RangeError('"length" is outside of buffer bounds') - } - - var buf - if (byteOffset === undefined && length === undefined) { - buf = new Uint8Array(array) - } else if (length === undefined) { - buf = new Uint8Array(array, byteOffset) - } else { - buf = new Uint8Array(array, byteOffset, length) - } - - // Return an augmented `Uint8Array` instance - buf.__proto__ = Buffer.prototype - return buf -} - -function fromObject (obj) { - if (Buffer.isBuffer(obj)) { - var len = checked(obj.length) | 0 - var buf = createBuffer(len) - - if (buf.length === 0) { - return buf - } - - obj.copy(buf, 0, 0, len) - return buf - } - - if (obj.length !== undefined) { - if (typeof obj.length !== 'number' || numberIsNaN(obj.length)) { - return createBuffer(0) - } - return fromArrayLike(obj) - } - - if (obj.type === 'Buffer' && Array.isArray(obj.data)) { - return fromArrayLike(obj.data) - } -} - -function checked (length) { - // Note: cannot use `length < K_MAX_LENGTH` here because that fails when - // length is NaN (which is otherwise coerced to zero.) - if (length >= K_MAX_LENGTH) { - throw new RangeError('Attempt to allocate Buffer larger than maximum ' + - 'size: 0x' + K_MAX_LENGTH.toString(16) + ' bytes') - } - return length | 0 -} - -function SlowBuffer (length) { - if (+length != length) { // eslint-disable-line eqeqeq - length = 0 - } - return Buffer.alloc(+length) -} - -Buffer.isBuffer = function isBuffer (b) { - return b != null && b._isBuffer === true && - b !== Buffer.prototype // so Buffer.isBuffer(Buffer.prototype) will be false -} - -Buffer.compare = function compare (a, b) { - if (isInstance(a, Uint8Array)) a = Buffer.from(a, a.offset, a.byteLength) - if (isInstance(b, Uint8Array)) b = Buffer.from(b, b.offset, b.byteLength) - if (!Buffer.isBuffer(a) || !Buffer.isBuffer(b)) { - throw new TypeError( - 'The "buf1", "buf2" arguments must be one of type Buffer or Uint8Array' - ) - } - - if (a === b) return 0 - - var x = a.length - var y = b.length - - for (var i = 0, len = Math.min(x, y); i < len; ++i) { - if (a[i] !== b[i]) { - x = a[i] - y = b[i] - break - } - } - - if (x < y) return -1 - if (y < x) return 1 - return 0 -} - -Buffer.isEncoding = function isEncoding (encoding) { - switch (String(encoding).toLowerCase()) { - case 'hex': - case 'utf8': - case 'utf-8': - case 'ascii': - case 'latin1': - case 'binary': - case 'base64': - case 'ucs2': - case 'ucs-2': - case 'utf16le': - case 'utf-16le': - return true - default: - return false - } -} - -Buffer.concat = function concat (list, length) { - if (!Array.isArray(list)) { - throw new TypeError('"list" argument must be an Array of Buffers') - } - - if (list.length === 0) { - return Buffer.alloc(0) - } - - var i - if (length === undefined) { - length = 0 - for (i = 0; i < list.length; ++i) { - length += list[i].length - } - } - - var buffer = Buffer.allocUnsafe(length) - var pos = 0 - for (i = 0; i < list.length; ++i) { - var buf = list[i] - if (isInstance(buf, Uint8Array)) { - buf = Buffer.from(buf) - } - if (!Buffer.isBuffer(buf)) { - throw new TypeError('"list" argument must be an Array of Buffers') - } - buf.copy(buffer, pos) - pos += buf.length - } - return buffer -} - -function byteLength (string, encoding) { - if (Buffer.isBuffer(string)) { - return string.length - } - if (ArrayBuffer.isView(string) || isInstance(string, ArrayBuffer)) { - return string.byteLength - } - if (typeof string !== 'string') { - throw new TypeError( - 'The "string" argument must be one of type string, Buffer, or ArrayBuffer. ' + - 'Received type ' + typeof string - ) - } - - var len = string.length - var mustMatch = (arguments.length > 2 && arguments[2] === true) - if (!mustMatch && len === 0) return 0 - - // Use a for loop to avoid recursion - var loweredCase = false - for (;;) { - switch (encoding) { - case 'ascii': - case 'latin1': - case 'binary': - return len - case 'utf8': - case 'utf-8': - return utf8ToBytes(string).length - case 'ucs2': - case 'ucs-2': - case 'utf16le': - case 'utf-16le': - return len * 2 - case 'hex': - return len >>> 1 - case 'base64': - return base64ToBytes(string).length - default: - if (loweredCase) { - return mustMatch ? -1 : utf8ToBytes(string).length // assume utf8 - } - encoding = ('' + encoding).toLowerCase() - loweredCase = true - } - } -} -Buffer.byteLength = byteLength - -function slowToString (encoding, start, end) { - var loweredCase = false - - // No need to verify that "this.length <= MAX_UINT32" since it's a read-only - // property of a typed array. - - // This behaves neither like String nor Uint8Array in that we set start/end - // to their upper/lower bounds if the value passed is out of range. - // undefined is handled specially as per ECMA-262 6th Edition, - // Section 13.3.3.7 Runtime Semantics: KeyedBindingInitialization. - if (start === undefined || start < 0) { - start = 0 - } - // Return early if start > this.length. Done here to prevent potential uint32 - // coercion fail below. - if (start > this.length) { - return '' - } - - if (end === undefined || end > this.length) { - end = this.length - } - - if (end <= 0) { - return '' - } - - // Force coersion to uint32. This will also coerce falsey/NaN values to 0. - end >>>= 0 - start >>>= 0 - - if (end <= start) { - return '' - } - - if (!encoding) encoding = 'utf8' - - while (true) { - switch (encoding) { - case 'hex': - return hexSlice(this, start, end) - - case 'utf8': - case 'utf-8': - return utf8Slice(this, start, end) - - case 'ascii': - return asciiSlice(this, start, end) - - case 'latin1': - case 'binary': - return latin1Slice(this, start, end) - - case 'base64': - return base64Slice(this, start, end) - - case 'ucs2': - case 'ucs-2': - case 'utf16le': - case 'utf-16le': - return utf16leSlice(this, start, end) - - default: - if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding) - encoding = (encoding + '').toLowerCase() - loweredCase = true - } - } -} - -// This property is used by `Buffer.isBuffer` (and the `is-buffer` npm package) -// to detect a Buffer instance. It's not possible to use `instanceof Buffer` -// reliably in a browserify context because there could be multiple different -// copies of the 'buffer' package in use. This method works even for Buffer -// instances that were created from another copy of the `buffer` package. -// See: https://github.com/feross/buffer/issues/154 -Buffer.prototype._isBuffer = true - -function swap (b, n, m) { - var i = b[n] - b[n] = b[m] - b[m] = i -} - -Buffer.prototype.swap16 = function swap16 () { - var len = this.length - if (len % 2 !== 0) { - throw new RangeError('Buffer size must be a multiple of 16-bits') - } - for (var i = 0; i < len; i += 2) { - swap(this, i, i + 1) - } - return this -} - -Buffer.prototype.swap32 = function swap32 () { - var len = this.length - if (len % 4 !== 0) { - throw new RangeError('Buffer size must be a multiple of 32-bits') - } - for (var i = 0; i < len; i += 4) { - swap(this, i, i + 3) - swap(this, i + 1, i + 2) - } - return this -} - -Buffer.prototype.swap64 = function swap64 () { - var len = this.length - if (len % 8 !== 0) { - throw new RangeError('Buffer size must be a multiple of 64-bits') - } - for (var i = 0; i < len; i += 8) { - swap(this, i, i + 7) - swap(this, i + 1, i + 6) - swap(this, i + 2, i + 5) - swap(this, i + 3, i + 4) - } - return this -} - -Buffer.prototype.toString = function toString () { - var length = this.length - if (length === 0) return '' - if (arguments.length === 0) return utf8Slice(this, 0, length) - return slowToString.apply(this, arguments) -} - -Buffer.prototype.toLocaleString = Buffer.prototype.toString - -Buffer.prototype.equals = function equals (b) { - if (!Buffer.isBuffer(b)) throw new TypeError('Argument must be a Buffer') - if (this === b) return true - return Buffer.compare(this, b) === 0 -} - -Buffer.prototype.inspect = function inspect () { - var str = '' - var max = exports.INSPECT_MAX_BYTES - str = this.toString('hex', 0, max).replace(/(.{2})/g, '$1 ').trim() - if (this.length > max) str += ' ... ' - return '' -} - -Buffer.prototype.compare = function compare (target, start, end, thisStart, thisEnd) { - if (isInstance(target, Uint8Array)) { - target = Buffer.from(target, target.offset, target.byteLength) - } - if (!Buffer.isBuffer(target)) { - throw new TypeError( - 'The "target" argument must be one of type Buffer or Uint8Array. ' + - 'Received type ' + (typeof target) - ) - } - - if (start === undefined) { - start = 0 - } - if (end === undefined) { - end = target ? target.length : 0 - } - if (thisStart === undefined) { - thisStart = 0 - } - if (thisEnd === undefined) { - thisEnd = this.length - } - - if (start < 0 || end > target.length || thisStart < 0 || thisEnd > this.length) { - throw new RangeError('out of range index') - } - - if (thisStart >= thisEnd && start >= end) { - return 0 - } - if (thisStart >= thisEnd) { - return -1 - } - if (start >= end) { - return 1 - } - - start >>>= 0 - end >>>= 0 - thisStart >>>= 0 - thisEnd >>>= 0 - - if (this === target) return 0 - - var x = thisEnd - thisStart - var y = end - start - var len = Math.min(x, y) - - var thisCopy = this.slice(thisStart, thisEnd) - var targetCopy = target.slice(start, end) - - for (var i = 0; i < len; ++i) { - if (thisCopy[i] !== targetCopy[i]) { - x = thisCopy[i] - y = targetCopy[i] - break - } - } - - if (x < y) return -1 - if (y < x) return 1 - return 0 -} - -// Finds either the first index of `val` in `buffer` at offset >= `byteOffset`, -// OR the last index of `val` in `buffer` at offset <= `byteOffset`. -// -// Arguments: -// - buffer - a Buffer to search -// - val - a string, Buffer, or number -// - byteOffset - an index into `buffer`; will be clamped to an int32 -// - encoding - an optional encoding, relevant is val is a string -// - dir - true for indexOf, false for lastIndexOf -function bidirectionalIndexOf (buffer, val, byteOffset, encoding, dir) { - // Empty buffer means no match - if (buffer.length === 0) return -1 - - // Normalize byteOffset - if (typeof byteOffset === 'string') { - encoding = byteOffset - byteOffset = 0 - } else if (byteOffset > 0x7fffffff) { - byteOffset = 0x7fffffff - } else if (byteOffset < -0x80000000) { - byteOffset = -0x80000000 - } - byteOffset = +byteOffset // Coerce to Number. - if (numberIsNaN(byteOffset)) { - // byteOffset: it it's undefined, null, NaN, "foo", etc, search whole buffer - byteOffset = dir ? 0 : (buffer.length - 1) - } - - // Normalize byteOffset: negative offsets start from the end of the buffer - if (byteOffset < 0) byteOffset = buffer.length + byteOffset - if (byteOffset >= buffer.length) { - if (dir) return -1 - else byteOffset = buffer.length - 1 - } else if (byteOffset < 0) { - if (dir) byteOffset = 0 - else return -1 - } - - // Normalize val - if (typeof val === 'string') { - val = Buffer.from(val, encoding) - } - - // Finally, search either indexOf (if dir is true) or lastIndexOf - if (Buffer.isBuffer(val)) { - // Special case: looking for empty string/buffer always fails - if (val.length === 0) { - return -1 - } - return arrayIndexOf(buffer, val, byteOffset, encoding, dir) - } else if (typeof val === 'number') { - val = val & 0xFF // Search for a byte value [0-255] - if (typeof Uint8Array.prototype.indexOf === 'function') { - if (dir) { - return Uint8Array.prototype.indexOf.call(buffer, val, byteOffset) - } else { - return Uint8Array.prototype.lastIndexOf.call(buffer, val, byteOffset) - } - } - return arrayIndexOf(buffer, [ val ], byteOffset, encoding, dir) - } - - throw new TypeError('val must be string, number or Buffer') -} - -function arrayIndexOf (arr, val, byteOffset, encoding, dir) { - var indexSize = 1 - var arrLength = arr.length - var valLength = val.length - - if (encoding !== undefined) { - encoding = String(encoding).toLowerCase() - if (encoding === 'ucs2' || encoding === 'ucs-2' || - encoding === 'utf16le' || encoding === 'utf-16le') { - if (arr.length < 2 || val.length < 2) { - return -1 - } - indexSize = 2 - arrLength /= 2 - valLength /= 2 - byteOffset /= 2 - } - } - - function read (buf, i) { - if (indexSize === 1) { - return buf[i] - } else { - return buf.readUInt16BE(i * indexSize) - } - } - - var i - if (dir) { - var foundIndex = -1 - for (i = byteOffset; i < arrLength; i++) { - if (read(arr, i) === read(val, foundIndex === -1 ? 0 : i - foundIndex)) { - if (foundIndex === -1) foundIndex = i - if (i - foundIndex + 1 === valLength) return foundIndex * indexSize - } else { - if (foundIndex !== -1) i -= i - foundIndex - foundIndex = -1 - } - } - } else { - if (byteOffset + valLength > arrLength) byteOffset = arrLength - valLength - for (i = byteOffset; i >= 0; i--) { - var found = true - for (var j = 0; j < valLength; j++) { - if (read(arr, i + j) !== read(val, j)) { - found = false - break - } - } - if (found) return i - } - } - - return -1 -} - -Buffer.prototype.includes = function includes (val, byteOffset, encoding) { - return this.indexOf(val, byteOffset, encoding) !== -1 -} - -Buffer.prototype.indexOf = function indexOf (val, byteOffset, encoding) { - return bidirectionalIndexOf(this, val, byteOffset, encoding, true) -} - -Buffer.prototype.lastIndexOf = function lastIndexOf (val, byteOffset, encoding) { - return bidirectionalIndexOf(this, val, byteOffset, encoding, false) -} - -function hexWrite (buf, string, offset, length) { - offset = Number(offset) || 0 - var remaining = buf.length - offset - if (!length) { - length = remaining - } else { - length = Number(length) - if (length > remaining) { - length = remaining - } - } - - var strLen = string.length - - if (length > strLen / 2) { - length = strLen / 2 - } - for (var i = 0; i < length; ++i) { - var parsed = parseInt(string.substr(i * 2, 2), 16) - if (numberIsNaN(parsed)) return i - buf[offset + i] = parsed - } - return i -} - -function utf8Write (buf, string, offset, length) { - return blitBuffer(utf8ToBytes(string, buf.length - offset), buf, offset, length) -} - -function asciiWrite (buf, string, offset, length) { - return blitBuffer(asciiToBytes(string), buf, offset, length) -} - -function latin1Write (buf, string, offset, length) { - return asciiWrite(buf, string, offset, length) -} - -function base64Write (buf, string, offset, length) { - return blitBuffer(base64ToBytes(string), buf, offset, length) -} - -function ucs2Write (buf, string, offset, length) { - return blitBuffer(utf16leToBytes(string, buf.length - offset), buf, offset, length) -} - -Buffer.prototype.write = function write (string, offset, length, encoding) { - // Buffer#write(string) - if (offset === undefined) { - encoding = 'utf8' - length = this.length - offset = 0 - // Buffer#write(string, encoding) - } else if (length === undefined && typeof offset === 'string') { - encoding = offset - length = this.length - offset = 0 - // Buffer#write(string, offset[, length][, encoding]) - } else if (isFinite(offset)) { - offset = offset >>> 0 - if (isFinite(length)) { - length = length >>> 0 - if (encoding === undefined) encoding = 'utf8' - } else { - encoding = length - length = undefined - } - } else { - throw new Error( - 'Buffer.write(string, encoding, offset[, length]) is no longer supported' - ) - } - - var remaining = this.length - offset - if (length === undefined || length > remaining) length = remaining - - if ((string.length > 0 && (length < 0 || offset < 0)) || offset > this.length) { - throw new RangeError('Attempt to write outside buffer bounds') - } - - if (!encoding) encoding = 'utf8' - - var loweredCase = false - for (;;) { - switch (encoding) { - case 'hex': - return hexWrite(this, string, offset, length) - - case 'utf8': - case 'utf-8': - return utf8Write(this, string, offset, length) - - case 'ascii': - return asciiWrite(this, string, offset, length) - - case 'latin1': - case 'binary': - return latin1Write(this, string, offset, length) - - case 'base64': - // Warning: maxLength not taken into account in base64Write - return base64Write(this, string, offset, length) - - case 'ucs2': - case 'ucs-2': - case 'utf16le': - case 'utf-16le': - return ucs2Write(this, string, offset, length) - - default: - if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding) - encoding = ('' + encoding).toLowerCase() - loweredCase = true - } - } -} - -Buffer.prototype.toJSON = function toJSON () { - return { - type: 'Buffer', - data: Array.prototype.slice.call(this._arr || this, 0) - } -} - -function base64Slice (buf, start, end) { - if (start === 0 && end === buf.length) { - return base64.fromByteArray(buf) - } else { - return base64.fromByteArray(buf.slice(start, end)) - } -} - -function utf8Slice (buf, start, end) { - end = Math.min(buf.length, end) - var res = [] - - var i = start - while (i < end) { - var firstByte = buf[i] - var codePoint = null - var bytesPerSequence = (firstByte > 0xEF) ? 4 - : (firstByte > 0xDF) ? 3 - : (firstByte > 0xBF) ? 2 - : 1 - - if (i + bytesPerSequence <= end) { - var secondByte, thirdByte, fourthByte, tempCodePoint - - switch (bytesPerSequence) { - case 1: - if (firstByte < 0x80) { - codePoint = firstByte - } - break - case 2: - secondByte = buf[i + 1] - if ((secondByte & 0xC0) === 0x80) { - tempCodePoint = (firstByte & 0x1F) << 0x6 | (secondByte & 0x3F) - if (tempCodePoint > 0x7F) { - codePoint = tempCodePoint - } - } - break - case 3: - secondByte = buf[i + 1] - thirdByte = buf[i + 2] - if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80) { - tempCodePoint = (firstByte & 0xF) << 0xC | (secondByte & 0x3F) << 0x6 | (thirdByte & 0x3F) - if (tempCodePoint > 0x7FF && (tempCodePoint < 0xD800 || tempCodePoint > 0xDFFF)) { - codePoint = tempCodePoint - } - } - break - case 4: - secondByte = buf[i + 1] - thirdByte = buf[i + 2] - fourthByte = buf[i + 3] - if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80 && (fourthByte & 0xC0) === 0x80) { - tempCodePoint = (firstByte & 0xF) << 0x12 | (secondByte & 0x3F) << 0xC | (thirdByte & 0x3F) << 0x6 | (fourthByte & 0x3F) - if (tempCodePoint > 0xFFFF && tempCodePoint < 0x110000) { - codePoint = tempCodePoint - } - } - } - } - - if (codePoint === null) { - // we did not generate a valid codePoint so insert a - // replacement char (U+FFFD) and advance only 1 byte - codePoint = 0xFFFD - bytesPerSequence = 1 - } else if (codePoint > 0xFFFF) { - // encode to utf16 (surrogate pair dance) - codePoint -= 0x10000 - res.push(codePoint >>> 10 & 0x3FF | 0xD800) - codePoint = 0xDC00 | codePoint & 0x3FF - } - - res.push(codePoint) - i += bytesPerSequence - } - - return decodeCodePointsArray(res) -} - -// Based on http://stackoverflow.com/a/22747272/680742, the browser with -// the lowest limit is Chrome, with 0x10000 args. -// We go 1 magnitude less, for safety -var MAX_ARGUMENTS_LENGTH = 0x1000 - -function decodeCodePointsArray (codePoints) { - var len = codePoints.length - if (len <= MAX_ARGUMENTS_LENGTH) { - return String.fromCharCode.apply(String, codePoints) // avoid extra slice() - } - - // Decode in chunks to avoid "call stack size exceeded". - var res = '' - var i = 0 - while (i < len) { - res += String.fromCharCode.apply( - String, - codePoints.slice(i, i += MAX_ARGUMENTS_LENGTH) - ) - } - return res -} - -function asciiSlice (buf, start, end) { - var ret = '' - end = Math.min(buf.length, end) - - for (var i = start; i < end; ++i) { - ret += String.fromCharCode(buf[i] & 0x7F) - } - return ret -} - -function latin1Slice (buf, start, end) { - var ret = '' - end = Math.min(buf.length, end) - - for (var i = start; i < end; ++i) { - ret += String.fromCharCode(buf[i]) - } - return ret -} - -function hexSlice (buf, start, end) { - var len = buf.length - - if (!start || start < 0) start = 0 - if (!end || end < 0 || end > len) end = len - - var out = '' - for (var i = start; i < end; ++i) { - out += toHex(buf[i]) - } - return out -} - -function utf16leSlice (buf, start, end) { - var bytes = buf.slice(start, end) - var res = '' - for (var i = 0; i < bytes.length; i += 2) { - res += String.fromCharCode(bytes[i] + (bytes[i + 1] * 256)) - } - return res -} - -Buffer.prototype.slice = function slice (start, end) { - var len = this.length - start = ~~start - end = end === undefined ? len : ~~end - - if (start < 0) { - start += len - if (start < 0) start = 0 - } else if (start > len) { - start = len - } - - if (end < 0) { - end += len - if (end < 0) end = 0 - } else if (end > len) { - end = len - } - - if (end < start) end = start - - var newBuf = this.subarray(start, end) - // Return an augmented `Uint8Array` instance - newBuf.__proto__ = Buffer.prototype - return newBuf -} - -/* - * Need to make sure that buffer isn't trying to write out of bounds. - */ -function checkOffset (offset, ext, length) { - if ((offset % 1) !== 0 || offset < 0) throw new RangeError('offset is not uint') - if (offset + ext > length) throw new RangeError('Trying to access beyond buffer length') -} - -Buffer.prototype.readUIntLE = function readUIntLE (offset, byteLength, noAssert) { - offset = offset >>> 0 - byteLength = byteLength >>> 0 - if (!noAssert) checkOffset(offset, byteLength, this.length) - - var val = this[offset] - var mul = 1 - var i = 0 - while (++i < byteLength && (mul *= 0x100)) { - val += this[offset + i] * mul - } - - return val -} - -Buffer.prototype.readUIntBE = function readUIntBE (offset, byteLength, noAssert) { - offset = offset >>> 0 - byteLength = byteLength >>> 0 - if (!noAssert) { - checkOffset(offset, byteLength, this.length) - } - - var val = this[offset + --byteLength] - var mul = 1 - while (byteLength > 0 && (mul *= 0x100)) { - val += this[offset + --byteLength] * mul - } - - return val -} - -Buffer.prototype.readUInt8 = function readUInt8 (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 1, this.length) - return this[offset] -} - -Buffer.prototype.readUInt16LE = function readUInt16LE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 2, this.length) - return this[offset] | (this[offset + 1] << 8) -} - -Buffer.prototype.readUInt16BE = function readUInt16BE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 2, this.length) - return (this[offset] << 8) | this[offset + 1] -} - -Buffer.prototype.readUInt32LE = function readUInt32LE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 4, this.length) - - return ((this[offset]) | - (this[offset + 1] << 8) | - (this[offset + 2] << 16)) + - (this[offset + 3] * 0x1000000) -} - -Buffer.prototype.readUInt32BE = function readUInt32BE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 4, this.length) - - return (this[offset] * 0x1000000) + - ((this[offset + 1] << 16) | - (this[offset + 2] << 8) | - this[offset + 3]) -} - -Buffer.prototype.readIntLE = function readIntLE (offset, byteLength, noAssert) { - offset = offset >>> 0 - byteLength = byteLength >>> 0 - if (!noAssert) checkOffset(offset, byteLength, this.length) - - var val = this[offset] - var mul = 1 - var i = 0 - while (++i < byteLength && (mul *= 0x100)) { - val += this[offset + i] * mul - } - mul *= 0x80 - - if (val >= mul) val -= Math.pow(2, 8 * byteLength) - - return val -} - -Buffer.prototype.readIntBE = function readIntBE (offset, byteLength, noAssert) { - offset = offset >>> 0 - byteLength = byteLength >>> 0 - if (!noAssert) checkOffset(offset, byteLength, this.length) - - var i = byteLength - var mul = 1 - var val = this[offset + --i] - while (i > 0 && (mul *= 0x100)) { - val += this[offset + --i] * mul - } - mul *= 0x80 - - if (val >= mul) val -= Math.pow(2, 8 * byteLength) - - return val -} - -Buffer.prototype.readInt8 = function readInt8 (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 1, this.length) - if (!(this[offset] & 0x80)) return (this[offset]) - return ((0xff - this[offset] + 1) * -1) -} - -Buffer.prototype.readInt16LE = function readInt16LE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 2, this.length) - var val = this[offset] | (this[offset + 1] << 8) - return (val & 0x8000) ? val | 0xFFFF0000 : val -} - -Buffer.prototype.readInt16BE = function readInt16BE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 2, this.length) - var val = this[offset + 1] | (this[offset] << 8) - return (val & 0x8000) ? val | 0xFFFF0000 : val -} - -Buffer.prototype.readInt32LE = function readInt32LE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 4, this.length) - - return (this[offset]) | - (this[offset + 1] << 8) | - (this[offset + 2] << 16) | - (this[offset + 3] << 24) -} - -Buffer.prototype.readInt32BE = function readInt32BE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 4, this.length) - - return (this[offset] << 24) | - (this[offset + 1] << 16) | - (this[offset + 2] << 8) | - (this[offset + 3]) -} - -Buffer.prototype.readFloatLE = function readFloatLE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 4, this.length) - return ieee754.read(this, offset, true, 23, 4) -} - -Buffer.prototype.readFloatBE = function readFloatBE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 4, this.length) - return ieee754.read(this, offset, false, 23, 4) -} - -Buffer.prototype.readDoubleLE = function readDoubleLE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 8, this.length) - return ieee754.read(this, offset, true, 52, 8) -} - -Buffer.prototype.readDoubleBE = function readDoubleBE (offset, noAssert) { - offset = offset >>> 0 - if (!noAssert) checkOffset(offset, 8, this.length) - return ieee754.read(this, offset, false, 52, 8) -} - -function checkInt (buf, value, offset, ext, max, min) { - if (!Buffer.isBuffer(buf)) throw new TypeError('"buffer" argument must be a Buffer instance') - if (value > max || value < min) throw new RangeError('"value" argument is out of bounds') - if (offset + ext > buf.length) throw new RangeError('Index out of range') -} - -Buffer.prototype.writeUIntLE = function writeUIntLE (value, offset, byteLength, noAssert) { - value = +value - offset = offset >>> 0 - byteLength = byteLength >>> 0 - if (!noAssert) { - var maxBytes = Math.pow(2, 8 * byteLength) - 1 - checkInt(this, value, offset, byteLength, maxBytes, 0) - } - - var mul = 1 - var i = 0 - this[offset] = value & 0xFF - while (++i < byteLength && (mul *= 0x100)) { - this[offset + i] = (value / mul) & 0xFF - } - - return offset + byteLength -} - -Buffer.prototype.writeUIntBE = function writeUIntBE (value, offset, byteLength, noAssert) { - value = +value - offset = offset >>> 0 - byteLength = byteLength >>> 0 - if (!noAssert) { - var maxBytes = Math.pow(2, 8 * byteLength) - 1 - checkInt(this, value, offset, byteLength, maxBytes, 0) - } - - var i = byteLength - 1 - var mul = 1 - this[offset + i] = value & 0xFF - while (--i >= 0 && (mul *= 0x100)) { - this[offset + i] = (value / mul) & 0xFF - } - - return offset + byteLength -} - -Buffer.prototype.writeUInt8 = function writeUInt8 (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 1, 0xff, 0) - this[offset] = (value & 0xff) - return offset + 1 -} - -Buffer.prototype.writeUInt16LE = function writeUInt16LE (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0) - this[offset] = (value & 0xff) - this[offset + 1] = (value >>> 8) - return offset + 2 -} - -Buffer.prototype.writeUInt16BE = function writeUInt16BE (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0) - this[offset] = (value >>> 8) - this[offset + 1] = (value & 0xff) - return offset + 2 -} - -Buffer.prototype.writeUInt32LE = function writeUInt32LE (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0) - this[offset + 3] = (value >>> 24) - this[offset + 2] = (value >>> 16) - this[offset + 1] = (value >>> 8) - this[offset] = (value & 0xff) - return offset + 4 -} - -Buffer.prototype.writeUInt32BE = function writeUInt32BE (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0) - this[offset] = (value >>> 24) - this[offset + 1] = (value >>> 16) - this[offset + 2] = (value >>> 8) - this[offset + 3] = (value & 0xff) - return offset + 4 -} - -Buffer.prototype.writeIntLE = function writeIntLE (value, offset, byteLength, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) { - var limit = Math.pow(2, (8 * byteLength) - 1) - - checkInt(this, value, offset, byteLength, limit - 1, -limit) - } - - var i = 0 - var mul = 1 - var sub = 0 - this[offset] = value & 0xFF - while (++i < byteLength && (mul *= 0x100)) { - if (value < 0 && sub === 0 && this[offset + i - 1] !== 0) { - sub = 1 - } - this[offset + i] = ((value / mul) >> 0) - sub & 0xFF - } - - return offset + byteLength -} - -Buffer.prototype.writeIntBE = function writeIntBE (value, offset, byteLength, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) { - var limit = Math.pow(2, (8 * byteLength) - 1) - - checkInt(this, value, offset, byteLength, limit - 1, -limit) - } - - var i = byteLength - 1 - var mul = 1 - var sub = 0 - this[offset + i] = value & 0xFF - while (--i >= 0 && (mul *= 0x100)) { - if (value < 0 && sub === 0 && this[offset + i + 1] !== 0) { - sub = 1 - } - this[offset + i] = ((value / mul) >> 0) - sub & 0xFF - } - - return offset + byteLength -} - -Buffer.prototype.writeInt8 = function writeInt8 (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 1, 0x7f, -0x80) - if (value < 0) value = 0xff + value + 1 - this[offset] = (value & 0xff) - return offset + 1 -} - -Buffer.prototype.writeInt16LE = function writeInt16LE (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000) - this[offset] = (value & 0xff) - this[offset + 1] = (value >>> 8) - return offset + 2 -} - -Buffer.prototype.writeInt16BE = function writeInt16BE (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000) - this[offset] = (value >>> 8) - this[offset + 1] = (value & 0xff) - return offset + 2 -} - -Buffer.prototype.writeInt32LE = function writeInt32LE (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000) - this[offset] = (value & 0xff) - this[offset + 1] = (value >>> 8) - this[offset + 2] = (value >>> 16) - this[offset + 3] = (value >>> 24) - return offset + 4 -} - -Buffer.prototype.writeInt32BE = function writeInt32BE (value, offset, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000) - if (value < 0) value = 0xffffffff + value + 1 - this[offset] = (value >>> 24) - this[offset + 1] = (value >>> 16) - this[offset + 2] = (value >>> 8) - this[offset + 3] = (value & 0xff) - return offset + 4 -} - -function checkIEEE754 (buf, value, offset, ext, max, min) { - if (offset + ext > buf.length) throw new RangeError('Index out of range') - if (offset < 0) throw new RangeError('Index out of range') -} - -function writeFloat (buf, value, offset, littleEndian, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) { - checkIEEE754(buf, value, offset, 4, 3.4028234663852886e+38, -3.4028234663852886e+38) - } - ieee754.write(buf, value, offset, littleEndian, 23, 4) - return offset + 4 -} - -Buffer.prototype.writeFloatLE = function writeFloatLE (value, offset, noAssert) { - return writeFloat(this, value, offset, true, noAssert) -} - -Buffer.prototype.writeFloatBE = function writeFloatBE (value, offset, noAssert) { - return writeFloat(this, value, offset, false, noAssert) -} - -function writeDouble (buf, value, offset, littleEndian, noAssert) { - value = +value - offset = offset >>> 0 - if (!noAssert) { - checkIEEE754(buf, value, offset, 8, 1.7976931348623157E+308, -1.7976931348623157E+308) - } - ieee754.write(buf, value, offset, littleEndian, 52, 8) - return offset + 8 -} - -Buffer.prototype.writeDoubleLE = function writeDoubleLE (value, offset, noAssert) { - return writeDouble(this, value, offset, true, noAssert) -} - -Buffer.prototype.writeDoubleBE = function writeDoubleBE (value, offset, noAssert) { - return writeDouble(this, value, offset, false, noAssert) -} - -// copy(targetBuffer, targetStart=0, sourceStart=0, sourceEnd=buffer.length) -Buffer.prototype.copy = function copy (target, targetStart, start, end) { - if (!Buffer.isBuffer(target)) throw new TypeError('argument should be a Buffer') - if (!start) start = 0 - if (!end && end !== 0) end = this.length - if (targetStart >= target.length) targetStart = target.length - if (!targetStart) targetStart = 0 - if (end > 0 && end < start) end = start - - // Copy 0 bytes; we're done - if (end === start) return 0 - if (target.length === 0 || this.length === 0) return 0 - - // Fatal error conditions - if (targetStart < 0) { - throw new RangeError('targetStart out of bounds') - } - if (start < 0 || start >= this.length) throw new RangeError('Index out of range') - if (end < 0) throw new RangeError('sourceEnd out of bounds') - - // Are we oob? - if (end > this.length) end = this.length - if (target.length - targetStart < end - start) { - end = target.length - targetStart + start - } - - var len = end - start - - if (this === target && typeof Uint8Array.prototype.copyWithin === 'function') { - // Use built-in when available, missing from IE11 - this.copyWithin(targetStart, start, end) - } else if (this === target && start < targetStart && targetStart < end) { - // descending copy from end - for (var i = len - 1; i >= 0; --i) { - target[i + targetStart] = this[i + start] - } - } else { - Uint8Array.prototype.set.call( - target, - this.subarray(start, end), - targetStart - ) - } - - return len -} - -// Usage: -// buffer.fill(number[, offset[, end]]) -// buffer.fill(buffer[, offset[, end]]) -// buffer.fill(string[, offset[, end]][, encoding]) -Buffer.prototype.fill = function fill (val, start, end, encoding) { - // Handle string cases: - if (typeof val === 'string') { - if (typeof start === 'string') { - encoding = start - start = 0 - end = this.length - } else if (typeof end === 'string') { - encoding = end - end = this.length - } - if (encoding !== undefined && typeof encoding !== 'string') { - throw new TypeError('encoding must be a string') - } - if (typeof encoding === 'string' && !Buffer.isEncoding(encoding)) { - throw new TypeError('Unknown encoding: ' + encoding) - } - if (val.length === 1) { - var code = val.charCodeAt(0) - if ((encoding === 'utf8' && code < 128) || - encoding === 'latin1') { - // Fast path: If `val` fits into a single byte, use that numeric value. - val = code - } - } - } else if (typeof val === 'number') { - val = val & 255 - } - - // Invalid ranges are not set to a default, so can range check early. - if (start < 0 || this.length < start || this.length < end) { - throw new RangeError('Out of range index') - } - - if (end <= start) { - return this - } - - start = start >>> 0 - end = end === undefined ? this.length : end >>> 0 - - if (!val) val = 0 - - var i - if (typeof val === 'number') { - for (i = start; i < end; ++i) { - this[i] = val - } - } else { - var bytes = Buffer.isBuffer(val) - ? val - : Buffer.from(val, encoding) - var len = bytes.length - if (len === 0) { - throw new TypeError('The value "' + val + - '" is invalid for argument "value"') - } - for (i = 0; i < end - start; ++i) { - this[i + start] = bytes[i % len] - } - } - - return this -} - -// HELPER FUNCTIONS -// ================ - -var INVALID_BASE64_RE = /[^+/0-9A-Za-z-_]/g - -function base64clean (str) { - // Node takes equal signs as end of the Base64 encoding - str = str.split('=')[0] - // Node strips out invalid characters like \n and \t from the string, base64-js does not - str = str.trim().replace(INVALID_BASE64_RE, '') - // Node converts strings with length < 2 to '' - if (str.length < 2) return '' - // Node allows for non-padded base64 strings (missing trailing ===), base64-js does not - while (str.length % 4 !== 0) { - str = str + '=' - } - return str -} - -function toHex (n) { - if (n < 16) return '0' + n.toString(16) - return n.toString(16) -} - -function utf8ToBytes (string, units) { - units = units || Infinity - var codePoint - var length = string.length - var leadSurrogate = null - var bytes = [] - - for (var i = 0; i < length; ++i) { - codePoint = string.charCodeAt(i) - - // is surrogate component - if (codePoint > 0xD7FF && codePoint < 0xE000) { - // last char was a lead - if (!leadSurrogate) { - // no lead yet - if (codePoint > 0xDBFF) { - // unexpected trail - if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) - continue - } else if (i + 1 === length) { - // unpaired lead - if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) - continue - } - - // valid lead - leadSurrogate = codePoint - - continue - } - - // 2 leads in a row - if (codePoint < 0xDC00) { - if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) - leadSurrogate = codePoint - continue - } - - // valid surrogate pair - codePoint = (leadSurrogate - 0xD800 << 10 | codePoint - 0xDC00) + 0x10000 - } else if (leadSurrogate) { - // valid bmp char, but last char was a lead - if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) - } - - leadSurrogate = null - - // encode utf8 - if (codePoint < 0x80) { - if ((units -= 1) < 0) break - bytes.push(codePoint) - } else if (codePoint < 0x800) { - if ((units -= 2) < 0) break - bytes.push( - codePoint >> 0x6 | 0xC0, - codePoint & 0x3F | 0x80 - ) - } else if (codePoint < 0x10000) { - if ((units -= 3) < 0) break - bytes.push( - codePoint >> 0xC | 0xE0, - codePoint >> 0x6 & 0x3F | 0x80, - codePoint & 0x3F | 0x80 - ) - } else if (codePoint < 0x110000) { - if ((units -= 4) < 0) break - bytes.push( - codePoint >> 0x12 | 0xF0, - codePoint >> 0xC & 0x3F | 0x80, - codePoint >> 0x6 & 0x3F | 0x80, - codePoint & 0x3F | 0x80 - ) - } else { - throw new Error('Invalid code point') - } - } - - return bytes -} - -function asciiToBytes (str) { - var byteArray = [] - for (var i = 0; i < str.length; ++i) { - // Node's code seems to be doing this and not & 0x7F.. - byteArray.push(str.charCodeAt(i) & 0xFF) - } - return byteArray -} - -function utf16leToBytes (str, units) { - var c, hi, lo - var byteArray = [] - for (var i = 0; i < str.length; ++i) { - if ((units -= 2) < 0) break - - c = str.charCodeAt(i) - hi = c >> 8 - lo = c % 256 - byteArray.push(lo) - byteArray.push(hi) - } - - return byteArray -} - -function base64ToBytes (str) { - return base64.toByteArray(base64clean(str)) -} - -function blitBuffer (src, dst, offset, length) { - for (var i = 0; i < length; ++i) { - if ((i + offset >= dst.length) || (i >= src.length)) break - dst[i + offset] = src[i] - } - return i -} - -// ArrayBuffer or Uint8Array objects from other contexts (i.e. iframes) do not pass -// the `instanceof` check but they should be treated as of that type. -// See: https://github.com/feross/buffer/issues/166 -function isInstance (obj, type) { - return obj instanceof type || - (obj != null && obj.constructor != null && obj.constructor.name != null && - obj.constructor.name === type.name) -} -function numberIsNaN (obj) { - // For IE11 support - return obj !== obj // eslint-disable-line no-self-compare -} - -},{"13":13,"6":6}],9:[function(_dereq_,module,exports){ -/*! codem-isoboxer v0.3.6 https://github.com/madebyhiro/codem-isoboxer/blob/master/LICENSE.txt */ -var ISOBoxer = {}; - -ISOBoxer.parseBuffer = function(arrayBuffer) { - return new ISOFile(arrayBuffer).parse(); -}; - -ISOBoxer.addBoxProcessor = function(type, parser) { - if (typeof type !== 'string' || typeof parser !== 'function') { - return; - } - ISOBox.prototype._boxProcessors[type] = parser; -}; - -ISOBoxer.createFile = function() { - return new ISOFile(); -}; - -// See ISOBoxer.append() for 'pos' parameter syntax -ISOBoxer.createBox = function(type, parent, pos) { - var newBox = ISOBox.create(type); - if (parent) { - parent.append(newBox, pos); - } - return newBox; -}; - -// See ISOBoxer.append() for 'pos' parameter syntax -ISOBoxer.createFullBox = function(type, parent, pos) { - var newBox = ISOBoxer.createBox(type, parent, pos); - newBox.version = 0; - newBox.flags = 0; - return newBox; -}; - -ISOBoxer.Utils = {}; -ISOBoxer.Utils.dataViewToString = function(dataView, encoding) { - var impliedEncoding = encoding || 'utf-8'; - if (typeof TextDecoder !== 'undefined') { - return new TextDecoder(impliedEncoding).decode(dataView); - } - var a = []; - var i = 0; - - if (impliedEncoding === 'utf-8') { - /* The following algorithm is essentially a rewrite of the UTF8.decode at - http://bannister.us/weblog/2007/simple-base64-encodedecode-javascript/ - */ - - while (i < dataView.byteLength) { - var c = dataView.getUint8(i++); - if (c < 0x80) { - // 1-byte character (7 bits) - } else if (c < 0xe0) { - // 2-byte character (11 bits) - c = (c & 0x1f) << 6; - c |= (dataView.getUint8(i++) & 0x3f); - } else if (c < 0xf0) { - // 3-byte character (16 bits) - c = (c & 0xf) << 12; - c |= (dataView.getUint8(i++) & 0x3f) << 6; - c |= (dataView.getUint8(i++) & 0x3f); - } else { - // 4-byte character (21 bits) - c = (c & 0x7) << 18; - c |= (dataView.getUint8(i++) & 0x3f) << 12; - c |= (dataView.getUint8(i++) & 0x3f) << 6; - c |= (dataView.getUint8(i++) & 0x3f); - } - a.push(String.fromCharCode(c)); - } - } else { // Just map byte-by-byte (probably wrong) - while (i < dataView.byteLength) { - a.push(String.fromCharCode(dataView.getUint8(i++))); - } - } - return a.join(''); -}; - -ISOBoxer.Utils.utf8ToByteArray = function(string) { - // Only UTF-8 encoding is supported by TextEncoder - var u, i; - if (typeof TextEncoder !== 'undefined') { - u = new TextEncoder().encode(string); - } else { - u = []; - for (i = 0; i < string.length; ++i) { - var c = string.charCodeAt(i); - if (c < 0x80) { - u.push(c); - } else if (c < 0x800) { - u.push(0xC0 | (c >> 6)); - u.push(0x80 | (63 & c)); - } else if (c < 0x10000) { - u.push(0xE0 | (c >> 12)); - u.push(0x80 | (63 & (c >> 6))); - u.push(0x80 | (63 & c)); - } else { - u.push(0xF0 | (c >> 18)); - u.push(0x80 | (63 & (c >> 12))); - u.push(0x80 | (63 & (c >> 6))); - u.push(0x80 | (63 & c)); - } - } - } - return u; -}; - -// Method to append a box in the list of child boxes -// The 'pos' parameter can be either: -// - (number) a position index at which to insert the new box -// - (string) the type of the box after which to insert the new box -// - (object) the box after which to insert the new box -ISOBoxer.Utils.appendBox = function(parent, box, pos) { - box._offset = parent._cursor.offset; - box._root = (parent._root ? parent._root : parent); - box._raw = parent._raw; - box._parent = parent; - - if (pos === -1) { - // The new box is a sub-box of the parent but not added in boxes array, - // for example when the new box is set as an entry (see dref and stsd for example) - return; - } - - if (pos === undefined || pos === null) { - parent.boxes.push(box); - return; - } - - var index = -1, - type; - - if (typeof pos === "number") { - index = pos; - } else { - if (typeof pos === "string") { - type = pos; - } else if (typeof pos === "object" && pos.type) { - type = pos.type; - } else { - parent.boxes.push(box); - return; - } - - for (var i = 0; i < parent.boxes.length; i++) { - if (type === parent.boxes[i].type) { - index = i + 1; - break; - } - } - } - parent.boxes.splice(index, 0, box); -}; - -if (typeof exports !== 'undefined') { - exports.parseBuffer = ISOBoxer.parseBuffer; - exports.addBoxProcessor = ISOBoxer.addBoxProcessor; - exports.createFile = ISOBoxer.createFile; - exports.createBox = ISOBoxer.createBox; - exports.createFullBox = ISOBoxer.createFullBox; - exports.Utils = ISOBoxer.Utils; -} - -ISOBoxer.Cursor = function(initialOffset) { - this.offset = (typeof initialOffset == 'undefined' ? 0 : initialOffset); -}; - -var ISOFile = function(arrayBuffer) { - this._cursor = new ISOBoxer.Cursor(); - this.boxes = []; - if (arrayBuffer) { - this._raw = new DataView(arrayBuffer); - } -}; - -ISOFile.prototype.fetch = function(type) { - var result = this.fetchAll(type, true); - return (result.length ? result[0] : null); -}; - -ISOFile.prototype.fetchAll = function(type, returnEarly) { - var result = []; - ISOFile._sweep.call(this, type, result, returnEarly); - return result; -}; - -ISOFile.prototype.parse = function() { - this._cursor.offset = 0; - this.boxes = []; - while (this._cursor.offset < this._raw.byteLength) { - var box = ISOBox.parse(this); - - // Box could not be parsed - if (typeof box.type === 'undefined') break; - - this.boxes.push(box); - } - return this; -}; - -ISOFile._sweep = function(type, result, returnEarly) { - if (this.type && this.type == type) result.push(this); - for (var box in this.boxes) { - if (result.length && returnEarly) return; - ISOFile._sweep.call(this.boxes[box], type, result, returnEarly); - } -}; - -ISOFile.prototype.write = function() { - - var length = 0, - i; - - for (i = 0; i < this.boxes.length; i++) { - length += this.boxes[i].getLength(false); - } - - var bytes = new Uint8Array(length); - this._rawo = new DataView(bytes.buffer); - this.bytes = bytes; - this._cursor.offset = 0; - - for (i = 0; i < this.boxes.length; i++) { - this.boxes[i].write(); - } - - return bytes.buffer; -}; - -ISOFile.prototype.append = function(box, pos) { - ISOBoxer.Utils.appendBox(this, box, pos); -}; -var ISOBox = function() { - this._cursor = new ISOBoxer.Cursor(); -}; - -ISOBox.parse = function(parent) { - var newBox = new ISOBox(); - newBox._offset = parent._cursor.offset; - newBox._root = (parent._root ? parent._root : parent); - newBox._raw = parent._raw; - newBox._parent = parent; - newBox._parseBox(); - parent._cursor.offset = newBox._raw.byteOffset + newBox._raw.byteLength; - return newBox; -}; - -ISOBox.create = function(type) { - var newBox = new ISOBox(); - newBox.type = type; - newBox.boxes = []; - return newBox; -}; - -ISOBox.prototype._boxContainers = ['dinf', 'edts', 'mdia', 'meco', 'mfra', 'minf', 'moof', 'moov', 'mvex', 'stbl', 'strk', 'traf', 'trak', 'tref', 'udta', 'vttc', 'sinf', 'schi', 'encv', 'enca']; - -ISOBox.prototype._boxProcessors = {}; - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// Generic read/write functions - -ISOBox.prototype._procField = function (name, type, size) { - if (this._parsing) { - this[name] = this._readField(type, size); - } - else { - this._writeField(type, size, this[name]); - } -}; - -ISOBox.prototype._procFieldArray = function (name, length, type, size) { - var i; - if (this._parsing) { - this[name] = []; - for (i = 0; i < length; i++) { - this[name][i] = this._readField(type, size); - } - } - else { - for (i = 0; i < this[name].length; i++) { - this._writeField(type, size, this[name][i]); - } - } -}; - -ISOBox.prototype._procFullBox = function() { - this._procField('version', 'uint', 8); - this._procField('flags', 'uint', 24); -}; - -ISOBox.prototype._procEntries = function(name, length, fn) { - var i; - if (this._parsing) { - this[name] = []; - for (i = 0; i < length; i++) { - this[name].push({}); - fn.call(this, this[name][i]); - } - } - else { - for (i = 0; i < length; i++) { - fn.call(this, this[name][i]); - } - } -}; - -ISOBox.prototype._procSubEntries = function(entry, name, length, fn) { - var i; - if (this._parsing) { - entry[name] = []; - for (i = 0; i < length; i++) { - entry[name].push({}); - fn.call(this, entry[name][i]); - } - } - else { - for (i = 0; i < length; i++) { - fn.call(this, entry[name][i]); - } - } -}; - -ISOBox.prototype._procEntryField = function (entry, name, type, size) { - if (this._parsing) { - entry[name] = this._readField(type, size); - } - else { - this._writeField(type, size, entry[name]); - } -}; - -ISOBox.prototype._procSubBoxes = function(name, length) { - var i; - if (this._parsing) { - this[name] = []; - for (i = 0; i < length; i++) { - this[name].push(ISOBox.parse(this)); - } - } - else { - for (i = 0; i < length; i++) { - if (this._rawo) { - this[name][i].write(); - } else { - this.size += this[name][i].getLength(); - } - } - } -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// Read/parse functions - -ISOBox.prototype._readField = function(type, size) { - switch (type) { - case 'uint': - return this._readUint(size); - case 'int': - return this._readInt(size); - case 'template': - return this._readTemplate(size); - case 'string': - return (size === -1) ? this._readTerminatedString() : this._readString(size); - case 'data': - return this._readData(size); - case 'utf8': - return this._readUTF8String(); - default: - return -1; - } -}; - -ISOBox.prototype._readInt = function(size) { - var result = null, - offset = this._cursor.offset - this._raw.byteOffset; - switch(size) { - case 8: - result = this._raw.getInt8(offset); - break; - case 16: - result = this._raw.getInt16(offset); - break; - case 32: - result = this._raw.getInt32(offset); - break; - case 64: - // Warning: JavaScript cannot handle 64-bit integers natively. - // This will give unexpected results for integers >= 2^53 - var s1 = this._raw.getInt32(offset); - var s2 = this._raw.getInt32(offset + 4); - result = (s1 * Math.pow(2,32)) + s2; - break; - } - this._cursor.offset += (size >> 3); - return result; -}; - -ISOBox.prototype._readUint = function(size) { - var result = null, - offset = this._cursor.offset - this._raw.byteOffset, - s1, s2; - switch(size) { - case 8: - result = this._raw.getUint8(offset); - break; - case 16: - result = this._raw.getUint16(offset); - break; - case 24: - s1 = this._raw.getUint16(offset); - s2 = this._raw.getUint8(offset + 2); - result = (s1 << 8) + s2; - break; - case 32: - result = this._raw.getUint32(offset); - break; - case 64: - // Warning: JavaScript cannot handle 64-bit integers natively. - // This will give unexpected results for integers >= 2^53 - s1 = this._raw.getUint32(offset); - s2 = this._raw.getUint32(offset + 4); - result = (s1 * Math.pow(2,32)) + s2; - break; - } - this._cursor.offset += (size >> 3); - return result; -}; - -ISOBox.prototype._readString = function(length) { - var str = ''; - for (var c = 0; c < length; c++) { - var char = this._readUint(8); - str += String.fromCharCode(char); - } - return str; -}; - -ISOBox.prototype._readTemplate = function(size) { - var pre = this._readUint(size / 2); - var post = this._readUint(size / 2); - return pre + (post / Math.pow(2, size / 2)); -}; - -ISOBox.prototype._readTerminatedString = function() { - var str = ''; - while (this._cursor.offset - this._offset < this._raw.byteLength) { - var char = this._readUint(8); - if (char === 0) break; - str += String.fromCharCode(char); - } - return str; -}; - -ISOBox.prototype._readData = function(size) { - var length = (size > 0) ? size : (this._raw.byteLength - (this._cursor.offset - this._offset)); - if (length > 0) { - var data = new Uint8Array(this._raw.buffer, this._cursor.offset, length); - - this._cursor.offset += length; - return data; - } - else { - return null; - } -}; - -ISOBox.prototype._readUTF8String = function() { - var length = this._raw.byteLength - (this._cursor.offset - this._offset); - var data = null; - if (length > 0) { - data = new DataView(this._raw.buffer, this._cursor.offset, length); - this._cursor.offset += length; - } - - return data ? ISOBoxer.Utils.dataViewToString(data) : data; -}; - -ISOBox.prototype._parseBox = function() { - this._parsing = true; - this._cursor.offset = this._offset; - - // return immediately if there are not enough bytes to read the header - if (this._offset + 8 > this._raw.buffer.byteLength) { - this._root._incomplete = true; - return; - } - - this._procField('size', 'uint', 32); - this._procField('type', 'string', 4); - - if (this.size === 1) { this._procField('largesize', 'uint', 64); } - if (this.type === 'uuid') { this._procFieldArray('usertype', 16, 'uint', 8); } - - switch(this.size) { - case 0: - this._raw = new DataView(this._raw.buffer, this._offset, (this._raw.byteLength - this._cursor.offset + 8)); - break; - case 1: - if (this._offset + this.size > this._raw.buffer.byteLength) { - this._incomplete = true; - this._root._incomplete = true; - } else { - this._raw = new DataView(this._raw.buffer, this._offset, this.largesize); - } - break; - default: - if (this._offset + this.size > this._raw.buffer.byteLength) { - this._incomplete = true; - this._root._incomplete = true; - } else { - this._raw = new DataView(this._raw.buffer, this._offset, this.size); - } - } - - // additional parsing - if (!this._incomplete) { - if (this._boxProcessors[this.type]) { - this._boxProcessors[this.type].call(this); - } - if (this._boxContainers.indexOf(this.type) !== -1) { - this._parseContainerBox(); - } else{ - // Unknown box => read and store box content - this._data = this._readData(); - } - } -}; - -ISOBox.prototype._parseFullBox = function() { - this.version = this._readUint(8); - this.flags = this._readUint(24); -}; - -ISOBox.prototype._parseContainerBox = function() { - this.boxes = []; - while (this._cursor.offset - this._raw.byteOffset < this._raw.byteLength) { - this.boxes.push(ISOBox.parse(this)); - } -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// Write functions - -ISOBox.prototype.append = function(box, pos) { - ISOBoxer.Utils.appendBox(this, box, pos); -}; - -ISOBox.prototype.getLength = function() { - this._parsing = false; - this._rawo = null; - - this.size = 0; - this._procField('size', 'uint', 32); - this._procField('type', 'string', 4); - - if (this.size === 1) { this._procField('largesize', 'uint', 64); } - if (this.type === 'uuid') { this._procFieldArray('usertype', 16, 'uint', 8); } - - if (this._boxProcessors[this.type]) { - this._boxProcessors[this.type].call(this); - } - - if (this._boxContainers.indexOf(this.type) !== -1) { - for (var i = 0; i < this.boxes.length; i++) { - this.size += this.boxes[i].getLength(); - } - } - - if (this._data) { - this._writeData(this._data); - } - - return this.size; -}; - -ISOBox.prototype.write = function() { - this._parsing = false; - this._cursor.offset = this._parent._cursor.offset; - - switch(this.size) { - case 0: - this._rawo = new DataView(this._parent._rawo.buffer, this._cursor.offset, (this.parent._rawo.byteLength - this._cursor.offset)); - break; - case 1: - this._rawo = new DataView(this._parent._rawo.buffer, this._cursor.offset, this.largesize); - break; - default: - this._rawo = new DataView(this._parent._rawo.buffer, this._cursor.offset, this.size); - } - - this._procField('size', 'uint', 32); - this._procField('type', 'string', 4); - - if (this.size === 1) { this._procField('largesize', 'uint', 64); } - if (this.type === 'uuid') { this._procFieldArray('usertype', 16, 'uint', 8); } - - if (this._boxProcessors[this.type]) { - this._boxProcessors[this.type].call(this); - } - - if (this._boxContainers.indexOf(this.type) !== -1) { - for (var i = 0; i < this.boxes.length; i++) { - this.boxes[i].write(); - } - } - - if (this._data) { - this._writeData(this._data); - } - - this._parent._cursor.offset += this.size; - - return this.size; -}; - -ISOBox.prototype._writeInt = function(size, value) { - if (this._rawo) { - var offset = this._cursor.offset - this._rawo.byteOffset; - switch(size) { - case 8: - this._rawo.setInt8(offset, value); - break; - case 16: - this._rawo.setInt16(offset, value); - break; - case 32: - this._rawo.setInt32(offset, value); - break; - case 64: - // Warning: JavaScript cannot handle 64-bit integers natively. - // This will give unexpected results for integers >= 2^53 - var s1 = Math.floor(value / Math.pow(2,32)); - var s2 = value - (s1 * Math.pow(2,32)); - this._rawo.setUint32(offset, s1); - this._rawo.setUint32(offset + 4, s2); - break; - } - this._cursor.offset += (size >> 3); - } else { - this.size += (size >> 3); - } -}; - -ISOBox.prototype._writeUint = function(size, value) { - - if (this._rawo) { - var offset = this._cursor.offset - this._rawo.byteOffset, - s1, s2; - switch(size) { - case 8: - this._rawo.setUint8(offset, value); - break; - case 16: - this._rawo.setUint16(offset, value); - break; - case 24: - s1 = (value & 0xFFFF00) >> 8; - s2 = (value & 0x0000FF); - this._rawo.setUint16(offset, s1); - this._rawo.setUint8(offset + 2, s2); - break; - case 32: - this._rawo.setUint32(offset, value); - break; - case 64: - // Warning: JavaScript cannot handle 64-bit integers natively. - // This will give unexpected results for integers >= 2^53 - s1 = Math.floor(value / Math.pow(2,32)); - s2 = value - (s1 * Math.pow(2,32)); - this._rawo.setUint32(offset, s1); - this._rawo.setUint32(offset + 4, s2); - break; - } - this._cursor.offset += (size >> 3); - } else { - this.size += (size >> 3); - } -}; - -ISOBox.prototype._writeString = function(size, str) { - for (var c = 0; c < size; c++) { - this._writeUint(8, str.charCodeAt(c)); - } -}; - -ISOBox.prototype._writeTerminatedString = function(str) { - if (str.length === 0) { - return; - } - for (var c = 0; c < str.length; c++) { - this._writeUint(8, str.charCodeAt(c)); - } - this._writeUint(8, 0); -}; - -ISOBox.prototype._writeTemplate = function(size, value) { - var pre = Math.floor(value); - var post = (value - pre) * Math.pow(2, size / 2); - this._writeUint(size / 2, pre); - this._writeUint(size / 2, post); -}; - -ISOBox.prototype._writeData = function(data) { - var i; - //data to copy - if (data) { - if (this._rawo) { - //Array and Uint8Array has also to be managed - if (data instanceof Array) { - var offset = this._cursor.offset - this._rawo.byteOffset; - for (var i = 0; i < data.length; i++) { - this._rawo.setInt8(offset + i, data[i]); - } - this._cursor.offset += data.length; - } - - if (data instanceof Uint8Array) { - this._root.bytes.set(data, this._cursor.offset); - this._cursor.offset += data.length; - } - - } else { - //nothing to copy only size to compute - this.size += data.length; - } - } -}; - -ISOBox.prototype._writeUTF8String = function(string) { - var u = ISOBoxer.Utils.utf8ToByteArray(string); - if (this._rawo) { - var dataView = new DataView(this._rawo.buffer, this._cursor.offset, u.length); - for (var i = 0; i < u.length; i++) { - dataView.setUint8(i, u[i]); - } - } else { - this.size += u.length; - } -}; - -ISOBox.prototype._writeField = function(type, size, value) { - switch (type) { - case 'uint': - this._writeUint(size, value); - break; - case 'int': - this._writeInt(size, value); - break; - case 'template': - this._writeTemplate(size, value); - break; - case 'string': - if (size == -1) { - this._writeTerminatedString(value); - } else { - this._writeString(size, value); - } - break; - case 'data': - this._writeData(value); - break; - case 'utf8': - this._writeUTF8String(value); - break; - default: - break; - } -}; - -// ISO/IEC 14496-15:2014 - avc1 box -ISOBox.prototype._boxProcessors['avc1'] = ISOBox.prototype._boxProcessors['encv'] = function() { - // SampleEntry fields - this._procFieldArray('reserved1', 6, 'uint', 8); - this._procField('data_reference_index', 'uint', 16); - // VisualSampleEntry fields - this._procField('pre_defined1', 'uint', 16); - this._procField('reserved2', 'uint', 16); - this._procFieldArray('pre_defined2', 3, 'uint', 32); - this._procField('width', 'uint', 16); - this._procField('height', 'uint', 16); - this._procField('horizresolution', 'template', 32); - this._procField('vertresolution', 'template', 32); - this._procField('reserved3', 'uint', 32); - this._procField('frame_count', 'uint', 16); - this._procFieldArray('compressorname', 32,'uint', 8); - this._procField('depth', 'uint', 16); - this._procField('pre_defined3', 'int', 16); - // AVCSampleEntry fields - this._procField('config', 'data', -1); -}; - -// ISO/IEC 14496-12:2012 - 8.7.2 Data Reference Box -ISOBox.prototype._boxProcessors['dref'] = function() { - this._procFullBox(); - this._procField('entry_count', 'uint', 32); - this._procSubBoxes('entries', this.entry_count); -}; - -// ISO/IEC 14496-12:2012 - 8.6.6 Edit List Box -ISOBox.prototype._boxProcessors['elst'] = function() { - this._procFullBox(); - this._procField('entry_count', 'uint', 32); - this._procEntries('entries', this.entry_count, function(entry) { - this._procEntryField(entry, 'segment_duration', 'uint', (this.version === 1) ? 64 : 32); - this._procEntryField(entry, 'media_time', 'int', (this.version === 1) ? 64 : 32); - this._procEntryField(entry, 'media_rate_integer', 'int', 16); - this._procEntryField(entry, 'media_rate_fraction', 'int', 16); - }); -}; - -// ISO/IEC 23009-1:2014 - 5.10.3.3 Event Message Box -ISOBox.prototype._boxProcessors['emsg'] = function() { - this._procFullBox(); - if (this.version == 1) { - this._procField('timescale', 'uint', 32); - this._procField('presentation_time', 'uint', 64); - this._procField('event_duration', 'uint', 32); - this._procField('id', 'uint', 32); - this._procField('scheme_id_uri', 'string', -1); - this._procField('value', 'string', -1); - } else { - this._procField('scheme_id_uri', 'string', -1); - this._procField('value', 'string', -1); - this._procField('timescale', 'uint', 32); - this._procField('presentation_time_delta', 'uint', 32); - this._procField('event_duration', 'uint', 32); - this._procField('id', 'uint', 32); - } - this._procField('message_data', 'data', -1); -}; -// ISO/IEC 14496-12:2012 - 8.1.2 Free Space Box -ISOBox.prototype._boxProcessors['free'] = ISOBox.prototype._boxProcessors['skip'] = function() { - this._procField('data', 'data', -1); -}; - -// ISO/IEC 14496-12:2012 - 8.12.2 Original Format Box -ISOBox.prototype._boxProcessors['frma'] = function() { - this._procField('data_format', 'uint', 32); -}; -// ISO/IEC 14496-12:2012 - 4.3 File Type Box / 8.16.2 Segment Type Box -ISOBox.prototype._boxProcessors['ftyp'] = -ISOBox.prototype._boxProcessors['styp'] = function() { - this._procField('major_brand', 'string', 4); - this._procField('minor_version', 'uint', 32); - var nbCompatibleBrands = -1; - if (this._parsing) { - nbCompatibleBrands = (this._raw.byteLength - (this._cursor.offset - this._raw.byteOffset)) / 4; - } - this._procFieldArray('compatible_brands', nbCompatibleBrands, 'string', 4); -}; - -// ISO/IEC 14496-12:2012 - 8.4.3 Handler Reference Box -ISOBox.prototype._boxProcessors['hdlr'] = function() { - this._procFullBox(); - this._procField('pre_defined', 'uint', 32); - this._procField('handler_type', 'string', 4); - this._procFieldArray('reserved', 3, 'uint', 32); - this._procField('name', 'string', -1); -}; - -// ISO/IEC 14496-12:2012 - 8.1.1 Media Data Box -ISOBox.prototype._boxProcessors['mdat'] = function() { - this._procField('data', 'data', -1); -}; - -// ISO/IEC 14496-12:2012 - 8.4.2 Media Header Box -ISOBox.prototype._boxProcessors['mdhd'] = function() { - this._procFullBox(); - this._procField('creation_time', 'uint', (this.version == 1) ? 64 : 32); - this._procField('modification_time', 'uint', (this.version == 1) ? 64 : 32); - this._procField('timescale', 'uint', 32); - this._procField('duration', 'uint', (this.version == 1) ? 64 : 32); - if (!this._parsing && typeof this.language === 'string') { - // In case of writing and language has been set as a string, then convert it into char codes array - this.language = ((this.language.charCodeAt(0) - 0x60) << 10) | - ((this.language.charCodeAt(1) - 0x60) << 5) | - ((this.language.charCodeAt(2) - 0x60)); - } - this._procField('language', 'uint', 16); - if (this._parsing) { - this.language = String.fromCharCode(((this.language >> 10) & 0x1F) + 0x60, - ((this.language >> 5) & 0x1F) + 0x60, - (this.language & 0x1F) + 0x60); - } - this._procField('pre_defined', 'uint', 16); -}; - -// ISO/IEC 14496-12:2012 - 8.8.2 Movie Extends Header Box -ISOBox.prototype._boxProcessors['mehd'] = function() { - this._procFullBox(); - this._procField('fragment_duration', 'uint', (this.version == 1) ? 64 : 32); -}; - -// ISO/IEC 14496-12:2012 - 8.8.5 Movie Fragment Header Box -ISOBox.prototype._boxProcessors['mfhd'] = function() { - this._procFullBox(); - this._procField('sequence_number', 'uint', 32); -}; - -// ISO/IEC 14496-12:2012 - 8.8.11 Movie Fragment Random Access Box -ISOBox.prototype._boxProcessors['mfro'] = function() { - this._procFullBox(); - this._procField('mfra_size', 'uint', 32); // Called mfra_size to distinguish from the normal "size" attribute of a box -}; - - -// ISO/IEC 14496-12:2012 - 8.5.2.2 mp4a box (use AudioSampleEntry definition and naming) -ISOBox.prototype._boxProcessors['mp4a'] = ISOBox.prototype._boxProcessors['enca'] = function() { - // SampleEntry fields - this._procFieldArray('reserved1', 6, 'uint', 8); - this._procField('data_reference_index', 'uint', 16); - // AudioSampleEntry fields - this._procFieldArray('reserved2', 2, 'uint', 32); - this._procField('channelcount', 'uint', 16); - this._procField('samplesize', 'uint', 16); - this._procField('pre_defined', 'uint', 16); - this._procField('reserved3', 'uint', 16); - this._procField('samplerate', 'template', 32); - // ESDescriptor fields - this._procField('esds', 'data', -1); -}; - -// ISO/IEC 14496-12:2012 - 8.2.2 Movie Header Box -ISOBox.prototype._boxProcessors['mvhd'] = function() { - this._procFullBox(); - this._procField('creation_time', 'uint', (this.version == 1) ? 64 : 32); - this._procField('modification_time', 'uint', (this.version == 1) ? 64 : 32); - this._procField('timescale', 'uint', 32); - this._procField('duration', 'uint', (this.version == 1) ? 64 : 32); - this._procField('rate', 'template', 32); - this._procField('volume', 'template', 16); - this._procField('reserved1', 'uint', 16); - this._procFieldArray('reserved2', 2, 'uint', 32); - this._procFieldArray('matrix', 9, 'template', 32); - this._procFieldArray('pre_defined', 6,'uint', 32); - this._procField('next_track_ID', 'uint', 32); -}; - -// ISO/IEC 14496-30:2014 - WebVTT Cue Payload Box. -ISOBox.prototype._boxProcessors['payl'] = function() { - this._procField('cue_text', 'utf8'); -}; - -//ISO/IEC 23001-7:2011 - 8.1 Protection System Specific Header Box -ISOBox.prototype._boxProcessors['pssh'] = function() { - this._procFullBox(); - - this._procFieldArray('SystemID', 16, 'uint', 8); - this._procField('DataSize', 'uint', 32); - this._procFieldArray('Data', this.DataSize, 'uint', 8); -}; -// ISO/IEC 14496-12:2012 - 8.12.5 Scheme Type Box -ISOBox.prototype._boxProcessors['schm'] = function() { - this._procFullBox(); - - this._procField('scheme_type', 'uint', 32); - this._procField('scheme_version', 'uint', 32); - - if (this.flags & 0x000001) { - this._procField('scheme_uri', 'string', -1); - } -}; -// ISO/IEC 14496-12:2012 - 8.6.4.1 sdtp box -ISOBox.prototype._boxProcessors['sdtp'] = function() { - this._procFullBox(); - - var sample_count = -1; - if (this._parsing) { - sample_count = (this._raw.byteLength - (this._cursor.offset - this._raw.byteOffset)); - } - - this._procFieldArray('sample_dependency_table', sample_count, 'uint', 8); -}; - -// ISO/IEC 14496-12:2012 - 8.16.3 Segment Index Box -ISOBox.prototype._boxProcessors['sidx'] = function() { - this._procFullBox(); - this._procField('reference_ID', 'uint', 32); - this._procField('timescale', 'uint', 32); - this._procField('earliest_presentation_time', 'uint', (this.version == 1) ? 64 : 32); - this._procField('first_offset', 'uint', (this.version == 1) ? 64 : 32); - this._procField('reserved', 'uint', 16); - this._procField('reference_count', 'uint', 16); - this._procEntries('references', this.reference_count, function(entry) { - if (!this._parsing) { - entry.reference = (entry.reference_type & 0x00000001) << 31; - entry.reference |= (entry.referenced_size & 0x7FFFFFFF); - entry.sap = (entry.starts_with_SAP & 0x00000001) << 31; - entry.sap |= (entry.SAP_type & 0x00000003) << 28; - entry.sap |= (entry.SAP_delta_time & 0x0FFFFFFF); - } - this._procEntryField(entry, 'reference', 'uint', 32); - this._procEntryField(entry, 'subsegment_duration', 'uint', 32); - this._procEntryField(entry, 'sap', 'uint', 32); - if (this._parsing) { - entry.reference_type = (entry.reference >> 31) & 0x00000001; - entry.referenced_size = entry.reference & 0x7FFFFFFF; - entry.starts_with_SAP = (entry.sap >> 31) & 0x00000001; - entry.SAP_type = (entry.sap >> 28) & 0x00000007; - entry.SAP_delta_time = (entry.sap & 0x0FFFFFFF); - } - }); -}; - -// ISO/IEC 14496-12:2012 - 8.4.5.3 Sound Media Header Box -ISOBox.prototype._boxProcessors['smhd'] = function() { - this._procFullBox(); - this._procField('balance', 'uint', 16); - this._procField('reserved', 'uint', 16); -}; - -// ISO/IEC 14496-12:2012 - 8.16.4 Subsegment Index Box -ISOBox.prototype._boxProcessors['ssix'] = function() { - this._procFullBox(); - this._procField('subsegment_count', 'uint', 32); - this._procEntries('subsegments', this.subsegment_count, function(subsegment) { - this._procEntryField(subsegment, 'ranges_count', 'uint', 32); - this._procSubEntries(subsegment, 'ranges', subsegment.ranges_count, function(range) { - this._procEntryField(range, 'level', 'uint', 8); - this._procEntryField(range, 'range_size', 'uint', 24); - }); - }); -}; - -// ISO/IEC 14496-12:2012 - 8.5.2 Sample Description Box -ISOBox.prototype._boxProcessors['stsd'] = function() { - this._procFullBox(); - this._procField('entry_count', 'uint', 32); - this._procSubBoxes('entries', this.entry_count); -}; - -// ISO/IEC 14496-12:2015 - 8.7.7 Sub-Sample Information Box -ISOBox.prototype._boxProcessors['subs'] = function () { - this._procFullBox(); - this._procField('entry_count', 'uint', 32); - this._procEntries('entries', this.entry_count, function(entry) { - this._procEntryField(entry, 'sample_delta', 'uint', 32); - this._procEntryField(entry, 'subsample_count', 'uint', 16); - this._procSubEntries(entry, 'subsamples', entry.subsample_count, function(subsample) { - this._procEntryField(subsample, 'subsample_size', 'uint', (this.version === 1) ? 32 : 16); - this._procEntryField(subsample, 'subsample_priority', 'uint', 8); - this._procEntryField(subsample, 'discardable', 'uint', 8); - this._procEntryField(subsample, 'codec_specific_parameters', 'uint', 32); - }); - }); -}; - -//ISO/IEC 23001-7:2011 - 8.2 Track Encryption Box -ISOBox.prototype._boxProcessors['tenc'] = function() { - this._procFullBox(); - - this._procField('default_IsEncrypted', 'uint', 24); - this._procField('default_IV_size', 'uint', 8); - this._procFieldArray('default_KID', 16, 'uint', 8); - }; - -// ISO/IEC 14496-12:2012 - 8.8.12 Track Fragmnent Decode Time -ISOBox.prototype._boxProcessors['tfdt'] = function() { - this._procFullBox(); - this._procField('baseMediaDecodeTime', 'uint', (this.version == 1) ? 64 : 32); -}; - -// ISO/IEC 14496-12:2012 - 8.8.7 Track Fragment Header Box -ISOBox.prototype._boxProcessors['tfhd'] = function() { - this._procFullBox(); - this._procField('track_ID', 'uint', 32); - if (this.flags & 0x01) this._procField('base_data_offset', 'uint', 64); - if (this.flags & 0x02) this._procField('sample_description_offset', 'uint', 32); - if (this.flags & 0x08) this._procField('default_sample_duration', 'uint', 32); - if (this.flags & 0x10) this._procField('default_sample_size', 'uint', 32); - if (this.flags & 0x20) this._procField('default_sample_flags', 'uint', 32); -}; - -// ISO/IEC 14496-12:2012 - 8.8.10 Track Fragment Random Access Box -ISOBox.prototype._boxProcessors['tfra'] = function() { - this._procFullBox(); - this._procField('track_ID', 'uint', 32); - if (!this._parsing) { - this.reserved = 0; - this.reserved |= (this.length_size_of_traf_num & 0x00000030) << 4; - this.reserved |= (this.length_size_of_trun_num & 0x0000000C) << 2; - this.reserved |= (this.length_size_of_sample_num & 0x00000003); - } - this._procField('reserved', 'uint', 32); - if (this._parsing) { - this.length_size_of_traf_num = (this.reserved & 0x00000030) >> 4; - this.length_size_of_trun_num = (this.reserved & 0x0000000C) >> 2; - this.length_size_of_sample_num = (this.reserved & 0x00000003); - } - this._procField('number_of_entry', 'uint', 32); - this._procEntries('entries', this.number_of_entry, function(entry) { - this._procEntryField(entry, 'time', 'uint', (this.version === 1) ? 64 : 32); - this._procEntryField(entry, 'moof_offset', 'uint', (this.version === 1) ? 64 : 32); - this._procEntryField(entry, 'traf_number', 'uint', (this.length_size_of_traf_num + 1) * 8); - this._procEntryField(entry, 'trun_number', 'uint', (this.length_size_of_trun_num + 1) * 8); - this._procEntryField(entry, 'sample_number', 'uint', (this.length_size_of_sample_num + 1) * 8); - }); -}; - -// ISO/IEC 14496-12:2012 - 8.3.2 Track Header Box -ISOBox.prototype._boxProcessors['tkhd'] = function() { - this._procFullBox(); - this._procField('creation_time', 'uint', (this.version == 1) ? 64 : 32); - this._procField('modification_time', 'uint', (this.version == 1) ? 64 : 32); - this._procField('track_ID', 'uint', 32); - this._procField('reserved1', 'uint', 32); - this._procField('duration', 'uint', (this.version == 1) ? 64 : 32); - this._procFieldArray('reserved2', 2, 'uint', 32); - this._procField('layer', 'uint', 16); - this._procField('alternate_group', 'uint', 16); - this._procField('volume', 'template', 16); - this._procField('reserved3', 'uint', 16); - this._procFieldArray('matrix', 9, 'template', 32); - this._procField('width', 'template', 32); - this._procField('height', 'template', 32); -}; - -// ISO/IEC 14496-12:2012 - 8.8.3 Track Extends Box -ISOBox.prototype._boxProcessors['trex'] = function() { - this._procFullBox(); - this._procField('track_ID', 'uint', 32); - this._procField('default_sample_description_index', 'uint', 32); - this._procField('default_sample_duration', 'uint', 32); - this._procField('default_sample_size', 'uint', 32); - this._procField('default_sample_flags', 'uint', 32); -}; - -// ISO/IEC 14496-12:2012 - 8.8.8 Track Run Box -// Note: the 'trun' box has a direct relation to the 'tfhd' box for defaults. -// These defaults are not set explicitly here, but are left to resolve for the user. -ISOBox.prototype._boxProcessors['trun'] = function() { - this._procFullBox(); - this._procField('sample_count', 'uint', 32); - if (this.flags & 0x1) this._procField('data_offset', 'int', 32); - if (this.flags & 0x4) this._procField('first_sample_flags', 'uint', 32); - this._procEntries('samples', this.sample_count, function(sample) { - if (this.flags & 0x100) this._procEntryField(sample, 'sample_duration', 'uint', 32); - if (this.flags & 0x200) this._procEntryField(sample, 'sample_size', 'uint', 32); - if (this.flags & 0x400) this._procEntryField(sample, 'sample_flags', 'uint', 32); - if (this.flags & 0x800) this._procEntryField(sample, 'sample_composition_time_offset', (this.version === 1) ? 'int' : 'uint', 32); - }); -}; - -// ISO/IEC 14496-12:2012 - 8.7.2 Data Reference Box -ISOBox.prototype._boxProcessors['url '] = ISOBox.prototype._boxProcessors['urn '] = function() { - this._procFullBox(); - if (this.type === 'urn ') { - this._procField('name', 'string', -1); - } - this._procField('location', 'string', -1); -}; - -// ISO/IEC 14496-30:2014 - WebVTT Source Label Box -ISOBox.prototype._boxProcessors['vlab'] = function() { - this._procField('source_label', 'utf8'); -}; - -// ISO/IEC 14496-12:2012 - 8.4.5.2 Video Media Header Box -ISOBox.prototype._boxProcessors['vmhd'] = function() { - this._procFullBox(); - this._procField('graphicsmode', 'uint', 16); - this._procFieldArray('opcolor', 3, 'uint', 16); -}; - -// ISO/IEC 14496-30:2014 - WebVTT Configuration Box -ISOBox.prototype._boxProcessors['vttC'] = function() { - this._procField('config', 'utf8'); -}; - -// ISO/IEC 14496-30:2014 - WebVTT Empty Sample Box -ISOBox.prototype._boxProcessors['vtte'] = function() { - // Nothing should happen here. -}; - -},{}],10:[function(_dereq_,module,exports){ -(function (Buffer){ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -// NOTE: These type checking functions intentionally don't use `instanceof` -// because it is fragile and can be easily faked with `Object.create()`. - -function isArray(arg) { - if (Array.isArray) { - return Array.isArray(arg); - } - return objectToString(arg) === '[object Array]'; -} -exports.isArray = isArray; - -function isBoolean(arg) { - return typeof arg === 'boolean'; -} -exports.isBoolean = isBoolean; - -function isNull(arg) { - return arg === null; -} -exports.isNull = isNull; - -function isNullOrUndefined(arg) { - return arg == null; -} -exports.isNullOrUndefined = isNullOrUndefined; - -function isNumber(arg) { - return typeof arg === 'number'; -} -exports.isNumber = isNumber; - -function isString(arg) { - return typeof arg === 'string'; -} -exports.isString = isString; - -function isSymbol(arg) { - return typeof arg === 'symbol'; -} -exports.isSymbol = isSymbol; - -function isUndefined(arg) { - return arg === void 0; -} -exports.isUndefined = isUndefined; - -function isRegExp(re) { - return objectToString(re) === '[object RegExp]'; -} -exports.isRegExp = isRegExp; - -function isObject(arg) { - return typeof arg === 'object' && arg !== null; -} -exports.isObject = isObject; - -function isDate(d) { - return objectToString(d) === '[object Date]'; -} -exports.isDate = isDate; - -function isError(e) { - return (objectToString(e) === '[object Error]' || e instanceof Error); -} -exports.isError = isError; - -function isFunction(arg) { - return typeof arg === 'function'; -} -exports.isFunction = isFunction; - -function isPrimitive(arg) { - return arg === null || - typeof arg === 'boolean' || - typeof arg === 'number' || - typeof arg === 'string' || - typeof arg === 'symbol' || // ES6 symbol - typeof arg === 'undefined'; -} -exports.isPrimitive = isPrimitive; - -exports.isBuffer = Buffer.isBuffer; - -function objectToString(o) { - return Object.prototype.toString.call(o); -} - -}).call(this,{"isBuffer":_dereq_(22)}) - -},{"22":22}],11:[function(_dereq_,module,exports){ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -var objectCreate = Object.create || objectCreatePolyfill -var objectKeys = Object.keys || objectKeysPolyfill -var bind = Function.prototype.bind || functionBindPolyfill - -function EventEmitter() { - if (!this._events || !Object.prototype.hasOwnProperty.call(this, '_events')) { - this._events = objectCreate(null); - this._eventsCount = 0; - } - - this._maxListeners = this._maxListeners || undefined; -} -module.exports = EventEmitter; - -// Backwards-compat with node 0.10.x -EventEmitter.EventEmitter = EventEmitter; - -EventEmitter.prototype._events = undefined; -EventEmitter.prototype._maxListeners = undefined; - -// By default EventEmitters will print a warning if more than 10 listeners are -// added to it. This is a useful default which helps finding memory leaks. -var defaultMaxListeners = 10; - -var hasDefineProperty; -try { - var o = {}; - if (Object.defineProperty) Object.defineProperty(o, 'x', { value: 0 }); - hasDefineProperty = o.x === 0; -} catch (err) { hasDefineProperty = false } -if (hasDefineProperty) { - Object.defineProperty(EventEmitter, 'defaultMaxListeners', { - enumerable: true, - get: function() { - return defaultMaxListeners; - }, - set: function(arg) { - // check whether the input is a positive number (whose value is zero or - // greater and not a NaN). - if (typeof arg !== 'number' || arg < 0 || arg !== arg) - throw new TypeError('"defaultMaxListeners" must be a positive number'); - defaultMaxListeners = arg; - } - }); -} else { - EventEmitter.defaultMaxListeners = defaultMaxListeners; -} - -// Obviously not all Emitters should be limited to 10. This function allows -// that to be increased. Set to zero for unlimited. -EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { - if (typeof n !== 'number' || n < 0 || isNaN(n)) - throw new TypeError('"n" argument must be a positive number'); - this._maxListeners = n; - return this; -}; - -function $getMaxListeners(that) { - if (that._maxListeners === undefined) - return EventEmitter.defaultMaxListeners; - return that._maxListeners; -} - -EventEmitter.prototype.getMaxListeners = function getMaxListeners() { - return $getMaxListeners(this); -}; - -// These standalone emit* functions are used to optimize calling of event -// handlers for fast cases because emit() itself often has a variable number of -// arguments and can be deoptimized because of that. These functions always have -// the same number of arguments and thus do not get deoptimized, so the code -// inside them can execute faster. -function emitNone(handler, isFn, self) { - if (isFn) - handler.call(self); - else { - var len = handler.length; - var listeners = arrayClone(handler, len); - for (var i = 0; i < len; ++i) - listeners[i].call(self); - } -} -function emitOne(handler, isFn, self, arg1) { - if (isFn) - handler.call(self, arg1); - else { - var len = handler.length; - var listeners = arrayClone(handler, len); - for (var i = 0; i < len; ++i) - listeners[i].call(self, arg1); - } -} -function emitTwo(handler, isFn, self, arg1, arg2) { - if (isFn) - handler.call(self, arg1, arg2); - else { - var len = handler.length; - var listeners = arrayClone(handler, len); - for (var i = 0; i < len; ++i) - listeners[i].call(self, arg1, arg2); - } -} -function emitThree(handler, isFn, self, arg1, arg2, arg3) { - if (isFn) - handler.call(self, arg1, arg2, arg3); - else { - var len = handler.length; - var listeners = arrayClone(handler, len); - for (var i = 0; i < len; ++i) - listeners[i].call(self, arg1, arg2, arg3); - } -} - -function emitMany(handler, isFn, self, args) { - if (isFn) - handler.apply(self, args); - else { - var len = handler.length; - var listeners = arrayClone(handler, len); - for (var i = 0; i < len; ++i) - listeners[i].apply(self, args); - } -} - -EventEmitter.prototype.emit = function emit(type) { - var er, handler, len, args, i, events; - var doError = (type === 'error'); - - events = this._events; - if (events) - doError = (doError && events.error == null); - else if (!doError) - return false; - - // If there is no 'error' event listener then throw. - if (doError) { - if (arguments.length > 1) - er = arguments[1]; - if (er instanceof Error) { - throw er; // Unhandled 'error' event - } else { - // At least give some kind of context to the user - var err = new Error('Unhandled "error" event. (' + er + ')'); - err.context = er; - throw err; - } - return false; - } - - handler = events[type]; - - if (!handler) - return false; - - var isFn = typeof handler === 'function'; - len = arguments.length; - switch (len) { - // fast cases - case 1: - emitNone(handler, isFn, this); - break; - case 2: - emitOne(handler, isFn, this, arguments[1]); - break; - case 3: - emitTwo(handler, isFn, this, arguments[1], arguments[2]); - break; - case 4: - emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]); - break; - // slower - default: - args = new Array(len - 1); - for (i = 1; i < len; i++) - args[i - 1] = arguments[i]; - emitMany(handler, isFn, this, args); - } - - return true; -}; - -function _addListener(target, type, listener, prepend) { - var m; - var events; - var existing; - - if (typeof listener !== 'function') - throw new TypeError('"listener" argument must be a function'); - - events = target._events; - if (!events) { - events = target._events = objectCreate(null); - target._eventsCount = 0; - } else { - // To avoid recursion in the case that type === "newListener"! Before - // adding it to the listeners, first emit "newListener". - if (events.newListener) { - target.emit('newListener', type, - listener.listener ? listener.listener : listener); - - // Re-assign `events` because a newListener handler could have caused the - // this._events to be assigned to a new object - events = target._events; - } - existing = events[type]; - } - - if (!existing) { - // Optimize the case of one listener. Don't need the extra array object. - existing = events[type] = listener; - ++target._eventsCount; - } else { - if (typeof existing === 'function') { - // Adding the second element, need to change to array. - existing = events[type] = - prepend ? [listener, existing] : [existing, listener]; - } else { - // If we've already got an array, just append. - if (prepend) { - existing.unshift(listener); - } else { - existing.push(listener); - } - } - - // Check for listener leak - if (!existing.warned) { - m = $getMaxListeners(target); - if (m && m > 0 && existing.length > m) { - existing.warned = true; - var w = new Error('Possible EventEmitter memory leak detected. ' + - existing.length + ' "' + String(type) + '" listeners ' + - 'added. Use emitter.setMaxListeners() to ' + - 'increase limit.'); - w.name = 'MaxListenersExceededWarning'; - w.emitter = target; - w.type = type; - w.count = existing.length; - if (typeof console === 'object' && console.warn) { - console.warn('%s: %s', w.name, w.message); - } - } - } - } - - return target; -} - -EventEmitter.prototype.addListener = function addListener(type, listener) { - return _addListener(this, type, listener, false); -}; - -EventEmitter.prototype.on = EventEmitter.prototype.addListener; - -EventEmitter.prototype.prependListener = - function prependListener(type, listener) { - return _addListener(this, type, listener, true); - }; - -function onceWrapper() { - if (!this.fired) { - this.target.removeListener(this.type, this.wrapFn); - this.fired = true; - switch (arguments.length) { - case 0: - return this.listener.call(this.target); - case 1: - return this.listener.call(this.target, arguments[0]); - case 2: - return this.listener.call(this.target, arguments[0], arguments[1]); - case 3: - return this.listener.call(this.target, arguments[0], arguments[1], - arguments[2]); - default: - var args = new Array(arguments.length); - for (var i = 0; i < args.length; ++i) - args[i] = arguments[i]; - this.listener.apply(this.target, args); - } - } -} - -function _onceWrap(target, type, listener) { - var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener }; - var wrapped = bind.call(onceWrapper, state); - wrapped.listener = listener; - state.wrapFn = wrapped; - return wrapped; -} - -EventEmitter.prototype.once = function once(type, listener) { - if (typeof listener !== 'function') - throw new TypeError('"listener" argument must be a function'); - this.on(type, _onceWrap(this, type, listener)); - return this; -}; - -EventEmitter.prototype.prependOnceListener = - function prependOnceListener(type, listener) { - if (typeof listener !== 'function') - throw new TypeError('"listener" argument must be a function'); - this.prependListener(type, _onceWrap(this, type, listener)); - return this; - }; - -// Emits a 'removeListener' event if and only if the listener was removed. -EventEmitter.prototype.removeListener = - function removeListener(type, listener) { - var list, events, position, i, originalListener; - - if (typeof listener !== 'function') - throw new TypeError('"listener" argument must be a function'); - - events = this._events; - if (!events) - return this; - - list = events[type]; - if (!list) - return this; - - if (list === listener || list.listener === listener) { - if (--this._eventsCount === 0) - this._events = objectCreate(null); - else { - delete events[type]; - if (events.removeListener) - this.emit('removeListener', type, list.listener || listener); - } - } else if (typeof list !== 'function') { - position = -1; - - for (i = list.length - 1; i >= 0; i--) { - if (list[i] === listener || list[i].listener === listener) { - originalListener = list[i].listener; - position = i; - break; - } - } - - if (position < 0) - return this; - - if (position === 0) - list.shift(); - else - spliceOne(list, position); - - if (list.length === 1) - events[type] = list[0]; - - if (events.removeListener) - this.emit('removeListener', type, originalListener || listener); - } - - return this; - }; - -EventEmitter.prototype.removeAllListeners = - function removeAllListeners(type) { - var listeners, events, i; - - events = this._events; - if (!events) - return this; - - // not listening for removeListener, no need to emit - if (!events.removeListener) { - if (arguments.length === 0) { - this._events = objectCreate(null); - this._eventsCount = 0; - } else if (events[type]) { - if (--this._eventsCount === 0) - this._events = objectCreate(null); - else - delete events[type]; - } - return this; - } - - // emit removeListener for all listeners on all events - if (arguments.length === 0) { - var keys = objectKeys(events); - var key; - for (i = 0; i < keys.length; ++i) { - key = keys[i]; - if (key === 'removeListener') continue; - this.removeAllListeners(key); - } - this.removeAllListeners('removeListener'); - this._events = objectCreate(null); - this._eventsCount = 0; - return this; - } - - listeners = events[type]; - - if (typeof listeners === 'function') { - this.removeListener(type, listeners); - } else if (listeners) { - // LIFO order - for (i = listeners.length - 1; i >= 0; i--) { - this.removeListener(type, listeners[i]); - } - } - - return this; - }; - -function _listeners(target, type, unwrap) { - var events = target._events; - - if (!events) - return []; - - var evlistener = events[type]; - if (!evlistener) - return []; - - if (typeof evlistener === 'function') - return unwrap ? [evlistener.listener || evlistener] : [evlistener]; - - return unwrap ? unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length); -} - -EventEmitter.prototype.listeners = function listeners(type) { - return _listeners(this, type, true); -}; - -EventEmitter.prototype.rawListeners = function rawListeners(type) { - return _listeners(this, type, false); -}; - -EventEmitter.listenerCount = function(emitter, type) { - if (typeof emitter.listenerCount === 'function') { - return emitter.listenerCount(type); - } else { - return listenerCount.call(emitter, type); - } -}; - -EventEmitter.prototype.listenerCount = listenerCount; -function listenerCount(type) { - var events = this._events; - - if (events) { - var evlistener = events[type]; - - if (typeof evlistener === 'function') { - return 1; - } else if (evlistener) { - return evlistener.length; - } - } - - return 0; -} - -EventEmitter.prototype.eventNames = function eventNames() { - return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : []; -}; - -// About 1.5x faster than the two-arg version of Array#splice(). -function spliceOne(list, index) { - for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) - list[i] = list[k]; - list.pop(); -} - -function arrayClone(arr, n) { - var copy = new Array(n); - for (var i = 0; i < n; ++i) - copy[i] = arr[i]; - return copy; -} - -function unwrapListeners(arr) { - var ret = new Array(arr.length); - for (var i = 0; i < ret.length; ++i) { - ret[i] = arr[i].listener || arr[i]; - } - return ret; -} - -function objectCreatePolyfill(proto) { - var F = function() {}; - F.prototype = proto; - return new F; -} -function objectKeysPolyfill(obj) { - var keys = []; - for (var k in obj) if (Object.prototype.hasOwnProperty.call(obj, k)) { - keys.push(k); - } - return k; -} -function functionBindPolyfill(context) { - var fn = this; - return function () { - return fn.apply(context, arguments); - }; -} - -},{}],12:[function(_dereq_,module,exports){ -'use strict'; - -var isArray = Array.isArray; -var keyList = Object.keys; -var hasProp = Object.prototype.hasOwnProperty; - -module.exports = function equal(a, b) { - if (a === b) return true; - - if (a && b && typeof a == 'object' && typeof b == 'object') { - var arrA = isArray(a) - , arrB = isArray(b) - , i - , length - , key; - - if (arrA && arrB) { - length = a.length; - if (length != b.length) return false; - for (i = length; i-- !== 0;) - if (!equal(a[i], b[i])) return false; - return true; - } - - if (arrA != arrB) return false; - - var dateA = a instanceof Date - , dateB = b instanceof Date; - if (dateA != dateB) return false; - if (dateA && dateB) return a.getTime() == b.getTime(); - - var regexpA = a instanceof RegExp - , regexpB = b instanceof RegExp; - if (regexpA != regexpB) return false; - if (regexpA && regexpB) return a.toString() == b.toString(); - - var keys = keyList(a); - length = keys.length; - - if (length !== keyList(b).length) - return false; - - for (i = length; i-- !== 0;) - if (!hasProp.call(b, keys[i])) return false; - - for (i = length; i-- !== 0;) { - key = keys[i]; - if (!equal(a[key], b[key])) return false; - } - - return true; - } - - return a!==a && b!==b; -}; - -},{}],13:[function(_dereq_,module,exports){ -exports.read = function (buffer, offset, isLE, mLen, nBytes) { - var e, m - var eLen = (nBytes * 8) - mLen - 1 - var eMax = (1 << eLen) - 1 - var eBias = eMax >> 1 - var nBits = -7 - var i = isLE ? (nBytes - 1) : 0 - var d = isLE ? -1 : 1 - var s = buffer[offset + i] - - i += d - - e = s & ((1 << (-nBits)) - 1) - s >>= (-nBits) - nBits += eLen - for (; nBits > 0; e = (e * 256) + buffer[offset + i], i += d, nBits -= 8) {} - - m = e & ((1 << (-nBits)) - 1) - e >>= (-nBits) - nBits += mLen - for (; nBits > 0; m = (m * 256) + buffer[offset + i], i += d, nBits -= 8) {} - - if (e === 0) { - e = 1 - eBias - } else if (e === eMax) { - return m ? NaN : ((s ? -1 : 1) * Infinity) - } else { - m = m + Math.pow(2, mLen) - e = e - eBias - } - return (s ? -1 : 1) * m * Math.pow(2, e - mLen) -} - -exports.write = function (buffer, value, offset, isLE, mLen, nBytes) { - var e, m, c - var eLen = (nBytes * 8) - mLen - 1 - var eMax = (1 << eLen) - 1 - var eBias = eMax >> 1 - var rt = (mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0) - var i = isLE ? 0 : (nBytes - 1) - var d = isLE ? 1 : -1 - var s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0 - - value = Math.abs(value) - - if (isNaN(value) || value === Infinity) { - m = isNaN(value) ? 1 : 0 - e = eMax - } else { - e = Math.floor(Math.log(value) / Math.LN2) - if (value * (c = Math.pow(2, -e)) < 1) { - e-- - c *= 2 - } - if (e + eBias >= 1) { - value += rt / c - } else { - value += rt * Math.pow(2, 1 - eBias) - } - if (value * c >= 2) { - e++ - c /= 2 - } - - if (e + eBias >= eMax) { - m = 0 - e = eMax - } else if (e + eBias >= 1) { - m = ((value * c) - 1) * Math.pow(2, mLen) - e = e + eBias - } else { - m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen) - e = 0 - } - } - - for (; mLen >= 8; buffer[offset + i] = m & 0xff, i += d, m /= 256, mLen -= 8) {} - - e = (e << mLen) | m - eLen += mLen - for (; eLen > 0; buffer[offset + i] = e & 0xff, i += d, e /= 256, eLen -= 8) {} - - buffer[offset + i - d] |= s * 128 -} - -},{}],14:[function(_dereq_,module,exports){ -/* - * Copyright (c) 2016, Pierre-Anthony Lemieux - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -/** - * @module imscDoc - */ - -; -(function (imscDoc, sax, imscNames, imscStyles, imscUtils) { - - - /** - * Allows a client to provide callbacks to handle children of the element - * @typedef {Object} MetadataHandler - * @property {?OpenTagCallBack} onOpenTag - * @property {?CloseTagCallBack} onCloseTag - * @property {?TextCallBack} onText - */ - - /** - * Called when the opening tag of an element node is encountered. - * @callback OpenTagCallBack - * @param {string} ns Namespace URI of the element - * @param {string} name Local name of the element - * @param {Object[]} attributes List of attributes, each consisting of a - * `uri`, `name` and `value` - */ - - /** - * Called when the closing tag of an element node is encountered. - * @callback CloseTagCallBack - */ - - /** - * Called when a text node is encountered. - * @callback TextCallBack - * @param {string} contents Contents of the text node - */ - - /** - * Parses an IMSC1 document into an opaque in-memory representation that exposes - * a single method
getMediaTimeEvents()
that returns a list of time - * offsets (in seconds) of the ISD, i.e. the points in time where the visual - * representation of the document change. `metadataHandler` allows the caller to - * be called back when nodes are present in elements. - * - * @param {string} xmlstring XML document - * @param {?module:imscUtils.ErrorHandler} errorHandler Error callback - * @param {?MetadataHandler} metadataHandler Callback for elements - * @returns {Object} Opaque in-memory representation of an IMSC1 document - */ - - imscDoc.fromXML = function (xmlstring, errorHandler, metadataHandler) { - var p = sax.parser(true, {xmlns: true}); - var estack = []; - var xmllangstack = []; - var xmlspacestack = []; - var metadata_depth = 0; - var doc = null; - - p.onclosetag = function (node) { - - if (estack[0] instanceof Styling) { - - /* flatten chained referential styling */ - - for (var sid in estack[0].styles) { - - mergeChainedStyles(estack[0], estack[0].styles[sid], errorHandler); - - } - - } else if (estack[0] instanceof P || estack[0] instanceof Span) { - - /* merge anonymous spans */ - - if (estack[0].contents.length > 1) { - - var cs = [estack[0].contents[0]]; - - var c; - - for (c = 1; c < estack[0].contents.length; c++) { - - if (estack[0].contents[c] instanceof AnonymousSpan && - cs[cs.length - 1] instanceof AnonymousSpan) { - - cs[cs.length - 1].text += estack[0].contents[c].text; - - } else { - - cs.push(estack[0].contents[c]); - - } - - } - - estack[0].contents = cs; - - } - - // remove redundant nested anonymous spans (9.3.3(1)(c)) - - if (estack[0] instanceof Span && - estack[0].contents.length === 1 && - estack[0].contents[0] instanceof AnonymousSpan) { - - estack[0].text = estack[0].contents[0].text; - delete estack[0].contents; - - } - - } else if (estack[0] instanceof ForeignElement) { - - if (estack[0].node.uri === imscNames.ns_tt && - estack[0].node.local === 'metadata') { - - /* leave the metadata element */ - - metadata_depth--; - - } else if (metadata_depth > 0 && - metadataHandler && - 'onCloseTag' in metadataHandler) { - - /* end of child of metadata element */ - - metadataHandler.onCloseTag(); - - } - - } - - // TODO: delete stylerefs? - - // maintain the xml:space stack - - xmlspacestack.shift(); - - // maintain the xml:lang stack - - xmllangstack.shift(); - - // prepare for the next element - - estack.shift(); - }; - - p.ontext = function (str) { - - if (estack[0] === undefined) { - - /* ignoring text outside of elements */ - - } else if (estack[0] instanceof Span || estack[0] instanceof P) { - - /* create an anonymous span */ - - var s = new AnonymousSpan(); - - s.initFromText(doc, estack[0], str, xmlspacestack[0], errorHandler); - - estack[0].contents.push(s); - - } else if (estack[0] instanceof ForeignElement && - metadata_depth > 0 && - metadataHandler && - 'onText' in metadataHandler) { - - /* text node within a child of metadata element */ - - metadataHandler.onText(str); - - } - - }; - - - p.onopentag = function (node) { - - // maintain the xml:space stack - - var xmlspace = node.attributes["xml:space"]; - - if (xmlspace) { - - xmlspacestack.unshift(xmlspace.value); - - } else { - - if (xmlspacestack.length === 0) { - - xmlspacestack.unshift("default"); - - } else { - - xmlspacestack.unshift(xmlspacestack[0]); - - } - - } - - /* maintain the xml:lang stack */ - - - var xmllang = node.attributes["xml:lang"]; - - if (xmllang) { - - xmllangstack.unshift(xmllang.value); - - } else { - - if (xmllangstack.length === 0) { - - xmllangstack.unshift(""); - - } else { - - xmllangstack.unshift(xmllangstack[0]); - - } - - } - - - /* process the element */ - - if (node.uri === imscNames.ns_tt) { - - if (node.local === 'tt') { - - if (doc !== null) { - - reportFatal(errorHandler, "Two elements at (" + this.line + "," + this.column + ")"); - - } - - doc = new TT(); - - doc.initFromNode(node, errorHandler); - - estack.unshift(doc); - - } else if (node.local === 'head') { - - if (!(estack[0] instanceof TT)) { - reportFatal(errorHandler, "Parent of element is not at (" + this.line + "," + this.column + ")"); - } - - if (doc.head !== null) { - reportFatal("Second element at (" + this.line + "," + this.column + ")"); - } - - doc.head = new Head(); - - estack.unshift(doc.head); - - } else if (node.local === 'styling') { - - if (!(estack[0] instanceof Head)) { - reportFatal(errorHandler, "Parent of element is not at (" + this.line + "," + this.column + ")"); - } - - if (doc.head.styling !== null) { - reportFatal("Second element at (" + this.line + "," + this.column + ")"); - } - - doc.head.styling = new Styling(); - - estack.unshift(doc.head.styling); - - } else if (node.local === 'style') { - - var s; - - if (estack[0] instanceof Styling) { - - s = new Style(); - - s.initFromNode(node, errorHandler); - - /* ignore + + + + + +
+
+
+ +
+
+
+
+

Configuration of ABR Rules

+

Example showing how to define the target ABR rules in dash.js. In this demo we enable the throughput based ABR decision logic. In addition, the InsufficientBufferRule is enabled.

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/abr/custom-abr-rules.html b/samples/abr/custom-abr-rules.html new file mode 100644 index 0000000000..469d29cae2 --- /dev/null +++ b/samples/abr/custom-abr-rules.html @@ -0,0 +1,87 @@ + + + + + Custom ABR Rules + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Custom ABR Rules

+

Example showing how to create and define custom ABR rules in dash.js.

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/abr/disable-abr.html b/samples/abr/disable-abr.html new file mode 100644 index 0000000000..8e283ce614 --- /dev/null +++ b/samples/abr/disable-abr.html @@ -0,0 +1,81 @@ + + + + + Disable ABR + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Disable ABR

+

Example showing how to disable the ABR switching in dash.js.

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/abr/fastswitch.html b/samples/abr/fastswitch.html new file mode 100644 index 0000000000..3514708305 --- /dev/null +++ b/samples/abr/fastswitch.html @@ -0,0 +1,86 @@ + + + + + Fast bitrate switch + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Fast bitrate switch

+

Example showing how to aggressively replace existing segments in the buffer when switching up in quality. + When fastswitch is enabled and an up-switch in quality is performed, the next fragment is not appended at the end of the current buffer range but closer to the current time. + +

Note, When ABR down-switch is detected, we appended the lower quality at the end of the buffer range to preserve the higher quality media for as long as possible.

+ +

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/abr/initial-bitrate.html b/samples/abr/initial-bitrate.html new file mode 100644 index 0000000000..207889621c --- /dev/null +++ b/samples/abr/initial-bitrate.html @@ -0,0 +1,82 @@ + + + + + Initial bitrate + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Initial bitrate

+

Example showing how to set the initial bitrate in dash.js. For visibility reasons the ABR switching is disabled as well.

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/abr/max-min-bitrate.html b/samples/abr/max-min-bitrate.html new file mode 100644 index 0000000000..26b326c672 --- /dev/null +++ b/samples/abr/max-min-bitrate.html @@ -0,0 +1,86 @@ + + + + + Max/min bitrate + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Max/min bitrate

+

Example showing how to set the maximum and minimum bitrate in dash.js. +

+

Note that the content used + in this sample has video representations with 4Mbit/s and 8Mbit/s. By setting the maximum + allowed bitrate to 5Mbit/s dash.js will never go above the 4Mbit/s representation.

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/advanced/abr/index.html b/samples/advanced/abr/index.html deleted file mode 100644 index 913fc5e14b..0000000000 --- a/samples/advanced/abr/index.html +++ /dev/null @@ -1,60 +0,0 @@ - - - - - Custom ABR Rules - - - - - - - - - - - - -
- -
- - - - - - diff --git a/samples/advanced/auto-play-browser-policy.html b/samples/advanced/auto-play-browser-policy.html index 747740e608..48744d619f 100644 --- a/samples/advanced/auto-play-browser-policy.html +++ b/samples/advanced/auto-play-browser-policy.html @@ -1,21 +1,29 @@ - + - + Autoplay browser policy example - - + + + + + + + + - - - -
- +
+
+
+ +
+
+
+
+

Autoplay browser policy example

+

This sample shows how to deal with autoplay browsers policy. It uses an event listener to detect + when auto playback is interrupted by the browser and how to recover from this situation muting + audio.

+
+
+
+ +
- - - +
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + diff --git a/samples/advanced/cmcd.html b/samples/advanced/cmcd.html index 1248924b19..905211b0fa 100644 --- a/samples/advanced/cmcd.html +++ b/samples/advanced/cmcd.html @@ -1,31 +1,44 @@ - + - - Manual-player instantiation example + + CMCD Reporting - - + + + + + + - + - -
- -
- + + +
+
+
+ +
+
+
+
+

CMCD Reporting

+

This sample shows how to use dash.js in order to enhance requests to the CDN with Common Media + Client Data (CMCD - CTA 5004).

+
+
- - - +
+
+ +
+
+
+ + +
+
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + diff --git a/samples/advanced/content-steering.html b/samples/advanced/content-steering.html new file mode 100644 index 0000000000..441ac937ef --- /dev/null +++ b/samples/advanced/content-steering.html @@ -0,0 +1,233 @@ + + + + + Content Steering + + + + + + + + + + + + + +
+
+
+ +
+
+

Content Steering

+
+
+

Description

+

Content distributors often use multiple Content Delivery Networks (CDNs) to + distribute their content to the end-users. They may upload a copy of their catalogue + to each CDN, or more commonly have all CDNs pull the content from a common + origin. Alternate URLs are generated, one for each CDN, that point at identical + content. DASH players may access alternate URLs in the event of delivery + problems.

+

Content steering describes a deterministic capability for a content + distributor to switch the content source that a player uses either at start-up or + midstream, by means of a remote steering service. The DASH implementation of + Content Steering also supports the notion of a proxy steering server which can + switch a mobile client between broadcast and unicast sources.

+
+
+
+
+

Architecture

+ Steering architecture illustration +
+
+
+
+
+
+ + +
+
+
+
+
+ +
+
+
+

CDN Selection

+ Steering alpha CDN + Steering alpha beta +
+
+
+
+
+
+

Fragment Requests

+ + + + + + + + + + + + + + + + + + + + +
TypeService LocationRequest URL
Audio
Video
+
+
+
+
+

Steering Data

+ + + + + + + + + + + + + + + + +
TimestampRequest URLResponse
+
    +
  • Version:
  • +
  • Reload URI:
  • +
  • Service Location Priority: +
  • +
  • TTL:
  • +
+
+
+
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/advanced/custom-capabilities-filters.html b/samples/advanced/custom-capabilities-filters.html index 7d0e929808..b145a819cd 100644 --- a/samples/advanced/custom-capabilities-filters.html +++ b/samples/advanced/custom-capabilities-filters.html @@ -1,13 +1,21 @@ - + - + Custom capabilities filter example - - + + + + + + - - -
- -
+ +
+
+
+ +
+
+
+
+

Custom capabilities filter example

+

This sample shows how to filter representations by defining a custom capabilities filter function.

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + diff --git a/samples/advanced/custom-initial-track-selection.html b/samples/advanced/custom-initial-track-selection.html new file mode 100644 index 0000000000..81a1405851 --- /dev/null +++ b/samples/advanced/custom-initial-track-selection.html @@ -0,0 +1,100 @@ + + + + + Custom initial track selection example + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Custom initial track selection example

+

This sample shows how to define your own initial track selection function. This can be useful to + select a specific AdaptationSet with a specific, desired codec.

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/advanced/extend.html b/samples/advanced/extend.html index fc127bff84..e8e86e93e5 100644 --- a/samples/advanced/extend.html +++ b/samples/advanced/extend.html @@ -1,13 +1,21 @@ - + - - Custom ABR Rules + + Extending dash.js - - + + + + + + - - -
- -
+ +
+
+
+ +
+
+
+
+

Extending dash.js

+

This sample shows how to use dash.js extend mechanism to add custom HTTP headers and modify URL's of the requests done by the player.

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + - - diff --git a/samples/advanced/img/content-steering-simulation-alpha-cdn.drawio.png b/samples/advanced/img/content-steering-simulation-alpha-cdn.drawio.png new file mode 100644 index 0000000000..33efd79071 Binary files /dev/null and b/samples/advanced/img/content-steering-simulation-alpha-cdn.drawio.png differ diff --git a/samples/advanced/img/content-steering-simulation-beta-cdn.drawio.png b/samples/advanced/img/content-steering-simulation-beta-cdn.drawio.png new file mode 100644 index 0000000000..66567e0ecd Binary files /dev/null and b/samples/advanced/img/content-steering-simulation-beta-cdn.drawio.png differ diff --git a/samples/advanced/img/steering.png b/samples/advanced/img/steering.png new file mode 100644 index 0000000000..c66d7873d6 Binary files /dev/null and b/samples/advanced/img/steering.png differ diff --git a/samples/advanced/listening-to-SCTE-EMSG-events.html b/samples/advanced/listening-to-SCTE-EMSG-events.html index 67ad5431a2..d73ed7fc0a 100644 --- a/samples/advanced/listening-to-SCTE-EMSG-events.html +++ b/samples/advanced/listening-to-SCTE-EMSG-events.html @@ -1,27 +1,41 @@ - + - - Events example + + Listening to SCTE-EMSG Events - - + + + + + + + + - - - -
- This sample catches and displays SCTE-35 events embedded inside EMSG boxes. The live sample stream being used contains embedded events at 10s and 40s in to each minute of media time. -

+

+
+
+ +
+
+
+
+

Listening to SCTE-EMSG Events

+

This sample catches and displays SCTE-35 events embedded inside EMSG boxes. The live sample stream being used contains embedded events at 10s and 40s in to each minute of media time.

+
+
-
- +
+
+ +
-
-
-

Received Events

- +
+
+
+ + +
+
+
+
+ + +
-
-

Started Events

- +
+
+
+
- - - +
+ © DASH-IF +
+
+
+ + + + + diff --git a/samples/advanced/load-with-starttime.html b/samples/advanced/load-with-starttime.html new file mode 100644 index 0000000000..0d9caa8936 --- /dev/null +++ b/samples/advanced/load-with-starttime.html @@ -0,0 +1,99 @@ + + + + + Manual load with start time + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Manual load with start time

+

A sample showing how to initialize playback at a specific start time. +

    +
  • For VoD content the start time is relative to the start time of the first period.
  • +
  • For live content +
      +
    • If the parameter starts from prefix + posix: it signifies the absolute time range defined in seconds of Coordinated + Universal Time + (ITU-R TF.460-6). This is the number of seconds since 01-01-1970 00:00:00 UTC. + Fractions of + seconds may be optionally specified down to the millisecond level. +
    • +
    • If no posix prefix is used the starttime is relative to MPD@availabilityStartTime
    • +
    +
  • +
+

+

In this example playback starts 60 seconds from the current wall clock time. +

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/advanced/load_with_manifest.html b/samples/advanced/load_with_manifest.html new file mode 100644 index 0000000000..1c092eb7be --- /dev/null +++ b/samples/advanced/load_with_manifest.html @@ -0,0 +1,2098 @@ + + + + + Load with a parsed manifest object + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Load with a parsed manifest object

+

This sample shows how to load the manifest as a parsed object instead of providing a url to the manifest.

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/advanced/monitoring.html b/samples/advanced/monitoring.html index a2b3503e75..6c0c6d201d 100644 --- a/samples/advanced/monitoring.html +++ b/samples/advanced/monitoring.html @@ -1,13 +1,28 @@ - + - + Monitoring stream example - + - - + + + + + + + + - - -
-
- -
-
- Reported bitrate: - -
- Buffer level: - -
- Calculated bitrate: - + +
+
+
+ +
+
+
+
+

Monitoring stream example

+

This example shows how to monitor metrics of the streams played by dash.js.

+
+
+
+
+
+
+
+ +
+
+
+
+
+

Metrics

+
+ Reported bitrate: + +
+ Buffer level: + +
+ Calculated bitrate: + +
+ Framerate: + +
+ Resolution: + +
+
+
- Framerate: - +
+
+
+
+
+
+ © DASH-IF +
-
+ - - diff --git a/samples/advanced/mpd-anchors.html b/samples/advanced/mpd-anchors.html new file mode 100644 index 0000000000..358ba31745 --- /dev/null +++ b/samples/advanced/mpd-anchors.html @@ -0,0 +1,73 @@ + + + + + MPD anchors + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

MPD anchors

+

This sample shows how to use MPD anchors to start a presentation at a given time. In this case a "#t=60" anchor is added to the MPD url and playback starts at 60 seconds.

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/advanced/settings.html b/samples/advanced/settings.html deleted file mode 100644 index 49b2fa0614..0000000000 --- a/samples/advanced/settings.html +++ /dev/null @@ -1,136 +0,0 @@ - - - - - Dash.js settings example - - - - - - - - - - - - -
-
- -
-
- Configuration parameters -
- Max stable buffer (seconds): (Current Buffer: 0 seconds) -
The time that the internal buffer target will be set to post startup/seeks (NOT top quality).
-
-
- Buffer lenght at top quality (seconds): -
The time that the internal buffer target will be set to once playing the top quality.
-
-
- Max selectable bitrate (Kbps): (Downloading bitrate: 0) -
The maximum bitrate selectable by the ABR algorithms will choose. Use -1 for no limit.
-
-
- Min selectable bitrate (Kbps): -
The minimum bitrate that the ABR algorithms will choose. Use -1 for no limit.
-
-
- Limit Bitrate By Portal Size: -
If true, the size of the video portal will limit the max chosen video resolution.
-
-
- -
-
-
- - - - - - diff --git a/samples/audio-only/index.html b/samples/audio-only/index.html index 5382a496b0..bad111b564 100644 --- a/samples/audio-only/index.html +++ b/samples/audio-only/index.html @@ -1,13 +1,21 @@ - + - + Audio only stream example - - + + + + + + -
- -
+ +
+
+
+ +
+
+
+
+

Audio only stream example

+

This example shows how to play audio-only streams in dash.js.

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + - - diff --git a/samples/buffer/buffer-cleanup.html b/samples/buffer/buffer-cleanup.html new file mode 100644 index 0000000000..947b66f396 --- /dev/null +++ b/samples/buffer/buffer-cleanup.html @@ -0,0 +1,87 @@ + + + + + Buffer cleanup + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Buffer cleanup

+

Example showing how to define the parameters for buffer cleanup/pruning in dash.js. The following settings are used:

+
    +
  • bufferPruningInterval:The interval (in seconds) in which the buffer is checked and pruned.
  • +
  • bufferToKeep:

    Defines the buffer that is kept in source buffer in seconds.

    +

    0|--bufferToPrune--|--bufferToKeep--|currentTime|

  • +
+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/buffer/buffer-target.html b/samples/buffer/buffer-target.html new file mode 100644 index 0000000000..1276654a29 --- /dev/null +++ b/samples/buffer/buffer-target.html @@ -0,0 +1,98 @@ + + + + + Buffer target + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Buffer target

+

Example showing how to define the buffer targets in dash.js. The following settings are used:

+
    +
  • stableBufferTime:The time that the internal buffer target will be set to post + startup/seeks (used when not playing at the top quality. See bufferTimeAtTopQuality). +
  • +
  • bufferTimeAtTopQuality:The time that the internal buffer target will be set to once + playing the top quality. +
  • +
  • bufferTimeAtTopQualityLongForm:The time that the internal buffer target will be set + to once playing the top quality for long form content. +
  • +
  • longFormContentDurationThreshold:The threshold which defines if the media is + considered long form content. +
  • +
+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/buffer/initial-buffer.html b/samples/buffer/initial-buffer.html new file mode 100644 index 0000000000..dd23a6b2c3 --- /dev/null +++ b/samples/buffer/initial-buffer.html @@ -0,0 +1,81 @@ + + + + + Buffer target + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Buffer target

+

Example showing how to define the initial buffer target at playback start in dash.js.

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/captioning/caption_vtt.html b/samples/captioning/caption_vtt.html index a7539868d9..35408f1996 100644 --- a/samples/captioning/caption_vtt.html +++ b/samples/captioning/caption_vtt.html @@ -1,27 +1,14 @@ - + - - WebVTT Dash Demo - - + + WebVTT example - - - + + + - -
- + + + + +
+
+
+ +
+
+
+
+

WebVTT Dash Demo

+

This example shows how content with VTT captions can be played back by the dash.js player. First + captions appear at the 15s mark.

+

Note: This sample will only work when using http as the subtitles are not hosted via https.

+
+
+
+ +
+
+
+
+
+
- - +
+ © DASH-IF +
+
+
+ + + + + diff --git a/samples/captioning/cea608.html b/samples/captioning/cea608.html new file mode 100644 index 0000000000..c0b69f5448 --- /dev/null +++ b/samples/captioning/cea608.html @@ -0,0 +1,119 @@ + + + + + CEA 608/708 Embedded Captions Sample + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

CEA 608/708 Embedded Captions Sample

+

This example shows how content with embedded CEA 608/708 captions can be played back by the dash.js player.

+
+
+
+
+
+ +
+
+
+ +
+ 00:00:00 +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+ 00:00:00 +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + diff --git a/samples/captioning/multi-track-captions.html b/samples/captioning/multi-track-captions.html index b5bbb9cdd0..0ead5ad820 100644 --- a/samples/captioning/multi-track-captions.html +++ b/samples/captioning/multi-track-captions.html @@ -1,16 +1,28 @@ - + - + Multiple Language Timed Text Sample - + + + + + + - - - - + + - - - - -
-
- -
- -
-
- -
- 00:00:00 -
- -
-
- -
- -
- -
-
- -
-
- -
- 00:00:00 -
-
-
-
+ + + +
+
+
+ +
+
+
+
+

Multiple Language Timed Text Sample

+

Example showing content with multiple timed text tracks.

+

The current texttrack settings can be saved in the local storage to be reused on the next stream startup.

+
+
+
+
+
+
+
+ +
+
+
+ +
+ 00:00:00 +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+ 00:00:00 +
+
+
+
+
+
-
-
- -
-
- - -
-
- - -
-
- - -
-
- -
- +
+
+

Settings

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+
- - +
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + diff --git a/samples/captioning/ttml-ebutt-sample.html b/samples/captioning/ttml-ebutt-sample.html index 663a3b4deb..a67a500f5c 100644 --- a/samples/captioning/ttml-ebutt-sample.html +++ b/samples/captioning/ttml-ebutt-sample.html @@ -1,17 +1,28 @@ - + - + Multiple Language EBU Timed Text Sample - + + + + + + - - - - + + + - - - -
-
- -
- -
-
- -
- 00:00:00 -
- -
-
- -
- -
- -
-
- -
-
- -
- 00:00:00 -
-
-
-
+
+
+
+ +
+
+
+
+

Multiple Language EBU Timed Text Sample

+

Example showing content with TTML EBU timed text tracks.

+
+
+
+
+
+ +
+
+
+ +
+ 00:00:00 +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+ 00:00:00 +
+
+
+
+
+
- - +
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + diff --git a/samples/chromecast/receiver/js/DashPlayerService.js b/samples/chromecast/receiver/js/DashPlayerService.js index 2add7b8333..964a133505 100644 --- a/samples/chromecast/receiver/js/DashPlayerService.js +++ b/samples/chromecast/receiver/js/DashPlayerService.js @@ -138,7 +138,7 @@ angular.module('DashCastReceiverApp.services', []) let allTracks = []; if (player && initialized) { let audioTracks = player.getTracksFor('audio'); - let textTracks = player.getTracksFor('fragmentedText'); + let textTracks = player.getTracksFor('text'); audioTracks.forEach(function (track) { allTracks.push(_convertTrack(track, cast.receiver.media.TrackType.AUDIO)); }); @@ -156,7 +156,7 @@ angular.module('DashCastReceiverApp.services', []) if (currentAudioTrack) { trackIds.push(currentAudioTrack.index); } - let currentTextTrack = player.getCurrentTrackFor('fragmentedText'); + let currentTextTrack = player.getCurrentTrackFor('text'); if (currentTextTrack) { trackIds.push(currentTextTrack.index); } @@ -192,7 +192,7 @@ angular.module('DashCastReceiverApp.services', []) if (audioTrack) { player.setCurrentTrack(audioTrack); } else { - let textTracks = player.getTracksFor('fragmentedText'); + let textTracks = player.getTracksFor('text'); textTrack = textTracks.find(function (track) { return track.index === activeTrackIds[i]; }); if (textTrack) { player.enableText(true); diff --git a/samples/control/controlbar.html b/samples/control/controlbar.html deleted file mode 100644 index 9d4a3b03db..0000000000 --- a/samples/control/controlbar.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - - Control bar Sample - - - - - - - - - - - -
- -
- -
-
- -
- 00:00:00 -
- -
-
- -
- -
- -
-
- -
-
- -
- 00:00:00 -
-
-
-
-
-
-
-
-
- - - - diff --git a/samples/control/logging.html b/samples/control/logging.html deleted file mode 100644 index fe57fbbb67..0000000000 --- a/samples/control/logging.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - - Logging to console example - - - - - - - - - - -
- -
- - - - diff --git a/samples/dash-if-reference-player/app/css/main.css b/samples/dash-if-reference-player/app/css/main.css index 1a7b2fd773..e021400bb4 100644 --- a/samples/dash-if-reference-player/app/css/main.css +++ b/samples/dash-if-reference-player/app/css/main.css @@ -4,7 +4,7 @@ body { overflow-x: hidden; } -label input { +label input select { display: inline; } @@ -95,14 +95,16 @@ a:hover { } .options-item-body input, -.options-item-body label { +.options-item-body label, +.options-item-body select { cursor: pointer; clear: both; float: left; margin-bottom: 2px !important; } -.options-item-body input { +.options-item-body input, +.options-item-body select { margin-right: 5px; } @@ -589,4 +591,108 @@ Conformance warnings margin-top: 15px; margin-bottom: 15px; padding: 12px; -} \ No newline at end of file +} + +/******************************************************** +DRM Header Dialogue +*********************************************************/ + +.requestHeaderDialogue { + display: none; + position: fixed; + z-index: 1; + padding-top: 100px; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + text-align: center; + background-color: rgb(255, 255, 255); + background-color: rgba(255, 255, 255, 0.4); +} + +.requestHeaderDialogueContent { + display: inline-block; + background-color: #fefefe; + padding: 20px; + border: 1px solid #888; + width: 80%; /* Could be more or less, depending on screen size */ +} + +.dialogue-btn{ + margin-bottom: 2px; +} + +.modal-hr { + border: none; + height: 1px; + color: #333; + background-color: #333; +} + +.close { + color: rgb(255, 0, 0); + float: right; + font-size: 28px; + font-weight: bold; +} + +.close:hover, +.close:focus { + color: black; + text-decoration: none; + cursor: pointer; +} + +/******************************************************** +Copy Settings URL Popup +*********************************************************/ + +.copyPopup { + display: none; + position: fixed; + z-index: 1; + top: 0; + right: 0; + overflow: auto; + text-align: center; +} + +.copyPopupContent { + display: inline-block; + background-color: #fefefe; + padding: 5px; + border: 1px solid #888; + font-size: large; + width: 100%; /* Could be more or less, depending on screen size */ +} + +/******************************************************** +BS Information +*********************************************************/ + +.bs-callout { + padding: 20px; + margin: 20px 0; + border: 1px solid #eee; + border-left-width: 5px; + border-radius: 3px; + background: #FFFFFF; +} + +.bs-callout-information { + border-left-color: #136bfb ; +} + +.bs-callout-success { + border-left-color: #5cb85c ; +} + +.bs-callout-warning { + border-left-color: #f0ad4e ; +} + +.bs-callout-danger { + border-left-color: #CA0B00; +} diff --git a/samples/dash-if-reference-player/app/main.js b/samples/dash-if-reference-player/app/main.js index 12d4d3d4da..adeff185a6 100644 --- a/samples/dash-if-reference-player/app/main.js +++ b/samples/dash-if-reference-player/app/main.js @@ -75,6 +75,8 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' $scope.contributors = data.items; }); + + /* ======= Chart related stuff ======= */ $scope.chartOptions = { legend: { labelBoxBorderColor: '#ffffff', @@ -126,13 +128,11 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' }, yaxes: [] }; - $scope.chartEnabled = true; $scope.maxPointsToChart = 30; $scope.maxChartableItems = 5; $scope.chartCount = 0; $scope.chartData = []; - $scope.chartState = { audio: { buffer: { data: [], selected: false, color: '#65080c', label: 'Audio Buffer Level' }, @@ -140,10 +140,11 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' index: { data: [], selected: false, color: '#ffd446', label: 'Audio Current Index' }, pendingIndex: { data: [], selected: false, color: '#FF6700', label: 'AudioPending Index' }, ratio: { data: [], selected: false, color: '#329d61', label: 'Audio Ratio' }, - download: { data: [], selected: false, color: '#44c248', label: 'Audio Download Rate (Mbps)' }, + download: { data: [], selected: false, color: '#44c248', label: 'Audio Download Time (sec)' }, latency: { data: [], selected: false, color: '#326e88', label: 'Audio Latency (ms)' }, droppedFPS: { data: [], selected: false, color: '#004E64', label: 'Audio Dropped FPS' }, - liveLatency: { data: [], selected: false, color: '#65080c', label: 'Live Latency' } + liveLatency: { data: [], selected: false, color: '#65080c', label: 'Live Latency' }, + playbackRate: { data: [], selected: false, color: '#65080c', label: 'Playback Rate' } }, video: { buffer: { data: [], selected: true, color: '#00589d', label: 'Video Buffer Level' }, @@ -151,13 +152,15 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' index: { data: [], selected: false, color: '#326e88', label: 'Video Current Quality' }, pendingIndex: { data: [], selected: false, color: '#44c248', label: 'Video Pending Index' }, ratio: { data: [], selected: false, color: '#00CCBE', label: 'Video Ratio' }, - download: { data: [], selected: false, color: '#FF6700', label: 'Video Download Rate (Mbps)' }, + download: { data: [], selected: false, color: '#FF6700', label: 'Video Download Time (sec)' }, latency: { data: [], selected: false, color: '#329d61', label: 'Video Latency (ms)' }, droppedFPS: { data: [], selected: false, color: '#65080c', label: 'Video Dropped FPS' }, - liveLatency: { data: [], selected: false, color: '#65080c', label: 'Live Latency' } + liveLatency: { data: [], selected: false, color: '#65080c', label: 'Live Latency' }, + playbackRate: { data: [], selected: false, color: '#65080c', label: 'Playback Rate' } } }; + /* ======= General ======= */ $scope.abrEnabled = true; $scope.toggleCCBubble = false; $scope.debugEnabled = false; @@ -173,21 +176,82 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' textEnabled: true, forceTextStreaming: false }; + $scope.additionalAbrRules = {}; $scope.mediaSettingsCacheEnabled = true; $scope.metricsTimer = null; $scope.updateMetricsInterval = 1000; - $scope.drmKeySystems = ['com.widevine.alpha', 'com.microsoft.playready']; + $scope.drmKeySystems = ['com.widevine.alpha', 'com.microsoft.playready', 'org.w3.clearkey']; $scope.drmKeySystem = ''; $scope.drmLicenseURL = ''; + $scope.drmRequestHeader = ''; + + + $scope.protectionData = {}; + $scope.prioritiesEnabled = false; + + $scope.drmPlayready = { + isActive: false, + drmKeySystem: 'com.microsoft.playready', + licenseServerUrl: '', + httpRequestHeaders: {}, + priority: 1 + } + + $scope.drmWidevine = { + isActive: false, + drmKeySystem: 'com.widevine.alpha', + licenseServerUrl: '', + httpRequestHeaders: {}, + priority: 0 + } + + $scope.drmClearkey = { + isActive: false, + drmKeySystem: 'org.w3.clearkey', + licenseServerUrl: '', + httpRequestHeaders: {}, + kid: '', + key: '', + clearkeys: {}, + inputMode: 'kidKey', + priority: 2 + } + + $scope.playreadyRequestHeaders = []; + + $scope.widevineRequestHeaders = []; + + $scope.clearkeyRequestHeaders = []; + + $scope.additionalClearkeyPairs = []; + + $scope.protData = {}; + + $scope.drmToday = false; $scope.isDynamic = false; $scope.conformanceViolations = []; + var defaultExternalSettings = { + mpd: encodeURIComponent('https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd'), + loop: true, + autoPlay: true, + drmToday: false, + forceQualitySwitchSelected: false, + drmPrioritiesEnabled: false, + languageAudio: null, + roleVideo: null, + languageText: null, + roleText: undefined, + forceTextStreaming: false + } + // metrics $scope.videoBitrate = 0; $scope.videoIndex = 0; $scope.videoPendingIndex = 0; + $scope.videoPendingMaxIndex = 0; $scope.videoMaxIndex = 0; $scope.videoBufferLength = 0; $scope.videoDroppedFrames = 0; @@ -198,10 +262,12 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' $scope.videoRatioCount = 0; $scope.videoRatio = ''; $scope.videoLiveLatency = 0; + $scope.videoPlaybackRate = 1.00; $scope.audioBitrate = 0; $scope.audioIndex = 0; - $scope.audioPendingIndex = ''; + $scope.audioPendingIndex = 0; + $scope.audioPendingMaxIndex = 0; $scope.audioMaxIndex = 0; $scope.audioBufferLength = 0; $scope.audioDroppedFrames = 0; @@ -212,6 +278,7 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' $scope.audioRatioCount = 0; $scope.audioRatio = ''; $scope.audioLiveLatency = 0; + $scope.audioPlaybackRate = 1.00; // Starting Options $scope.autoPlaySelected = true; @@ -223,10 +290,22 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' $scope.localStorageSelected = true; $scope.jumpGapsSelected = true; $scope.fastSwitchSelected = true; + $scope.applyServiceDescription = true; + $scope.applyContentSteering = true; + $scope.useSuggestedPresentationDelay = true; $scope.videoAutoSwitchSelected = true; + $scope.forceQualitySwitchSelected = false; $scope.videoQualities = []; $scope.ABRStrategy = 'abrDynamic'; + $scope.liveCatchupMode = 'liveCatchupModeDefault'; + $scope.abrThroughputCalculationMode = 'abrFetchThroughputCalculationMoofParsing'; + $scope.videoTrackSwitchMode = 'alwaysReplace'; + $scope.audioTrackSwitchMode = 'neverReplace'; + $scope.currentLogLevel = 'info'; + $scope.cmcdMode = 'query'; + $scope.cmcdAllKeys = ['br', 'd', 'ot', 'tb', 'bl', 'dl', 'mtp', 'nor', 'nrr', 'su', 'bs', 'rtp', 'cid', 'pr', 'sf', 'sid', 'st', 'v'] + // Persistent license $scope.persistentSessionId = {}; $scope.selectedKeySystem = null; @@ -249,45 +328,10 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' // store a ref in window.player to provide an easy way to play with dash.js API window.player = $scope.player = dashjs.MediaPlayer().create(); /* jshint ignore:line */ - //////////////////////////////////////// - // - // Configuration file - // - //////////////////////////////////////// - let reqConfig = new XMLHttpRequest(); - reqConfig.onload = function () { - if (reqConfig.status === 200) { - let config = JSON.parse(reqConfig.responseText); - if ($scope.player) { - $scope.player.updateSettings(config); - setLatencyAttributes(); - } - } else { - // Set default initial configuration - var initialConfig = { - 'debug': { - 'logLevel': dashjs.Debug.LOG_LEVEL_INFO - }, - 'streaming': { - 'fastSwitchEnabled': $scope.fastSwitchSelected, - 'jumpGaps': true, - 'abr': { - 'autoSwitchBitrate': { - 'video': $scope.videoAutoSwitchSelected - } - } - } - }; - $scope.player.updateSettings(initialConfig); - setLatencyAttributes(); - } - }; - - reqConfig.open("GET", "dashjs_config.json", true); - reqConfig.setRequestHeader("Content-type", "application/json"); - reqConfig.send(); + const defaultSettings = JSON.parse(JSON.stringify($scope.player.getSettings())); $scope.player.on(dashjs.MediaPlayer.events.ERROR, function (e) { /* jshint ignore:line */ + console.log(e); if (!e.event) { $scope.$apply(function () { $scope.error = e.error.message; @@ -296,8 +340,6 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' case dashjs.MediaPlayer.errors.MANIFEST_LOADER_PARSING_FAILURE_ERROR_CODE: case dashjs.MediaPlayer.errors.MANIFEST_LOADER_LOADING_FAILURE_ERROR_CODE: case dashjs.MediaPlayer.errors.XLINK_LOADER_LOADING_FAILURE_ERROR_CODE: - case dashjs.MediaPlayer.errors.SEGMENTS_UPDATE_FAILED_ERROR_CODE: - case dashjs.MediaPlayer.errors.SEGMENTS_UNAVAILABLE_ERROR_CODE: case dashjs.MediaPlayer.errors.SEGMENT_BASE_LOADER_ERROR_CODE: case dashjs.MediaPlayer.errors.TIME_SYNC_FAILED_ERROR_CODE: case dashjs.MediaPlayer.errors.FRAGMENT_LOADER_LOADING_FAILURE_ERROR_CODE: @@ -326,17 +368,14 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' break; } }); - $("#errorModal").modal('show'); + $('#errorModal').modal('show'); } }, $scope); $scope.player.initialize($scope.video, null, $scope.autoPlaySelected); + $scope.player.attachTTMLRenderingDiv($('#video-caption')[0]); - // Add HTML-rendered TTML subtitles except for Firefox < v49 (issue #1164) - if (doesTimeMarchesOn()) { - $scope.player.attachTTMLRenderingDiv($('#video-caption')[0]); - } var currentConfig = $scope.player.getSettings(); @@ -365,26 +404,30 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' $scope.isDynamic = e.data.type === 'dynamic'; }, $scope); - $scope.player.on(dashjs.MediaPlayer.events.QUALITY_CHANGE_REQUESTED, function (e) { /* jshint ignore:line */ - $scope[e.mediaType + 'Index'] = e.oldQuality + 1; - $scope[e.mediaType + 'PendingIndex'] = e.newQuality + 1; + + $scope.player.on(dashjs.MediaPlayer.events.REPRESENTATION_SWITCH, function (e) { + var bitrate = Math.round(e.currentRepresentation.bandwidth / 1000); + + $scope[e.mediaType + 'PendingIndex'] = e.currentRepresentation.index + 1; + $scope[e.mediaType + 'PendingMaxIndex'] = e.numberOfRepresentations; + $scope[e.mediaType + 'Bitrate'] = bitrate; $scope.plotPoint('pendingIndex', e.mediaType, e.newQuality + 1, getTimeForPlot()); $scope.safeApply(); }, $scope); + + $scope.player.on(dashjs.MediaPlayer.events.PERIOD_SWITCH_COMPLETED, function (e) { /* jshint ignore:line */ + $scope.currentStreamInfo = e.toStreamInfo; + }, $scope); + $scope.player.on(dashjs.MediaPlayer.events.QUALITY_CHANGE_RENDERED, function (e) { /* jshint ignore:line */ $scope[e.mediaType + 'Index'] = e.newQuality + 1; $scope.plotPoint('index', e.mediaType, e.newQuality + 1, getTimeForPlot()); $scope.safeApply(); }, $scope); - $scope.player.on(dashjs.MediaPlayer.events.PERIOD_SWITCH_COMPLETED, function (e) { /* jshint ignore:line */ - $scope.streamInfo = e.toStreamInfo; - }, $scope); - $scope.player.on(dashjs.MediaPlayer.events.STREAM_INITIALIZED, function (e) { /* jshint ignore:line */ stopMetricsInterval(); - $scope.videoQualities = $scope.player.getBitrateInfoListFor('video'); $scope.chartCount = 0; $scope.metricsTimer = setInterval(function () { @@ -411,7 +454,7 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' if (e.data) { var session = e.data; if (session.getSessionType() === 'persistent-license') { - $scope.persistentSessionId[$scope.selectedItem.url] = session.getSessionID(); + $scope.persistentSessionId[$scope.selectedItem.url] = session.getSessionId(); } } }, $scope); @@ -422,7 +465,7 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' return violation.event.key === e.event.key; }) - if(!existingViolation || existingViolation.length === 0) { + if (!existingViolation || existingViolation.length === 0) { $scope.conformanceViolations.push(e); } } @@ -467,7 +510,9 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' $scope.changeABRStrategy = function (strategy) { $scope.player.updateSettings({ streaming: { - stallThreshold: 0.5, + buffer: { + stallThreshold: 0.5 + }, abr: { ABRStrategy: strategy } @@ -477,7 +522,9 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' if (strategy === 'abrLoLP') { $scope.player.updateSettings({ streaming: { - stallThreshold: 0.05 + buffer: { + stallThreshold: 0.05 + } } }); $scope.changeFetchThroughputCalculation('abrFetchThroughputCalculationMoofParsing'); @@ -509,7 +556,35 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' $scope.toggleFastSwitch = function () { $scope.player.updateSettings({ 'streaming': { - 'fastSwitchEnabled': $scope.fastSwitchSelected + 'buffer': { + 'fastSwitchEnabled': $scope.fastSwitchSelected + } + } + }); + }; + + $scope.toggleApplyServiceDescription = function () { + $scope.player.updateSettings({ + streaming: { + applyServiceDescription: $scope.applyServiceDescription + } + }); + }; + + $scope.toggleApplyContentSteering = function () { + $scope.player.updateSettings({ + streaming: { + applyContentSteering: $scope.applyContentSteering + } + }); + }; + + $scope.toggleUseSuggestedPresentationDelay = function () { + $scope.player.updateSettings({ + streaming: { + delay: { + useSuggestedPresentationDelay: $scope.useSuggestedPresentationDelay + } } }); }; @@ -526,26 +601,51 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' }); }; + $scope.toggleForceQualitySwitch = function () { + $scope.controlbar.forceQualitySwitch($scope.forceQualitySwitchSelected); + }; + + $scope.toggleBufferRule = function () { + $scope.player.updateSettings({ + streaming: { + abr: { + additionalAbrRules: { + insufficientBufferRule: $scope.additionalAbrRules.insufficientBufferRule, + switchHistoryRule: $scope.additionalAbrRules.switchHistoryRule, + droppedFramesRule: $scope.additionalAbrRules.droppedFramesRule, + abandonRequestsRule: $scope.additionalAbrRules.abandonRequestsRule, + } + } + } + }); + }; + $scope.toggleScheduleWhilePaused = function () { $scope.player.updateSettings({ 'streaming': { - 'scheduleWhilePaused': $scope.scheduleWhilePausedSelected + 'scheduling': { + 'scheduleWhilePaused': $scope.scheduleWhilePausedSelected + } } }); }; $scope.toggleCalcSegmentAvailabilityRangeFromTimeline = function () { $scope.player.updateSettings({ - 'streaming': { - 'calcSegmentAvailabilityRangeFromTimeline': $scope.calcSegmentAvailabilityRangeFromTimelineSelected + streaming: { + timeShiftBuffer: { + calcFromSegmentTimeline: $scope.calcSegmentAvailabilityRangeFromTimelineSelected + } } }); }; $scope.toggleReuseExistingSourceBuffers = function () { $scope.player.updateSettings({ - 'streaming': { - 'reuseExistingSourceBuffers': $scope.reuseExistingSourceBuffersSelected + streaming: { + buffer: { + reuseExistingSourceBuffers: $scope.reuseExistingSourceBuffersSelected + } } }); }; @@ -566,33 +666,209 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' $scope.toggleJumpGaps = function () { $scope.player.updateSettings({ 'streaming': { - 'jumpGaps': $scope.jumpGapsSelected + 'gaps': { + 'jumpGaps': $scope.jumpGapsSelected + } } }); }; - $scope.togglelowLatencyMode = function () { + $scope.toggleLiveCatchupEnabled = function () { $scope.player.updateSettings({ - 'streaming': { - 'lowLatencyEnabled': $scope.lowLatencyModeSelected + streaming: { + liveCatchup: { + enabled: $scope.liveCatchupEnabled + } } }); }; - $scope.toggleLiveCatchupEnabled = function () { + $scope.updateInitialLiveDelay = function () { $scope.player.updateSettings({ streaming: { - liveCatchup: { - enabled: $scope.liveCatchupEnabled + delay: { + liveDelay: parseInt($scope.initialLiveDelay) + } + } + }); + }; + + $scope.updateLiveDelayFragmentCount = function () { + $scope.player.updateSettings({ + streaming: { + delay: { + liveDelayFragmentCount: parseInt($scope.liveDelayFragmentCount) + } + } + }); + }; + + $scope.updateInitialBitrateVideo = function () { + $scope.player.updateSettings({ + streaming: { + abr: { + initialBitrate: { + video: parseInt($scope.initialVideoBitrate) + } + } + } + }); + }; + + $scope.updateMinimumBitrateVideo = function () { + $scope.player.updateSettings({ + streaming: { + abr: { + minBitrate: { + video: parseInt($scope.minVideoBitrate) + } } } }); }; + $scope.updateMaximumBitrateVideo = function () { + $scope.player.updateSettings({ + streaming: { + abr: { + maxBitrate: { + video: parseInt($scope.maxVideoBitrate) + } + } + } + }); + }; + + $scope.updateInitialLanguageAudio = function () { + $scope.player.setInitialMediaSettingsFor('audio', { + lang: $scope.initialSettings.audio + }); + }; + + $scope.updateInitialRoleVideo = function () { + $scope.player.setInitialMediaSettingsFor('video', { + role: $scope.initialSettings.video + }); + }; + + $scope.updateInitialLanguageText = function () { + $scope.player.setInitialMediaSettingsFor('text', { + lang: $scope.initialSettings.text + }); + }; + + $scope.updateInitialRoleText = function () { + $scope.player.setInitialMediaSettingsFor('text', { + role: $scope.initialSettings.textRole + }); + }; + + $scope.toggleText = function () { + $scope.player.updateSettings({ streaming: { text: { defaultEnabled: $scope.initialSettings.textEnabled } } }); + } + + $scope.toggleForcedTextStreaming = function () { + $scope.player.enableForcedTextStreaming($scope.initialSettings.forceTextStreaming); + } + + $scope.updateCmcdSessionId = function () { + $scope.player.updateSettings({ + streaming: { + cmcd: { + sid: $scope.cmcdSessionId + } + } + }); + } + + $scope.updateCmcdContentId = function () { + $scope.player.updateSettings({ + streaming: { + cmcd: { + cid: $scope.cmcdContentId + } + } + }); + } + + $scope.updateCmcdRtp = function () { + $scope.player.updateSettings({ + streaming: { + cmcd: { + rtp: $scope.cmcdRtp + } + } + }); + } + + $scope.updateCmcdRtpSafetyFactor = function () { + $scope.player.updateSettings({ + streaming: { + cmcd: { + rtpSafetyFactor: $scope.cmcdRtpSafetyFactor + } + } + }); + } + + $scope._getFormatedCmcdEnabledKeys = function () { + let formatedKeys; + if (!Array.isArray($scope.cmcdEnabledKeys)) { + let cmcdEnabledKeys = $scope.cmcdEnabledKeys.split(','); + formatedKeys = $scope.cmcdAllKeys.map(key => { + let mappedKey = key; + if (!cmcdEnabledKeys.includes(key)) mappedKey = ''; + + return mappedKey; + }); + } else { + formatedKeys = $scope.cmcdEnabledKeys; + } + + return formatedKeys + } + + $scope.updateCmcdEnabledKeys = function () { + let cmcdEnabledKeys = $scope._getFormatedCmcdEnabledKeys(); + + $scope.player.updateSettings({ + streaming: { + cmcd: { + enabledKeys: cmcdEnabledKeys + } + } + }); + } + $scope.setStream = function (item) { $scope.selectedItem = JSON.parse(JSON.stringify(item)); + $scope.protData = {}; + //Reset previous data + $scope.clearDRM(); + // Execute if the loaded video already has preset DRM data + if ($scope.selectedItem.hasOwnProperty('protData')) { + $scope.protData = $scope.selectedItem.protData; + // Handle preset protection data to be reflected in the UI and work with setDrm() + $scope.handleProtectionData($scope.protData); + } }; + $scope.clearDRM = function () { + //Reset previous data + let drmList = [$scope.drmPlayready, $scope.drmWidevine, $scope.drmClearkey]; + for (let drm of drmList) { + drm.isActive = false; + drm.licenseServerUrl = ''; + drm.kid = ''; + drm.key = ''; + } + $scope.playreadyRequestHeaders = []; + $scope.widevineRequestHeaders = []; + $scope.clearkeyRequestHeaders = []; + $scope.clearkeys = []; + $scope.additionalClearkeyPairs = []; + } + $scope.toggleOptionsGutter = function (bool) { $scope.optionsGutter = bool; }; @@ -607,43 +883,50 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' }); }; - $scope.selectVideoQuality = function (quality) { - $scope.player.setQualityFor('video', quality); - }; - $scope.doLoad = function () { $scope.initSession(); - var protData = {}; + // Execute if the loaded video already has preset DRM data if ($scope.selectedItem.hasOwnProperty('protData')) { - protData = $scope.selectedItem.protData; + + // Set DRM options + $scope.setDrm(); + $scope.protData = $scope.protectionData; + } + // Execute if setDrm() has been called with manually entered values + else if ($scope.protectionData !== {}) { + $scope.setDrm(); + $scope.protData = $scope.protectionData; } else if ($scope.drmLicenseURL !== '' && $scope.drmKeySystem !== '') { - protData[$scope.drmKeySystem] = { + $scope.protData[$scope.drmKeySystem] = { serverURL: $scope.drmLicenseURL }; } else { - protData = null; + $scope.protData = null; } // Check if persistent license session ID is stored for current stream var sessionId = $scope.persistentSessionId[$scope.selectedItem.url]; if (sessionId) { - if (!protData) { - protData = {}; + if (!$scope.protData) { + $scope.protData = {}; } - if (!protData[$scope.selectedKeySystem]) { - protData[$scope.selectedKeySystem] = {}; + if (!$scope.protData[$scope.selectedKeySystem]) { + $scope.protData[$scope.selectedKeySystem] = {}; } - protData[$scope.selectedKeySystem].sessionId = sessionId; + $scope.protData[$scope.selectedKeySystem].sessionId = sessionId; } var config = { - 'streaming': { - 'liveDelay': $scope.defaultLiveDelay, - 'stableBufferTime': $scope.defaultStableBufferDelay, - 'bufferTimeAtTopQuality': $scope.defaultBufferTimeAtTopQuality, - 'bufferTimeAtTopQualityLongForm': $scope.defaultBufferTimeAtTopQualityLongForm, - 'lowLatencyEnabled': $scope.lowLatencyModeSelected, + streaming: { + buffer: { + stableBufferTime: $scope.defaultStableBufferDelay, + bufferTimeAtTopQuality: $scope.defaultBufferTimeAtTopQuality, + bufferTimeAtTopQualityLongForm: $scope.defaultBufferTimeAtTopQualityLongForm, + }, + delay: { + liveDelay: $scope.defaultLiveDelay + }, abr: {}, cmcd: {} } @@ -653,29 +936,31 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' var selectedConfig = $scope.selectedItem.bufferConfig; if (selectedConfig.liveDelay) { - config.streaming.liveDelay = selectedConfig.liveDelay; + config.streaming.delay.liveDelay = selectedConfig.liveDelay; } if (selectedConfig.stableBufferTime) { - config.streaming.stableBufferTime = selectedConfig.stableBufferTime; + config.streaming.buffer.stableBufferTime = selectedConfig.stableBufferTime; } if (selectedConfig.bufferTimeAtTopQuality) { - config.streaming.bufferTimeAtTopQuality = selectedConfig.bufferTimeAtTopQuality; + config.streaming.buffer.bufferTimeAtTopQuality = selectedConfig.bufferTimeAtTopQuality; } if (selectedConfig.bufferTimeAtTopQualityLongForm) { - config.streaming.bufferTimeAtTopQualityLongForm = selectedConfig.bufferTimeAtTopQualityLongForm; + config.streaming.buffer.bufferTimeAtTopQualityLongForm = selectedConfig.bufferTimeAtTopQualityLongForm; } - if (selectedConfig.lowLatencyMode !== undefined) { - config.streaming.lowLatencyEnabled = selectedConfig.lowLatencyMode; - } + } + + const liveDelayFragmentCount = parseInt($scope.liveDelayFragmentCount); + if (!isNaN(liveDelayFragmentCount)) { + config.streaming.delay.liveDelayFragmentCount = liveDelayFragmentCount; } const initialLiveDelay = parseFloat($scope.initialLiveDelay); if (!isNaN(initialLiveDelay)) { - config.streaming.liveDelay = initialLiveDelay; + config.streaming.delay.liveDelay = initialLiveDelay; } const initBitrate = parseInt($scope.initialVideoBitrate); @@ -697,15 +982,16 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' config.streaming.cmcd.cid = $scope.cmcdContentId ? $scope.cmcdContentId : null; config.streaming.cmcd.rtp = $scope.cmcdRtp ? $scope.cmcdRtp : null; config.streaming.cmcd.rtpSafetyFactor = $scope.cmcdRtpSafetyFactor ? $scope.cmcdRtpSafetyFactor : null; + config.streaming.cmcd.enabledKeys = $scope.cmcdEnabledKeys ? $scope._getFormatedCmcdEnabledKeys() : []; $scope.player.updateSettings(config); $scope.controlbar.reset(); $scope.conformanceViolations = []; if ($scope.isCasting) { - loadCastMedia($scope.selectedItem.url, protData); + loadCastMedia($scope.selectedItem.url, $scope.protData); } else { - $scope.player.setProtectionData(protData); + $scope.player.setProtectionData($scope.protData); $scope.player.attachSource($scope.selectedItem.url); } if ($scope.initialSettings.audio) { @@ -720,17 +1006,17 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' } if ($scope.initialSettings.text) { if ($scope.initialSettings.textRole) { - $scope.player.setInitialMediaSettingsFor('fragmentedText', { + $scope.player.setInitialMediaSettingsFor('text', { role: $scope.initialSettings.textRole, lang: $scope.initialSettings.text }); } else { - $scope.player.setInitialMediaSettingsFor('fragmentedText', { + $scope.player.setInitialMediaSettingsFor('text', { lang: $scope.initialSettings.text }); } } - $scope.player.setTextDefaultEnabled($scope.initialSettings.textEnabled); + $scope.player.updateSettings({ streaming: { text: { defaultEnabled: $scope.initialSettings.textEnabled } } }); $scope.player.enableForcedTextStreaming($scope.initialSettings.forceTextStreaming); $scope.controlbar.enable(); }; @@ -749,7 +1035,7 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' }; $scope.setLogLevel = function () { - var level = $("input[name='log-level']:checked").val(); + var level = $('input[name=\'log-level\']:checked').val(); switch (level) { case 'none': $scope.player.updateSettings({ 'debug': { 'logLevel': dashjs.Debug.LOG_LEVEL_NONE } }); @@ -777,18 +1063,18 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' }; $scope.setCmcdMode = function () { - var mode = $("input[name='cmcd-mode']:checked").val(); + var mode = $('input[name=\'cmcd-mode\']:checked').val(); switch (mode) { case 'query': - $scope.player.updateSettings({ streaming: { cmcd: { mode: 'query' }}}); + $scope.player.updateSettings({ streaming: { cmcd: { mode: 'query' } } }); break; case 'header': - $scope.player.updateSettings({ streaming: { cmcd: { mode: 'header' }}}); + $scope.player.updateSettings({ streaming: { cmcd: { mode: 'header' } } }); break; default: - $scope.player.updateSettings({ streaming: { cmcd: { mode: 'query' }}}); + $scope.player.updateSettings({ streaming: { cmcd: { mode: 'query' } } }); } }; @@ -808,6 +1094,292 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' $scope.drmKeySystem = item; }; + /** Handle form input */ + $scope.setDrm = function () { + + let drmInputs = [$scope.drmPlayready, $scope.drmWidevine, $scope.drmClearkey]; + let protectionData = {}; + + $scope.handleRequestHeaders(); + $scope.handleClearkeys(); + + for (let input of drmInputs) { + if (input.isActive) { + + // Check if the provided DRM is Clearkey and whether KID=KEY or LicenseServer + Header is selected; Default is KID=KEY + if (input.hasOwnProperty('inputMode') && input.inputMode === 'kidKey') { + //Check clearkeys has at least one entry + if (input.clearkeys !== {}) { + // Check if priority is enabled + protectionData[input.drmKeySystem] = { + 'clearkeys': {}, + 'priority': 0 + }; + if (this.prioritiesEnabled) { + for (let key in input.clearkeys) { + protectionData[input.drmKeySystem]['clearkeys'][key] = input.clearkeys[key]; + } + protectionData[input.drmKeySystem]['priority'] = parseInt(input.priority); + } else { + for (let key in input.clearkeys) { + protectionData[input.drmKeySystem]['clearkeys'][key] = input.clearkeys[key]; + } + } + + for (let key in input) { + if (key !== 'isActive' && + key !== 'drmKeySystem' && + key !== 'licenseServerUrl' && + key !== 'httpRequestHeaders' && + key !== 'priority' && + key !== 'kid' && + key !== 'key' && + key !== 'inputMode') { + protectionData[input.drmKeySystem][key] = input[key]; + } + } + + if (!angular.equals(input.httpRequestHeaders, {})) { + protectionData[input.drmKeySystem]['httpRequestHeaders'] = input.httpRequestHeaders; + } + } else { + alert('Kid and Key must be specified!'); + } + + } else { + // Check if DRM-Priorisation is enabled + if (this.prioritiesEnabled) { + protectionData[input.drmKeySystem] = { + 'serverURL': input.licenseServerUrl, + 'priority': parseInt(input.priority) + } + if (!angular.equals(input.httpRequestHeaders, {})) + protectionData[input.drmKeySystem]['httpRequestHeaders'] = input.httpRequestHeaders; + + } else { + protectionData[input.drmKeySystem] = { + 'serverURL': input.licenseServerUrl, + } + } + + + // Enable DRM Today + if ($scope.drmToday) { + protectionData[input.drmKeySystem].drmtoday = true; + } + + for (let key in input) { + if (key !== 'isActive' && + key !== 'drmKeySystem' && + key !== 'licenseServerUrl' && + key !== 'httpRequestHeaders' && + key !== 'priority') { + protectionData[input.drmKeySystem][key] = input[key]; + } + } + + // Only set request header if any have been specified + if (!angular.equals(input.httpRequestHeaders, {})) { + protectionData[input.drmKeySystem]['httpRequestHeaders'] = input.httpRequestHeaders; + } + } + } + } + + $scope.protectionData = protectionData; + $scope.player.setProtectionData(protectionData); + } + + $scope.addPopupInput = function (keySystem) { + + switch (keySystem) { + case 'playready': + $scope.playreadyRequestHeaders.push({ + id: $scope.playreadyRequestHeaders.length + 1, + key: '', + value: '' + }) + break; + case 'widevine': + $scope.widevineRequestHeaders.push({ + id: $scope.widevineRequestHeaders.length + 1, + key: '', + value: '' + }) + break; + case 'clearkey': + $scope.clearkeyRequestHeaders.push({ + id: $scope.clearkeyRequestHeaders.length + 1, + key: '', + value: '' + }) + break; + case 'additionalClearkeys': + $scope.additionalClearkeyPairs.push({ + id: $scope.additionalClearkeyPairs.length + 1, + kid: '', + key: '' + }) + } + } + + $scope.removePopupInput = function (keySystem, index) { + switch (keySystem) { + case 'playready': + $scope.playreadyRequestHeaders.splice(index, 1); + break; + case 'widevine': + $scope.widevineRequestHeaders.splice(index, 1); + break; + case 'clearkey': + $scope.clearkeyRequestHeaders.splice(index, 1); + break; + case 'additionalClearkeys': + $scope.additionalClearkeyPairs.splice(index, 1); + break; + } + + } + + $scope.handleRequestHeaders = function () { + // Initialize with current headers as empty + $scope.drmPlayready.httpRequestHeaders = {}; + $scope.drmWidevine.httpRequestHeaders = {}; + $scope.drmClearkey.httpRequestHeaders = {}; + + // fill headers with current inputs + for (let header of $scope.playreadyRequestHeaders) { + $scope.drmPlayready.httpRequestHeaders[header.key] = header.value; + } + for (let header of $scope.widevineRequestHeaders) { + $scope.drmWidevine.httpRequestHeaders[header.key] = header.value; + } + for (let header of $scope.clearkeyRequestHeaders) { + $scope.drmClearkey.httpRequestHeaders[header.key] = header.value; + } + } + + /** Handle multiple clearkeys */ + $scope.handleClearkeys = function () { + // Initialize with empty + $scope.drmClearkey.clearkeys = {} + + // Set default KID=KEY pair + if ($scope.drmClearkey.kid !== '' && $scope.drmClearkey.key !== '') { + $scope.drmClearkey.clearkeys[$scope.drmClearkey.kid] = $scope.drmClearkey.key; + } + // fill drmClearkey objects "clearkeys" property + for (let clearkey of $scope.additionalClearkeyPairs) { + $scope.drmClearkey.clearkeys[clearkey.kid] = clearkey.key; + } + // if clearkey property is empty, alert + if ($scope.additionalClearkeyPairs === {}) { + alert('You must specify at least one KID=KEY pair!'); + } + } + + /** Handle inherent protection data passed by selectedItem */ + $scope.handleProtectionData = function (protectionData) { + for (let data in protectionData) { + switch (data) { + case 'playready': + case 'com.microsoft.playready': + // Set DRM to active + $scope.drmPlayready.isActive = true; + // Fill the drmPlayready object with data to be used by setDRM() later. + $scope.drmPlayready.licenseServerUrl = protectionData[data]['serverURL']; + for (let header in protectionData[data]['httpRequestHeaders']) { + $scope.playreadyRequestHeaders.push({ + id: $scope.playreadyRequestHeaders.length + 1, + key: header, + value: protectionData[data]['httpRequestHeaders'][header] + }); + } + // Add any additional parameters + for (let parameter in protectionData[data]) { + if (parameter !== 'serverURL' && + parameter !== 'httpRequestHeaders') { + $scope.drmPlayready[parameter] = protectionData[data][parameter]; + } + } + break; + + case 'widevine': + case 'com.widevine.alpha': + // Set DRM to active + $scope.drmWidevine.isActive = true; + // Fill the drmWidevine object with data to be used by setDRM() later + $scope.drmWidevine.licenseServerUrl = protectionData[data]['serverURL']; + for (let header in protectionData[data]['httpRequestHeaders']) { + $scope.widevineRequestHeaders.push({ + id: $scope.widevineRequestHeaders.length + 1, + key: header, + value: protectionData[data]['httpRequestHeaders'][header] + }); + } + // Add any additional parameters + for (let parameter in protectionData[data]) { + if (parameter !== 'serverURL' && + parameter !== 'httpRequestHeaders') { + $scope.drmWidevine[parameter] = protectionData[data][parameter]; + } + } + break; + + case 'clearkey': + case 'org.w3.clearkey': + // Set DRM to active + $scope.drmClearkey.isActive = true; + //TODO : Check if any examples are not kid=key method! + if (!protectionData[data].hasOwnProperty('inputMode')) { + protectionData[data]['inputMode'] = 'kidKey'; + } + $scope.drmClearkey.inputMode = protectionData[data]['inputMode']; + // Handle clearkey data if specified using a license server + if (protectionData[data]['serverURL'] !== undefined) { + $scope.drmClearkey.licenseServerUrl = protectionData[data]['serverURL']; + for (let header in protectionData[data]['httpRequestHeaders']) { + $scope.clearkeyRequestHeaders.push({ + id: $scope.clearkeyRequestHeaders.length + 1, + key: header, + value: protectionData[data]['httpRequestHeaders'][header] + }); + } + } + // Handle clearkey data if specified using KID=KEY. + else { + let first = true; + if (protectionData[data]['clearkeys'] !== {}) { + for (let kid in protectionData[data]['clearkeys']) { + // For the first KID=Key pair, set drmClearkey properties so that it shows in the main text boxes + if (first === true) { + $scope.drmClearkey.kid = kid; + $scope.drmClearkey.key = protectionData[data]['clearkeys'][kid]; + delete protectionData[data]['clearkeys'][kid]; + first = false; + } else if (protectionData[data]['clearkeys'] !== {}) { + $scope.additionalClearkeyPairs.push({ + id: $scope.additionalClearkeyPairs.length + 1, + kid: kid, + key: protectionData[data]['clearkeys'][kid] + }); + } + } + } + } + // Add any additional parameters + for (let parameter in protectionData[data]) { + if (parameter !== 'serverURL' && + parameter !== 'httpRequestHeaders' && + parameter !== 'clearkeys') { + $scope.drmClearkey[parameter] = protectionData[data][parameter]; + } + } + break; + } + } + } + // from: https://gist.github.com/siongui/4969449 $scope.safeApply = function (fn) { var phase = this.$root.$$phase; @@ -817,6 +1389,416 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' this.$apply(fn); }; + $scope.openDialogue = function (keySystem) { + switch (keySystem) { + case 'playready': + document.getElementById('playreadyRequestHeaderDialogue').style.display = 'inline-block'; + break; + case 'widevine': + document.getElementById('widevineRequestHeaderDialogue').style.display = 'block'; + break; + case 'clearkey': + document.getElementById('clearkeyRequestHeaderDialogue').style.display = 'block'; + break; + case 'additionalClearkeys': + document.getElementById('additionalClearkeysDialogue').style.display = 'block'; + break; + } + } + + $scope.closeDialogue = function (keySystem) { + switch (keySystem) { + case 'playready': + document.getElementById('playreadyRequestHeaderDialogue').style.display = 'none'; + break; + case 'widevine': + document.getElementById('widevineRequestHeaderDialogue').style.display = 'none'; + break; + case 'clearkey': + document.getElementById('clearkeyRequestHeaderDialogue').style.display = 'none'; + break; + case 'additionalClearkeys': + document.getElementById('additionalClearkeysDialogue').style.display = 'none'; + } + } + + $scope.copyNotificationShow = function () { + document.getElementById('copyNotificationPopup').style.display = 'block'; + setTimeout($scope.copyNotificationHide, 3000); + } + + $scope.copyNotificationHide = function () { + document.getElementById('copyNotificationPopup').style.display = 'none'; + } + + window.onclick = function (event) { + if (event.target == document.getElementById('playreadyRequestHeaderDialogue') || + event.target == document.getElementById('widevineRequestHeaderDialogue') || + event.target == document.getElementById('clearkeyRequestHeaderDialogue') || + event.target == document.getElementById('additionalClearkeysDialogue')) { + event.target.style.display = 'none'; + } + } + + /** Copy a URL containing the current settings as query Parameters to the Clipboard */ + $scope.copyQueryUrl = function () { + var currentExternalSettings = { + mpd: encodeURIComponent(decodeURIComponent($scope.selectedItem.url)), + loop: $scope.loopSelected, + autoPlay: $scope.autoPlaySelected, + drmToday: $scope.drmToday, + forceQualitySwitchSelected: $scope.forceQualitySwitchSelected, + drmPrioritiesEnabled: $scope.prioritiesEnabled, + languageAudio: $scope.initialSettings.audio, + roleVideo: $scope.initialSettings.video, + languageText: $scope.initialSettings.text, + roleText: $scope.initialSettings.textRole, + forceTextStreaming: $scope.initialSettings.forceTextStreaming + } + + var externalSettingsString = $scope.toQueryString($scope.makeSettingDifferencesObject(currentExternalSettings, defaultExternalSettings)); + + $scope.handleRequestHeaders(); + $scope.handleClearkeys(); + var drmList = [$scope.drmPlayready, $scope.drmWidevine, $scope.drmClearkey]; + var currentDrm; + for (var drm of drmList) { + if (drm.isActive) { + switch (drm.drmKeySystem) { + case 'com.microsoft.playready': + currentDrm = { 'playready': drm }; + externalSettingsString += '&' + $scope.toQueryString(currentDrm); + break; + case 'com.widevine.alpha': + currentDrm = { 'widevine': drm }; + externalSettingsString += '&' + $scope.toQueryString(currentDrm); + break; + case 'org.w3.clearkey': + currentDrm = { 'clearkey': drm }; + externalSettingsString += '&' + $scope.toQueryString(currentDrm); + break; + } + } + } + var currentSetting = $scope.player.getSettings(); + currentSetting = $scope.makeSettingDifferencesObject(currentSetting, defaultSettings); + + var url = window.location.protocol + '//' + window.location.host + window.location.pathname + '?'; + var queryString = externalSettingsString + '+&' + $scope.toQueryString(currentSetting); + + var urlString = url + queryString; + + if (urlString.slice(-1) === '&') urlString = urlString.slice(0, -1); + + $scope.checkQueryLength(urlString); + + const element = document.createElement('textarea'); + element.value = urlString; + document.body.appendChild(element); + element.select(); + document.execCommand('copy'); + document.body.removeChild(element); + } + + $scope.makeSettingDifferencesObject = function (settings, defaultSettings) { + var settingDifferencesObject = {}; + + if (Array.isArray(settings)) { + return _arraysEqual(settings, defaultSettings) ? {} : settings; + } + + for (var setting in settings) { + if (typeof defaultSettings[setting] === 'object' && defaultSettings[setting] !== null && !(defaultSettings[setting] instanceof Array)) { + settingDifferencesObject[setting] = this.makeSettingDifferencesObject(settings[setting], defaultSettings[setting], false); + } + else if(settings[setting] !== defaultSettings[setting]){ + if(Array.isArray(settings[setting])){ + settingDifferencesObject[setting] = _arraysEqual(settings[setting], defaultSettings[setting]) ? {} : settings[setting]; + } + else { + settingDifferencesObject[setting] = settings[setting]; + } + + } + } + + return settingDifferencesObject; + } + + function _arraysEqual(a, b) { + if (a === b) { + return true; + } + if (a == null || b == null) { + return false; + } + if (a.length !== b.length) { + return false; + } + + // If you don't care about the order of the elements inside + // the array, you should sort both arrays here. + // Please note that calling sort on an array will modify that array. + // you might want to clone your array first. + + for (var i = 0; i < a.length; ++i) { + if (a[i] !== b[i]) { + return false; + } + } + + return true; + } + + /** Transform the current Settings into a nested query-string format */ + $scope.toQueryString = function (settings, prefix) { + var urlString = []; + for (var setting in settings) { + if (settings.hasOwnProperty(setting)) { + var k = prefix ? prefix + '.' + setting : setting, + v = settings[setting]; + urlString.push((v != null && typeof v === 'object') ? + this.toQueryString(v, k) : + encodeURIComponent(decodeURIComponent(k)) + '=' + encodeURIComponent(decodeURIComponent(v))); + } + } + // Make the string, then remove all cases of && caused by empty settings + return urlString.join('&').split(/&&*/).join('&'); + } + + /** Resolve nested query parameters */ + $scope.resolveQueryNesting = function (base, nestedKey, value) { + var keyList = nestedKey.split('.'); + var lastProperty = value !== null ? keyList.pop() : false; + var obj = base; + + for (var key = 0; key < keyList.length; key++) { + base = base[keyList[key]] = base [keyList[key]] || {}; + } + + + value = $scope.handleQueryParameters(value); + + if (lastProperty) base = base [lastProperty] = value; + + return obj; + } + + $scope.activeDrms = {}; + + /** Transform query-string into Object */ + $scope.toSettingsObject = function (queryString) { + //Remove double & in case of empty settings field + var querySegments = queryString.split('&&').join('&'); + querySegments = queryString.split('&'); + var settingsObject = {}; + var drmObject = {}; + var prioritiesEnabled = false; + var key, value; + var i = 1; + + for (var segment in querySegments) { + [key, value] = querySegments[segment].split('='); + value = decodeURIComponent(value); + + $scope.resolveQueryNesting(settingsObject, key, value); + } + + for (var settingCategory of Object.keys(settingsObject)) { + if (settingsObject !== {} && + (settingCategory === 'playready' || + settingCategory === 'widevine' || + settingCategory === 'clearkey') && + settingsObject[settingCategory].isActive) { + drmObject[settingCategory] = settingsObject[settingCategory]; + $scope.activeDrms[settingCategory] = settingsObject[settingCategory]; + delete settingsObject.settingCategory; + + } + } + prioritiesEnabled = settingsObject.drmPrioritiesEnabled; + if (prioritiesEnabled !== undefined) { + drmObject = $scope.makeProtectionData(drmObject, prioritiesEnabled); + } + return [settingsObject, drmObject]; + } + + $scope.makeProtectionData = function (drmObject, prioritiesEnabled) { + var queryProtectionData = {}; + + for (var drm in drmObject) { + if (drmObject[drm].hasOwnProperty('inputMode') && drmObject[drm].inputMode === 'kidKey') { + if (drmObject[drm].clearkeys !== {}) { + queryProtectionData[drmObject[drm].drmKeySystem] = { + 'clearkeys': {}, + 'priority': 0 + }; + if (prioritiesEnabled) { + for (var key in drmObject[drm].clearkeys) { + queryProtectionData[drmObject[drm].drmKeySystem]['clearkeys'][key] = drmObject[drm].clearkeys[key]; + } + queryProtectionData[drmObject[drm].drmKeySystem]['priority'] = parseInt(drmObject[drm].priority); + } else { + for (var key in drmObject[drm].clearkeys) { + queryProtectionData[drmObject[drm].drmKeySystem]['clearkeys'][key] = drmObject[drm].clearkeys[key]; + } + } + + for (var key in drmObject[drm]) { + if (key !== 'isActive' && + key !== 'drmKeySystem' && + key !== 'licenseServerUrl' && + key !== 'httpRequestHeaders' && + key !== 'priority' && + key !== 'kid' && + key !== 'key' && + key !== 'inputMode') { + queryProtectionData[drmObject[drm].drmKeySystem][key] = drmObject[drm][key]; + } + } + + if (drmObject[drm].httpRequestHeaders !== {}) { + queryProtectionData[drmObject[drm].drmKeySystem]['httpRequestHeaders'] = drmObject[drm].httpRequestHeaders; + } + } else { + alert('Kid and Key must be specified!'); + } + + } else { + //check if priority is enabled + if (prioritiesEnabled) { + queryProtectionData[drmObject[drm].drmKeySystem] = { + 'serverURL': decodeURIComponent(drmObject[drm].licenseServerUrl), + 'priority': parseInt(drmObject[drm].priority) + } + if (drmObject[drm].httpRequestHeaders !== {}) + queryProtectionData[drmObject[drm].drmKeySystem]['httpRequestHeaders'] = drmObject[drm].httpRequestHeaders; + + } else { + queryProtectionData[drmObject[drm].drmKeySystem] = { + 'serverURL': decodeURIComponent(drmObject[drm].licenseServerUrl), + } + } + + for (var key in drmObject[drm]) { + if (key !== 'isActive' && + key !== 'drmKeySystem' && + key !== 'licenseServerUrl' && + key !== 'httpRequestHeaders' && + key !== 'priority') { + queryProtectionData[drmObject[drm].drmKeySystem][key] = drmObject[drm][key]; + } + } + + // Only set request header if any have been specified + if (drmObject[drm].httpRequestHeaders !== {}) { + queryProtectionData[drmObject[drm].drmKeySystem]['httpRequestHeaders'] = drmObject[drm].httpRequestHeaders; + } + } + } + return queryProtectionData; + } + + $scope.setExternalSettings = function (currentQuery) { + var handleExternalSettings = currentQuery.split('+').join('').split('&'); + for (var index = 0; index < handleExternalSettings.length; index++) { + var [key, value] = handleExternalSettings[index].split('=') || ''; + switch (key) { + case 'mpd': + $scope.selectedItem.url = decodeURIComponent(value); + break; + case 'loop': + $scope.loopSelected = this.parseBoolean(value); + break; + case 'autoPlay': + $scope.autoPlaySelected = this.parseBoolean(value); + $scope.toggleAutoPlay(); + break; + case 'drmToday': + $scope.drmToday = this.parseBoolean(value); + break; + case 'forceQualitySwitchSelected': + $scope.forceQualitySwitchSelected = this.parseBoolean(value); + $scope.toggleForceQualitySwitch($scope.forceQualitySwitchSelected); + break; + case 'drmPrioritiesEnabled': + $scope.prioritiesEnabled = this.parseBoolean(value); + break; + case 'languageAudio': + $scope.player.setInitialMediaSettingsFor('audio', { + lang: $scope.handleQueryParameters(value) + }); + break; + case 'roleVideo': + $scope.player.setInitialMediaSettingsFor('video', { + role: $scope.handleQueryParameters(value) + }); + break; + case 'languageText': + $scope.initialSettings.text = $scope.handleQueryParameters(value) + $scope.player.setInitialMediaSettingsFor('text', { + lang: $scope.handleQueryParameters(value) + }); + break; + case 'roleText': + $scope.player.setInitialMediaSettingsFor('text', { + lang: $scope.handleQueryParameters($scope.initialSettings.text), + role: $scope.handleQueryParameters(value) + }); + break; + case 'forceTextStreaming': + $scope.initialSettings.forceTextStreaming = this.parseBoolean(value); + $scope.player.enableForcedTextStreaming($scope.initialSettings.forceTextStreaming); + break; + } + } + } + + $scope.setQueryData = function (currentQuery) { + if (!currentQuery.includes('&')) { + return; + } + var passedSettings = currentQuery.slice(currentQuery.indexOf('+')).substring(1); + passedSettings = $scope.toSettingsObject(passedSettings)[0]; + $scope.protectionData = $scope.toSettingsObject(currentQuery.split('+').join(''))[1]; + $scope.player.updateSettings(passedSettings); + $scope.handleProtectionData($scope.protectionData); + $scope.player.setProtectionData($scope.protectionData); + } + + $scope.parseBoolean = function (value) { + return value === true || value === 'true'; + } + + /** Takes a string value extracted from the query-string and transforms it into the appropriate type */ + $scope.handleQueryParameters = function (value) { + var typedValue; + var integerRegEx = /^-?\d+$/; + var floatRegEx = /^-?\d+.\d+$/; + if (value === 'true' || value === 'false') { + typedValue = this.parseBoolean(value); + } else if (value === 'NaN') typedValue = NaN; + else if (value === 'null') typedValue = null; + else if (value === 'undefined') typedValue = undefined; + else integerRegEx.test(value) ? typedValue = parseInt(value) : + (floatRegEx.test(value) ? typedValue = parseFloat(value) : + typedValue = value); + + return typedValue; + } + + $scope.checkQueryLength = function (string) { + var maxUrlLength = 30000; + if (window.document.documentMode) { + maxUrlLength = 2083; + //Alt: "Due to the low url character limit on IE, please use the config file method instead." + //Alt2: If IE detected, copy settings-file content instead of creating a url, alert userto the change. + } + if (string.length > maxUrlLength) { + alert('The length of the URL may exceed the Browser url character limit.') + } + } + //////////////////////////////////////// // // Metrics @@ -965,9 +1947,9 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' var dashMetrics = $scope.player.getDashMetrics(); var dashAdapter = $scope.player.getDashAdapter(); - if (dashMetrics && $scope.streamInfo) { - var period = dashAdapter.getPeriodById($scope.streamInfo.id); - var periodIdx = period ? period.index : $scope.streamInfo.index; + if (dashMetrics && $scope.currentStreamInfo) { + var period = dashAdapter.getPeriodById($scope.currentStreamInfo.id); + var periodIdx = period ? period.index : $scope.currentStreamInfo.index; var maxIndex = dashAdapter.getMaxIndexForBufferType(type, periodIdx); var repSwitch = dashMetrics.getCurrentRepresentationSwitch(type, true); @@ -978,15 +1960,17 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' var droppedFramesMetrics = dashMetrics.getCurrentDroppedFrames(); var droppedFPS = droppedFramesMetrics ? droppedFramesMetrics.droppedFrames : 0; var liveLatency = 0; + var playbackRate = 1.00 if ($scope.isDynamic) { liveLatency = $scope.player.getCurrentLiveLatency(); + playbackRate = parseFloat($scope.player.getPlaybackRate().toFixed(2)); } $scope[type + 'BufferLength'] = bufferLevel; $scope[type + 'MaxIndex'] = maxIndex; - $scope[type + 'Bitrate'] = bitrate; $scope[type + 'DroppedFrames'] = droppedFPS; $scope[type + 'LiveLatency'] = liveLatency; + $scope[type + 'PlaybackRate'] = playbackRate; var httpMetrics = calculateHTTPMetrics(type, dashMetrics.getHttpRequests(type)); if (httpMetrics) { @@ -1002,6 +1986,7 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' $scope.plotPoint('bitrate', type, bitrate, time); $scope.plotPoint('droppedFPS', type, droppedFPS, time); $scope.plotPoint('liveLatency', type, liveLatency, time); + $scope.plotPoint('playbackRate', type, playbackRate, time); if (httpMetrics) { $scope.plotPoint('download', type, httpMetrics.download[type].average.toFixed(2), time); @@ -1037,95 +2022,263 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' // //////////////////////////////////////// - function doesTimeMarchesOn() { - var version; - var REQUIRED_VERSION = 49.0; + function setLatencyAttributes() { + // get buffer default value + var currentConfig = $scope.player.getSettings(); + $scope.defaultLiveDelay = currentConfig.streaming.delay.liveDelay; + $scope.defaultStableBufferDelay = currentConfig.streaming.buffer.stableBufferTime; + $scope.defaultBufferTimeAtTopQuality = currentConfig.streaming.buffer.bufferTimeAtTopQuality; + $scope.defaultBufferTimeAtTopQualityLongForm = currentConfig.streaming.buffer.bufferTimeAtTopQualityLongForm; + $scope.liveCatchupEnabled = currentConfig.streaming.liveCatchup.enabled; + $scope.liveCatchupMode = currentConfig.streaming.liveCatchup.mode; + } - if (typeof navigator !== 'undefined') { - if (!navigator.userAgent.match(/Firefox/)) { - return true; - } + function setAbrRules() { + var currentConfig = $scope.player.getSettings(); + $scope.additionalAbrRules.insufficientBufferRule = currentConfig.streaming.abr.additionalAbrRules.insufficientBufferRule; + $scope.additionalAbrRules.switchHistoryRule = currentConfig.streaming.abr.additionalAbrRules.switchHistoryRule; + $scope.additionalAbrRules.droppedFramesRule = currentConfig.streaming.abr.additionalAbrRules.droppedFramesRule; + $scope.additionalAbrRules.abandonRequestsRule = currentConfig.streaming.abr.additionalAbrRules.abandonRequestsRule; + $scope.ABRStrategy = currentConfig.streaming.abr.ABRStrategy; + $scope.abrThroughputCalculationMode = currentConfig.streaming.abr.fetchThroughputCalculationMode; + } - version = parseFloat(navigator.userAgent.match(/rv:([0-9.]+)/)[1]); + function setAdditionalPlaybackOptions() { + var currentConfig = $scope.player.getSettings(); + $scope.applyServiceDescription = currentConfig.streaming.applyServiceDescription; + $scope.applyContentSteering = currentConfig.streaming.applyContentSteering; + $scope.scheduleWhilePausedSelected = currentConfig.streaming.scheduling.scheduleWhilePaused; + $scope.calcSegmentAvailabilityRangeFromTimelineSelected = currentConfig.streaming.timeShiftBuffer.calcFromSegmentTimeline; + $scope.reuseExistingSourceBuffersSelected = currentConfig.streaming.buffer.reuseExistingSourceBuffers; + $scope.localStorageSelected = currentConfig.streaming.lastBitrateCachingInfo.enabled; + $scope.jumpGapsSelected = currentConfig.streaming.gaps.jumpGaps; + } - if (!isNaN(version) && version >= REQUIRED_VERSION) { - return true; - } - } + function setAdditionalAbrOptions() { + var currentConfig = $scope.player.getSettings(); + $scope.fastSwitchSelected = currentConfig.streaming.buffer.fastSwitchEnabled; + $scope.videoAutoSwitchSelected = currentConfig.streaming.abr.autoSwitchBitrate.video; + $scope.customABRRulesSelected = !currentConfig.streaming.abr.useDefaultABRRules; } - function setLatencyAttributes() { - // get buffer default value + function setDrmOptions() { var currentConfig = $scope.player.getSettings(); - $scope.defaultLiveDelay = currentConfig.streaming.liveDelay; - $scope.defaultStableBufferDelay = currentConfig.streaming.stableBufferTime; - $scope.defaultBufferTimeAtTopQuality = currentConfig.streaming.bufferTimeAtTopQuality; - $scope.defaultBufferTimeAtTopQualityLongForm = currentConfig.streaming.bufferTimeAtTopQualityLongForm; - $scope.lowLatencyModeSelected = currentConfig.streaming.lowLatencyEnabled; - $scope.liveCatchupEnabled = currentConfig.streaming.liveCatchup.enabled; + $scope.drmPlayready.priority = $scope.drmPlayready.priority.toString(); + $scope.drmWidevine.priority = $scope.drmWidevine.priority.toString(); + $scope.drmClearkey.priority = $scope.drmClearkey.priority.toString(); } + function setLiveDelayOptions() { + var currentConfig = $scope.player.getSettings(); + $scope.initialLiveDelay = currentConfig.streaming.delay.liveDelay; + $scope.liveDelayFragmentCount = currentConfig.streaming.delay.liveDelayFragmentCount; + $scope.useSuggestedPresentationDelay = currentConfig.streaming.delay.useSuggestedPresentationDelay; + } - (function init() { - $scope.initChartingByMediaType('video'); - $scope.initChartingByMediaType('audio'); + function setInitialSettings() { + var currentConfig = $scope.player.getSettings(); + if (currentConfig.streaming.abr.initialBitrate.video !== -1) { + $scope.initialVideoBitrate = currentConfig.streaming.abr.initialBitrate.video; + } + if (currentConfig.streaming.abr.minBitrate.video !== -1) { + $scope.minVideoBitrate = currentConfig.streaming.abr.minBitrate.video; + } + if (currentConfig.streaming.abr.maxBitrate.video !== -1) { + $scope.maxVideoBitrate = currentConfig.streaming.abr.maxBitrate.video; + } - function getUrlVars() { - var vars = {}; - window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (m, key, value) { - vars[key] = value; - }); - return vars; + if ($scope.player.getInitialMediaSettingsFor('audio')) { + $scope.initialSettings.audio = $scope.player.getInitialMediaSettingsFor('audio').lang; + } + if ($scope.player.getInitialMediaSettingsFor('video')) { + $scope.initialSettings.video = $scope.player.getInitialMediaSettingsFor('video').role; + } + if ($scope.player.getInitialMediaSettingsFor('text')) { + $scope.initialSettings.text = $scope.player.getInitialMediaSettingsFor('text').lang; + } + if ($scope.player.getInitialMediaSettingsFor('text')) { + $scope.initialSettings.textRole = $scope.player.getInitialMediaSettingsFor('text').role; } - var vars = getUrlVars(); - var item = {}; + $scope.initialSettings.textEnabled = currentConfig.streaming.text.defaultEnabled; + } - if (vars && vars.hasOwnProperty('url')) { - item.url = vars.url; - } + function setTrackSwitchModeSettings() { + currentConfig = $scope.player.getSettings(); + initAudioTrackSwitchMode = currentConfig.streaming.trackSwitchMode.audio; + $scope.audioTrackSwitchMode = currentConfig.streaming.trackSwitchMode.audio; + initVideoTrackSwitchMode = currentConfig.streaming.trackSwitchMode.video; + $scope.videoTrackSwitchMode = currentConfig.streaming.trackSwitchMode.video; + } - if (vars && vars.hasOwnProperty('mpd')) { - item.url = vars.mpd; + function setInitialLogLevel() { + var initialLogLevel = $scope.player.getSettings().debug.logLevel; + switch (initialLogLevel) { + case 0: + $scope.currentLogLevel = 'none'; + break; + case 1: + $scope.currentLogLevel = 'fatal'; + break; + case 2: + $scope.currentLogLevel = 'error'; + break; + case 3: + $scope.currentLogLevel = 'warning'; + break; + case 4: + $scope.currentLogLevel = 'info'; + break; + case 5: + $scope.currentLogLevel = 'debug'; + break; } + } - if (vars && vars.hasOwnProperty('source')) { - item.url = vars.source; + function setCMCDSettings() { + var currentConfig = $scope.player.getSettings(); + $scope.cmcdEnabled = currentConfig.streaming.cmcd.enabled; + if (currentConfig.streaming.cmcd.sid) { + $scope.cmcdSessionId = currentConfig.streaming.cmcd.sid; + } + if (currentConfig.streaming.cmcd.cid) { + $scope.cmcdContentId = currentConfig.streaming.cmcd.cid; + } + if (currentConfig.streaming.cmcd.rtp) { + $scope.cmcdRtp = currentConfig.streaming.cmcd.rtp; + } + if (currentConfig.streaming.cmcd.rtpSafetyFactor) { + $scope.cmcdRtpSafetyFactor = currentConfig.streaming.cmcd.rtpSafetyFactor; } - if (vars && vars.hasOwnProperty('stream')) { - try { - item = JSON.parse(atob(vars.stream)); - } catch (e) { - } + $scope.cmcdMode = currentConfig.streaming.cmcd.mode; + + if (currentConfig.streaming.cmcd.enabledKeys) { + $scope.cmcdEnabledKeys = currentConfig.streaming.cmcd.enabledKeys; } + } + function getUrlVars() { + var vars = {}; + window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (m, key, value) { + vars[key] = value; + }); + return vars; + } + + + (function init() { - if (vars && vars.hasOwnProperty('targetLatency')) { - let targetLatency = parseInt(vars.targetLatency, 10); - if (!isNaN(targetLatency)) { - item.bufferConfig = { - lowLatencyMode: true, - liveDelay: targetLatency / 1000 + //////////////////////////////////////// + // + // Configuration file + // + //////////////////////////////////////// + let reqConfig = new XMLHttpRequest(); + reqConfig.onload = function () { + if (reqConfig.status === 200) { + let config = JSON.parse(reqConfig.responseText); + if ($scope.player) { + $scope.player.updateSettings(config); + } + } else { + // Set default initial configuration + var initialConfig = { + 'debug': { + 'logLevel': dashjs.Debug.LOG_LEVEL_INFO + }, + 'streaming': { + 'buffer': { + 'fastSwitchEnabled': $scope.fastSwitchSelected, + }, + 'jumpGaps': true, + 'abr': { + 'autoSwitchBitrate': { + 'video': $scope.videoAutoSwitchSelected + } + } + } }; + $scope.player.updateSettings(initialConfig); + } - $scope.lowLatencyModeSelected = true; + /** Fetch query string and pass it to handling function */ + var currentQuery = window.location.search; + if (currentQuery !== '') { + currentQuery = currentQuery.substring(1); + $scope.checkQueryLength(window.location.href); + $scope.setExternalSettings(currentQuery); + $scope.setQueryData(currentQuery); } - } - if (item.url) { - var startPlayback = false; + setLatencyAttributes(); + setAbrRules(); + setAdditionalPlaybackOptions(); + setAdditionalAbrOptions(); + setDrmOptions(); + setLiveDelayOptions(); + setInitialSettings(); + setTrackSwitchModeSettings(); + setInitialLogLevel(); + setCMCDSettings(); + + checkLocationProtocol(); + + var vars = getUrlVars(); + var item = {}; + + if (vars && vars.hasOwnProperty('url')) { + item.url = vars.url; + } - $scope.selectedItem = item; + // if (vars && vars.hasOwnProperty('mpd')) { + // item.url = vars.mpd; + // } - if (vars.hasOwnProperty('autoplay')) { - startPlayback = (vars.autoplay === 'true'); + if (vars && vars.hasOwnProperty('source')) { + item.url = vars.source; } - if (startPlayback) { - $scope.doLoad(); + if (vars && vars.hasOwnProperty('stream')) { + try { + item = JSON.parse(atob(vars.stream)); + } catch (e) { + } + } + + + if (vars && vars.hasOwnProperty('targetLatency')) { + let targetLatency = parseInt(vars.targetLatency, 10); + if (!isNaN(targetLatency)) { + item.bufferConfig = { + lowLatencyMode: true, + liveDelay: targetLatency / 1000 + }; + + } + } + + if (item.url) { + var startPlayback = false; + + $scope.selectedItem = item; + + if (vars.hasOwnProperty('autoplay')) { + startPlayback = (vars.autoplay === 'true'); + } + + if (startPlayback) { + $scope.doLoad(); + } } } + + reqConfig.open('GET', 'dashjs_config.json', true); + reqConfig.setRequestHeader('Content-type', 'application/json'); + reqConfig.send(); + + $scope.initChartingByMediaType('video'); + $scope.initChartingByMediaType('audio'); })(); //////////////////////////////////////// @@ -1142,18 +2295,18 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' let castPlayer; - $window['__onGCastApiAvailable'] = function(isAvailable) { + $window['__onGCastApiAvailable'] = function (isAvailable) { if (isAvailable) { castContext = cast.framework.CastContext.getInstance(); castContext.setOptions({ - receiverApplicationId: CAST_APP_ID, - autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED + receiverApplicationId: CAST_APP_ID, + autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED }); castContext.addEventListener(cast.framework.CastContextEventType.CAST_STATE_CHANGED, function (e) { console.log('[Cast]', e); if (e.castState === cast.framework.CastState.CONNECTED) { onCastReady(); - } else if (e.castState === cast.framework.CastState.NOT_CONNECTED) { + } else if (e.castState === cast.framework.CastState.NOT_CONNECTED) { onCastEnd(); } }); @@ -1195,16 +2348,29 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' if (castSession) { castPlayer.reset(); castSession.loadMedia(request).then( - function() { + function () { let media = castSession.getMediaSession(); if (media) { console.info('cast media: ', media); } }, - function(errorCode) { console.log('Error code: ' + errorCode); } + function (errorCode) { + console.log('Error code: ' + errorCode); + } ); } } + + function checkLocationProtocol() { + if (location.protocol === 'http:' && location.hostname !== 'localhost') { + var out = 'This page has been loaded under http. This can result in the EME APIs not being available to the player and any DRM-protected content will fail to play. ' + + 'If you wish to test manifest URLs that require EME support, then reload this page under https.' + var divContainer = document.getElementById('http-warning-container'); + var spanText = document.getElementById('http-warning-text'); + spanText.innerHTML = out; + divContainer.style.display = '' + } + } }]); function legendLabelClickHandler(obj) { /* jshint ignore:line */ diff --git a/samples/dash-if-reference-player/app/rules/DownloadRatioRule.js b/samples/dash-if-reference-player/app/rules/DownloadRatioRule.js index 9c8234a922..c18c2cc1eb 100644 --- a/samples/dash-if-reference-player/app/rules/DownloadRatioRule.js +++ b/samples/dash-if-reference-player/app/rules/DownloadRatioRule.js @@ -64,7 +64,7 @@ function DownloadRatioRuleClass() { let dashManifest = DashManifestModel(context).getInstance(); let streamController = StreamController(context).getInstance(); let abrController = rulesContext.getAbrController(); - let current = abrController.getQualityFor(mediaType, streamController.getActiveStreamInfo()); + let current = abrController.getQualityFor(mediaType, streamController.getActiveStreamInfo().id); let requests = dashMetrics.getHttpRequests(mediaType), lastRequest = null, @@ -108,7 +108,7 @@ function DownloadRatioRuleClass() { return SwitchRequest(context).create(); } - if(lastRequest.type !== 'MediaSegment' ) { + if (lastRequest.type !== 'MediaSegment') { logger.debug("[CustomRules][" + mediaType + "][DownloadRatioRule] Last request is not a media segment, bailing."); return SwitchRequest(context).create(); } @@ -171,7 +171,7 @@ function DownloadRatioRuleClass() { p = SwitchRequest.PRIORITY.WEAK; logger.debug("[CustomRules] SwitchRequest: q=" + q + "/" + (count - 1) + " (" + bandwidths[q] + ")"/* + ", p=" + p*/); - return SwitchRequest(context).create(q, {name : DownloadRatioRuleClass.__dashjs_factory_name}, p); + return SwitchRequest(context).create(q, { name: DownloadRatioRuleClass.__dashjs_factory_name }, p); } else { for (i = count - 1; i > current; i -= 1) { if (calculatedBandwidth > (bandwidths[i] * switchUpRatioSafetyFactor)) { @@ -184,7 +184,7 @@ function DownloadRatioRuleClass() { p = SwitchRequest.PRIORITY.STRONG; logger.debug("[CustomRules] SwitchRequest: q=" + q + "/" + (count - 1) + " (" + bandwidths[q] + ")"/* + ", p=" + p*/); - return SwitchRequest(context).create(q, {name : DownloadRatioRuleClass.__dashjs_factory_name}, p); + return SwitchRequest(context).create(q, { name: DownloadRatioRuleClass.__dashjs_factory_name }, p); } } diff --git a/samples/dash-if-reference-player/app/sources.json b/samples/dash-if-reference-player/app/sources.json index 5ebc242c3a..4d07693759 100644 --- a/samples/dash-if-reference-player/app/sources.json +++ b/samples/dash-if-reference-player/app/sources.json @@ -58,6 +58,26 @@ "acronym": "AWS", "name": "AWS", "url": "https://www.elemental.com/" + }, + "dvb": { + "acronym": "DVB", + "name": "DVB", + "url": "https://dvb-2017-dm.s3.eu-central-1.amazonaws.com/overview.html" + }, + "hbbtv": { + "acronym": "HbbTV", + "name": "HbbTV", + "url": "https://www.hbbtv.org/" + }, + "google": { + "acronym": "Google", + "name": "Google", + "url": "https://www.google.com" + }, + "vdms": { + "acronym": "VDMS", + "name": "Verizon Media", + "url": "https://www.verizonmedia.com/" } }, "items": [ @@ -96,9 +116,28 @@ "provider": "bbc" }, { - "name": "SegmentTemplate with SegmentTimeline", - "url": "https://demo.unified-streaming.com/video/tears-of-steel/tears-of-steel.ism/.mpd", - "provider": "unified" + "name": "Single-period, 1080p, H.264, 5 video, 3 audio, 3 text tracks, CMAF, no encryption", + "url": "https://media.axprod.net/TestVectors/Cmaf/clear_1080p_h264/manifest.mpd", + "moreInfo": "https://github.com/Axinom/public-test-vectors", + "provider": "axinom" + }, + { + "name": "Single-period, 1080p, H.265, 5 video, 3 audio, 3 text tracks, CMAF, no encryption", + "url": "https://media.axprod.net/TestVectors/H265/clear_cmaf_1080p_h265/manifest.mpd", + "moreInfo": "https://github.com/Axinom/public-test-vectors", + "provider": "axinom" + }, + { + "name": "Single-period, 1080p, H.264, 5 video, 3 audio, 3 text tracks, DASH, no encryption", + "url": "https://media.axprod.net/TestVectors/Dash/not_protected_dash_1080p_h264/manifest.mpd", + "moreInfo": "https://github.com/Axinom/public-test-vectors", + "provider": "axinom" + }, + { + "name": "Single-period, 1080p, H.265, 5 video, 3 audio, 3 text tracks, DASH, no encryption", + "url": "https://media.axprod.net/TestVectors/H265/clear_dash_1080p_h265/manifest.mpd", + "moreInfo": "https://github.com/Axinom/public-test-vectors", + "provider": "axinom" }, { "name": "1080p without encryption", @@ -136,19 +175,34 @@ "provider": "streamroot" }, { - "name": "Clear Static SegmentTimeline", - "url": "//wowzaec2demo.streamlock.net/vod/_definst_/ElephantsDream/smil:ElephantsDream.smil/manifest_mvtime.mpd", - "provider": "wowza" + "name": "Caminandes 01, Llama Drama (25fps, 75gop, 1080p) ", + "url": "http://refapp.hbbtv.org/videos/01_llama_drama_1080p_25f75g6sv3/manifest.mpd", + "provider": "hbbtv" + }, + { + "name": "Caminandes 02, Gran Dillama (25fps, 75gop, 1080p, KID=1236, subob,evtib) v5 ", + "url": "http://refapp.hbbtv.org/videos/02_gran_dillama_1080p_25f75g6sv5/manifest_subob_evtib.mpd", + "provider": "hbbtv" + }, + { + "name": "Tears of Steel (25fps, 75gop, 1080p, KID=1237) v3", + "url": "http://refapp.hbbtv.org/videos/tears_of_steel_1080p_25f75g6sv3/manifest.mpd", + "provider": "hbbtv" + }, + { + "name": "Caminandes 02, Gran Dillama (25fps, 75gop, 1080p, KID=1236), multiaudio v4", + "url": "http://refapp.hbbtv.org/videos/02_gran_dillama_1080p_ma_25f75g6sv4/manifest_subob_evtib.mpd", + "provider": "hbbtv" }, { - "name": "Clear Static SegmentTemplate", - "url": "//wowzaec2demo.streamlock.net/vod/_definst_/ElephantsDream/smil:ElephantsDream.smil/manifest_mvnumber.mpd", - "provider": "wowza" + "name": "Caminandes 02, Gran Dillama (25fps, 75gop, 1080p, KID=1236), multiaudio v5", + "url": "http://refapp.hbbtv.org/videos/02_gran_dillama_1080p_ma_25f75g6sv5/manifest.mpd", + "provider": "hbbtv" }, { - "name": "Clear Static SegmentList", - "url": "//wowzaec2demo.streamlock.net/vod/_definst_/ElephantsDream/smil:ElephantsDream.smil/manifest_mvlist.mpd", - "provider": "wowza" + "name": "Spring (25fps, 75gop, 1920x804(2.40) h264, KID=148D) v1", + "url": "http://refapp.hbbtv.org/videos/spring_804p_v1/manifest.mpd", + "provider": "hbbtv" } ] }, @@ -184,27 +238,47 @@ "url": "https://livesim.dashif.org/livesim/ato_inf/testpic_2s/Manifest.mpd", "name": "Infinite offset - all segments available at availability start (livesim)", "provider": "dashif" + }, + { + "url": "https://livesim.dashif.org/livesim/scte35_2/testpic_2s/Manifest.mpd", + "name": "SCTE35 events", + "provider": "dashif" }, { - "url": "https://live.unified-streaming.com/scte35/scte35.isml/.mpd", + "url": "https://demo.unified-streaming.com/k8s/live/scte35.isml/.mpd", "name": "Unified Streaming reference stream with scte35 markers", "moreInfo": "http://demo.unified-streaming.com/features.html", "provider": "unified" }, { - "name": "Clear Dynamic SegmentTimeline", - "url": "//wowzaec2demo.streamlock.net/live/bigbuckbunny/manifest_mvtime.mpd", - "provider": "wowza" + "name": "Multiperiod - Number + Timeline - Compact manifest - Thumbnails (1 track) - In-the-clear", + "url": "https://d24rwxnt7vw9qb.cloudfront.net/v1/dash/e6d234965645b411ad572802b6c9d5a10799c9c1/All_Reference_Streams/4577dca5f8a44756875ab5cc913cd1f1/index.mpd", + "provider": "aws" + }, + { + "name": "Multiperiod - Number + Timeline - Full manifest - Thumbnails (1 track) - In-the-clear", + "url": "https://d24rwxnt7vw9qb.cloudfront.net/v1/dash/e6d234965645b411ad572802b6c9d5a10799c9c1/All_Reference_Streams/ee565ea510cb4b4d8df5f48918c3d6dc/index.mpd", + "provider": "aws" + }, + { + "name": "Multiperiod - Time + Timeline - Compact manifest - Thumbnails (1 track) - In-the-clear", + "url": "https://d24rwxnt7vw9qb.cloudfront.net/v1/dash/e6d234965645b411ad572802b6c9d5a10799c9c1/All_Reference_Streams/91d37b0389de47e0b5266736d3633077/index.mpd", + "provider": "aws" }, { - "name": "Clear Dynamic SegmentTemplate", - "url": "//wowzaec2demo.streamlock.net/live/bigbuckbunny/manifest_mvnumber.mpd", - "provider": "wowza" + "name": "Multiperiod - Time + Timeline - Full manifest - Thumbnails (1 track) - In-the-clear", + "url": "https://d24rwxnt7vw9qb.cloudfront.net/v1/dash/e6d234965645b411ad572802b6c9d5a10799c9c1/All_Reference_Streams/6ba06d17f65b4e1cbd1238eaa05c02c1/index.mpd", + "provider": "aws" }, { - "name": "Clear Dynamic SegmentList", - "url": "//wowzaec2demo.streamlock.net/live/bigbuckbunny/manifest_mvlist.mpd", - "provider": "wowza" + "name": "Single period - Number + Duration - Full manifest - Thumbnails (2 tracks: 174p/1080p) - In-the-clear", + "url": "https://d10gktn8v7end7.cloudfront.net/out/v1/6ee19df3afa24fe190a8ae16c2c88560/index.mpd", + "provider": "aws" + }, + { + "name": "LiveSIM Caminandes 02, Gran Dillama (25fps, 25gop, 2sec, multi MOOF/MDAT, 1080p, KID=1236) v2", + "url": "http://refapp.hbbtv.org/livesim/02_llamav2/manifest.mpd", + "provider": "hbbtv" } ] }, @@ -214,37 +288,21 @@ { "url": "https://akamaibroadcasteruseast.akamaized.net/cmaf/live/657078/akasource/out.mpd", "name": "Akamai Low Latency Stream (Single Rate)", - "bufferConfig": { - "lowLatencyMode": true, - "liveDelay": 3 - }, "provider": "akamai" }, { "url": "https://cmafref.akamaized.net/cmaf/live-ull/2006350/akambr/out.mpd", "name": "Akamai Low Latency Stream (Multi Rate)", - "bufferConfig": { - "lowLatencyMode": true, - "liveDelay": 3 - }, "provider": "akamai" }, { "url": "https://livesim.dashif.org/livesim/chunkdur_1/ato_7/testpic4_8s/Manifest300.mpd", "name": "Low Latency (Single-Rate) (livesim-chunked)", - "bufferConfig": { - "lowLatencyMode": true, - "liveDelay": 4 - }, "provider": "dashif" }, { "url": "https://livesim.dashif.org/livesim/chunkdur_1/ato_7/testpic4_8s/Manifest.mpd", "name": "Low Latency (Multi-Rate) (livesim-chunked)", - "bufferConfig": { - "lowLatencyMode": true, - "liveDelay": 4 - }, "provider": "dashif" } ] @@ -253,7 +311,7 @@ "name": "Subtitles and Captions", "submenu": [ { - "url": "https://dash.akamaized.net/akamai/test/caption_test/ElephantsDream/elephants_dream_480p_heaac5_1.mpd", + "url": "https://dash.akamaized.net/akamai/test/caption_test/ElephantsDream/elephants_dream_480p_heaac5_1_https.mpd", "name": "External VTT subtitle file", "provider": "dashif" }, @@ -326,6 +384,209 @@ { "name": "DRM (modern)", "submenu": [ + { + "name": "Multiperiod - Number + Timeline - Compact manifest - Thumbnails (1 track) - Encryption (1 key) PlayReady/Widevine (DRMtoday) - Key rotation (60s)", + "url": "https://d24rwxnt7vw9qb.cloudfront.net/v1/dash/e6d234965645b411ad572802b6c9d5a10799c9c1/All_Reference_Streams/2fc23947945841b9b1be9768f9c13e75/index.mpd", + "protData": { + "com.widevine.alpha": { + "serverURL": "https://lic.staging.drmtoday.com/license-proxy-widevine/cenc/?specConform=true", + "httpRequestHeaders": { + "x-dt-custom-data": "ewogICAgInVzZXJJZCI6ICJhd3MtZWxlbWVudGFsOjpzcGVrZS10ZXN0aW5nIiwKICAgICJzZXNzaW9uSWQiOiAiZWxlbWVudGFsLXJlZnN0cmVhbSIsCiAgICAibWVyY2hhbnQiOiAiYXdzLWVsZW1lbnRhbCIKfQo=" + } + }, + "com.microsoft.playready": { + "serverURL": "https://lic.staging.drmtoday.com/license-proxy-headerauth/drmtoday/RightsManager.asmx", + "httpRequestHeaders": { + "x-dt-custom-data": "ewogICAgInVzZXJJZCI6ICJhd3MtZWxlbWVudGFsOjpzcGVrZS10ZXN0aW5nIiwKICAgICJzZXNzaW9uSWQiOiAiZWxlbWVudGFsLXJlZnN0cmVhbSIsCiAgICAibWVyY2hhbnQiOiAiYXdzLWVsZW1lbnRhbCIKfQo=" + } + } + }, + "provider": "aws" + }, + { + "name": "Multiperiod - Number + Timeline - Compact manifest - Thumbnails (1 track) - Encryption (2 keys : audio + video) - No key rotation", + "url": "https://d24rwxnt7vw9qb.cloudfront.net/v1/dash/e6d234965645b411ad572802b6c9d5a10799c9c1/All_Reference_Streams//6e16c26536564c2f9dbc5f725a820cff/index.mpd", + "protData": { + "com.widevine.alpha": { + "serverURL": "https://lic.staging.drmtoday.com/license-proxy-widevine/cenc/?specConform=true", + "httpRequestHeaders": { + "x-dt-custom-data": "ewogICAgInVzZXJJZCI6ICJhd3MtZWxlbWVudGFsOjpzcGVrZS10ZXN0aW5nIiwKICAgICJzZXNzaW9uSWQiOiAiZWxlbWVudGFsLXJlZnN0cmVhbSIsCiAgICAibWVyY2hhbnQiOiAiYXdzLWVsZW1lbnRhbCIKfQo=" + } + }, + "com.microsoft.playready": { + "serverURL": "https://lic.staging.drmtoday.com/license-proxy-headerauth/drmtoday/RightsManager.asmx", + "httpRequestHeaders": { + "x-dt-custom-data": "ewogICAgInVzZXJJZCI6ICJhd3MtZWxlbWVudGFsOjpzcGVrZS10ZXN0aW5nIiwKICAgICJzZXNzaW9uSWQiOiAiZWxlbWVudGFsLXJlZnN0cmVhbSIsCiAgICAibWVyY2hhbnQiOiAiYXdzLWVsZW1lbnRhbCIKfQo=" + } + } + }, + "provider": "aws" + }, + { + "name": "Shaka Demo Assets: Angel-One Widevine", + "url": "https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine/dash.mpd", + "protData": { + "com.widevine.alpha": { + "serverURL": "https://cwip-shaka-proxy.appspot.com/no_auth" + } + }, + "moreInfo": "https://github.com/Axinom/dash-test-vectors", + "provider": "google" + }, + { + "name": "Single-period, 1080p, H.264, 5 video, 3 audio, 3 text tracks, CMAF, cbcs encryption, single key, Widevine+PlayReady", + "url": "https://media.axprod.net/TestVectors/Cmaf/protected_1080p_h264_cbcs/manifest.mpd", + "protData": { + "com.widevine.alpha": { + "serverURL": "https://7975c5c6.drm-widevine-licensing.axtest.net/AcquireLicense", + "httpRequestHeaders": { + "X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICIzMDJmODBkZC00MTFlLTQ4ODYtYmNhNS1iYjFmODAxOGEwMjQiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAicm9LQWcwdDdKaTFpNDNmd3YremZ0UT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ._NfhLVY7S6k8TJDWPeMPhUawhympnrk6WAZHOVjER6M" + }, + "httpTimeout": 5000 + }, + "com.microsoft.playready": { + "serverURL": "https://7975c5c6.drm-playready-licensing.axtest.net/AcquireLicense", + "httpRequestHeaders": { + "X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICIzMDJmODBkZC00MTFlLTQ4ODYtYmNhNS1iYjFmODAxOGEwMjQiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAicm9LQWcwdDdKaTFpNDNmd3YremZ0UT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ._NfhLVY7S6k8TJDWPeMPhUawhympnrk6WAZHOVjER6M" + }, + "httpTimeout": 5000 + } + }, + "moreInfo": "https://github.com/Axinom/public-test-vectors", + "provider": "axinom" + }, + { + "name": "Single-period, 1080p, H.264, 5 video, 3 audio, 3 text tracks, CMAF, cbcs encryption, multi key, Widevine+PlayReady", + "url": "https://media.axprod.net/TestVectors/MultiKey/Cmaf_h264_1080p_cbcs/manifest.mpd", + "protData": { + "com.widevine.alpha": { + "serverURL": "https://7975c5c6.drm-widevine-licensing.axtest.net/AcquireLicense", + "httpRequestHeaders": { + "X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICJiNTRlYzkxNC0xOTJkLTRlYTEtYWMxOS1mNDI5ZWI0OTgyNjgiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAiR1ZERnJZUU9Bb1kzZmpxVVVtamswQT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAiaWQiOiAiYzgzYzRlYTgtMGYyYS00NTIzLTg1MWMtZmJlY2NkYzBmMjAyIiwKICAgICAgICAgICJlbmNyeXB0ZWRfa2V5IjogIlRKZGZsWmJLYmZXQXl5K1dta21UUEE9PSIsCiAgICAgICAgICAidXNhZ2VfcG9saWN5IjogIlBvbGljeSBBIgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgImlkIjogImM4NjhjNzAyLWM3MWItNDA2NC1hZTJiLWMyNGY3Y2MxMDc5MiIsCiAgICAgICAgICAiZW5jcnlwdGVkX2tleSI6ICJ4QXJpUkpOcUFTdXp6RExDRzNXSjdnPT0iLAogICAgICAgICAgInVzYWdlX3BvbGljeSI6ICJQb2xpY3kgQSIKICAgICAgICB9CiAgICAgIF0KICAgIH0sCiAgICAiY29udGVudF9rZXlfdXNhZ2VfcG9saWNpZXMiOiBbCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJQb2xpY3kgQSIsCiAgICAgICAgInBsYXlyZWFkeSI6IHsKICAgICAgICAgICJtaW5fZGV2aWNlX3NlY3VyaXR5X2xldmVsIjogMTUwLAogICAgICAgICAgInBsYXlfZW5hYmxlcnMiOiBbCiAgICAgICAgICAgICI3ODY2MjdEOC1DMkE2LTQ0QkUtOEY4OC0wOEFFMjU1QjAxQTciCiAgICAgICAgICBdCiAgICAgICAgfQogICAgICB9CiAgICBdCiAgfQp9.XC0YIbZpKGFc3IZROklP4LvISc6cZGpE9UL-XcpcqWg" + }, + "httpTimeout": 5000 + }, + "com.microsoft.playready": { + "serverURL": "https://7975c5c6.drm-playready-licensing.axtest.net/AcquireLicense", + "httpRequestHeaders": { + "X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICJiNTRlYzkxNC0xOTJkLTRlYTEtYWMxOS1mNDI5ZWI0OTgyNjgiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAiR1ZERnJZUU9Bb1kzZmpxVVVtamswQT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAiaWQiOiAiYzgzYzRlYTgtMGYyYS00NTIzLTg1MWMtZmJlY2NkYzBmMjAyIiwKICAgICAgICAgICJlbmNyeXB0ZWRfa2V5IjogIlRKZGZsWmJLYmZXQXl5K1dta21UUEE9PSIsCiAgICAgICAgICAidXNhZ2VfcG9saWN5IjogIlBvbGljeSBBIgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgImlkIjogImM4NjhjNzAyLWM3MWItNDA2NC1hZTJiLWMyNGY3Y2MxMDc5MiIsCiAgICAgICAgICAiZW5jcnlwdGVkX2tleSI6ICJ4QXJpUkpOcUFTdXp6RExDRzNXSjdnPT0iLAogICAgICAgICAgInVzYWdlX3BvbGljeSI6ICJQb2xpY3kgQSIKICAgICAgICB9CiAgICAgIF0KICAgIH0sCiAgICAiY29udGVudF9rZXlfdXNhZ2VfcG9saWNpZXMiOiBbCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJQb2xpY3kgQSIsCiAgICAgICAgInBsYXlyZWFkeSI6IHsKICAgICAgICAgICJtaW5fZGV2aWNlX3NlY3VyaXR5X2xldmVsIjogMTUwLAogICAgICAgICAgInBsYXlfZW5hYmxlcnMiOiBbCiAgICAgICAgICAgICI3ODY2MjdEOC1DMkE2LTQ0QkUtOEY4OC0wOEFFMjU1QjAxQTciCiAgICAgICAgICBdCiAgICAgICAgfQogICAgICB9CiAgICBdCiAgfQp9.XC0YIbZpKGFc3IZROklP4LvISc6cZGpE9UL-XcpcqWg" + }, + "httpTimeout": 5000 + } + }, + "moreInfo": "https://github.com/Axinom/public-test-vectors", + "provider": "axinom" + }, + { + "name": "Single-period, 1080p, H.265, 5 video, 3 audio, 3 text tracks, CMAF, cbcs encryption, single key, Widevine+PlayReady", + "url": "https://media.axprod.net/TestVectors/H265/protected_cmaf_1080p_h265_singlekey/manifest.mpd", + "protData": { + "com.widevine.alpha": { + "serverURL": "https://7975c5c6.drm-widevine-licensing.axtest.net/AcquireLicense", + "httpRequestHeaders": { + "X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICJhYmNjNDRlNS1jMTIyLTQ1YWItYWM4MC1hNWIzNTIyYTBhMzEiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAiZnM2VUx1UzR3SFQxdkI2M0RONnI5UT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ.5rM_qUo4dKrHNDKQO0yzbCiufJxFUzHeOQc13Z48rv4" + }, + "httpTimeout": 5000 + }, + "com.microsoft.playready": { + "serverURL": "https://7975c5c6.drm-playready-licensing.axtest.net/AcquireLicense", + "httpRequestHeaders": { + "X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICJhYmNjNDRlNS1jMTIyLTQ1YWItYWM4MC1hNWIzNTIyYTBhMzEiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAiZnM2VUx1UzR3SFQxdkI2M0RONnI5UT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ.5rM_qUo4dKrHNDKQO0yzbCiufJxFUzHeOQc13Z48rv4" + }, + "httpTimeout": 5000 + } + }, + "moreInfo": "https://github.com/Axinom/public-test-vectors", + "provider": "axinom" + }, + { + "name": "Single-period, 1080p, H.265, 5 video, 3 audio, 3 text tracks, CMAF, cbcs encryption, multi key, Widevine+PlayReady", + "url": "https://media.axprod.net/TestVectors/H265/protected_cmaf_1080p_h265_multikey/manifest.mpd", + "protData": { + "com.widevine.alpha": { + "serverURL": "https://7975c5c6.drm-widevine-licensing.axtest.net/AcquireLicense", + "httpRequestHeaders": { + "X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICI1M2RjM2VhYS01MTY0LTQxMGEtOGY0ZS1lMTUxMTNiNDMwNDAiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAiSk00UnNXR0M5dVpjd1llRk5NakNPdz09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAiaWQiOiAiOWRiYWNlOWUtNDEwMy00YzUyLTk2YWEtNjMyMjdkYzVmNzczIiwKICAgICAgICAgICJlbmNyeXB0ZWRfa2V5IjogInliTUNkUkRnamgvR215cG9mTVdDa3c9PSIsCiAgICAgICAgICAidXNhZ2VfcG9saWN5IjogIlBvbGljeSBBIgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgImlkIjogImE3NmYwY2E2LThlN2QtNDBkMC04YTM3LTkwNmYzZTI0ZGRlMiIsCiAgICAgICAgICAiZW5jcnlwdGVkX2tleSI6ICJTTnlTSFlEZ3MzYkJtamhPTlh5SmRBPT0iLAogICAgICAgICAgInVzYWdlX3BvbGljeSI6ICJQb2xpY3kgQSIKICAgICAgICB9CiAgICAgIF0KICAgIH0sCiAgICAiY29udGVudF9rZXlfdXNhZ2VfcG9saWNpZXMiOiBbCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJQb2xpY3kgQSIsCiAgICAgICAgInBsYXlyZWFkeSI6IHsKICAgICAgICAgICJtaW5fZGV2aWNlX3NlY3VyaXR5X2xldmVsIjogMTUwLAogICAgICAgICAgInBsYXlfZW5hYmxlcnMiOiBbCiAgICAgICAgICAgICI3ODY2MjdEOC1DMkE2LTQ0QkUtOEY4OC0wOEFFMjU1QjAxQTciCiAgICAgICAgICBdCiAgICAgICAgfQogICAgICB9CiAgICBdCiAgfQp9.SSRguglJk2l3VahbSq8N5O4Qhxv78n2gSL5Za8HZJmk" + }, + "httpTimeout": 5000 + }, + "com.microsoft.playready": { + "serverURL": "https://7975c5c6.drm-playready-licensing.axtest.net/AcquireLicense", + "httpRequestHeaders": { + "X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICI1M2RjM2VhYS01MTY0LTQxMGEtOGY0ZS1lMTUxMTNiNDMwNDAiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAiSk00UnNXR0M5dVpjd1llRk5NakNPdz09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAiaWQiOiAiOWRiYWNlOWUtNDEwMy00YzUyLTk2YWEtNjMyMjdkYzVmNzczIiwKICAgICAgICAgICJlbmNyeXB0ZWRfa2V5IjogInliTUNkUkRnamgvR215cG9mTVdDa3c9PSIsCiAgICAgICAgICAidXNhZ2VfcG9saWN5IjogIlBvbGljeSBBIgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgImlkIjogImE3NmYwY2E2LThlN2QtNDBkMC04YTM3LTkwNmYzZTI0ZGRlMiIsCiAgICAgICAgICAiZW5jcnlwdGVkX2tleSI6ICJTTnlTSFlEZ3MzYkJtamhPTlh5SmRBPT0iLAogICAgICAgICAgInVzYWdlX3BvbGljeSI6ICJQb2xpY3kgQSIKICAgICAgICB9CiAgICAgIF0KICAgIH0sCiAgICAiY29udGVudF9rZXlfdXNhZ2VfcG9saWNpZXMiOiBbCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJQb2xpY3kgQSIsCiAgICAgICAgInBsYXlyZWFkeSI6IHsKICAgICAgICAgICJtaW5fZGV2aWNlX3NlY3VyaXR5X2xldmVsIjogMTUwLAogICAgICAgICAgInBsYXlfZW5hYmxlcnMiOiBbCiAgICAgICAgICAgICI3ODY2MjdEOC1DMkE2LTQ0QkUtOEY4OC0wOEFFMjU1QjAxQTciCiAgICAgICAgICBdCiAgICAgICAgfQogICAgICB9CiAgICBdCiAgfQp9.SSRguglJk2l3VahbSq8N5O4Qhxv78n2gSL5Za8HZJmk" + }, + "httpTimeout": 5000 + } + }, + "moreInfo": "https://github.com/Axinom/public-test-vectors", + "provider": "axinom" + }, + { + "name": "Single-period, 1080p, H.264, 5 video, 3 audio, 3 text tracks, DASH, cenc encryption, single key, Widevine+PlayReady", + "url": "https://media.axprod.net/TestVectors/Dash/protected_dash_1080p_h264_singlekey/manifest.mpd", + "protData": { + "com.widevine.alpha": { + "serverURL": "https://7975c5c6.drm-widevine-licensing.axtest.net/AcquireLicense", + "httpRequestHeaders": { + "X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICI0MDYwYTg2NS04ODc4LTQyNjctOWNiZi05MWFlNWJhZTFlNzIiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAid3QzRW51dVI1UkFybjZBRGYxNkNCQT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ.l8PnZznspJ6lnNmfAE9UQV532Ypzt1JXQkvrk8gFSRw" + }, + "httpTimeout": 5000 + }, + "com.microsoft.playready": { + "serverURL": "https://7975c5c6.drm-playready-licensing.axtest.net/AcquireLicense", + "httpRequestHeaders": { + "X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICI0MDYwYTg2NS04ODc4LTQyNjctOWNiZi05MWFlNWJhZTFlNzIiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAid3QzRW51dVI1UkFybjZBRGYxNkNCQT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ.l8PnZznspJ6lnNmfAE9UQV532Ypzt1JXQkvrk8gFSRw" + }, + "httpTimeout": 5000 + } + }, + "moreInfo": "https://github.com/Axinom/public-test-vectors", + "provider": "axinom" + }, + { + "name": "Single-period, 1080p, H.264, 5 video, 3 audio, 3 text tracks, DASH, cenc encryption, multi key, Widevine+PlayReady", + "url": "https://media.axprod.net/TestVectors/MultiKey/Dash_h264_1080p_cenc/manifest.mpd", + "protData": { + "com.widevine.alpha": { + "serverURL": "https://7975c5c6.drm-widevine-licensing.axtest.net/AcquireLicense", + "httpRequestHeaders": { + "X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICI0MjZkMWEzMi03OGZkLTRmMjItODczMC02OGRiMzk3NGRkYTkiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAiZjFsLy95M0dnN3pFVE9qM1ZQTXovQT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAiaWQiOiAiOWRjOGU4MGEtY2JmYS00MWMzLTk4NGYtYjYwNDM0NDAzOTFhIiwKICAgICAgICAgICJlbmNyeXB0ZWRfa2V5IjogInlxOW9pSjJ0QnQ1bkpFM1VENE53bXc9PSIsCiAgICAgICAgICAidXNhZ2VfcG9saWN5IjogIlBvbGljeSBBIgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgImlkIjogIjQxYmFhNTk5LTY5MDUtNGZjMC1hOGM2LTM1NWRjZDFhYjM5ZiIsCiAgICAgICAgICAiZW5jcnlwdGVkX2tleSI6ICJ0ZWhGVGhwK2RpMUFHSHM2eGdySjBRPT0iLAogICAgICAgICAgInVzYWdlX3BvbGljeSI6ICJQb2xpY3kgQSIKICAgICAgICB9CiAgICAgIF0KICAgIH0sCiAgICAiY29udGVudF9rZXlfdXNhZ2VfcG9saWNpZXMiOiBbCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJQb2xpY3kgQSIsCiAgICAgICAgInBsYXlyZWFkeSI6IHsKICAgICAgICAgICJtaW5fZGV2aWNlX3NlY3VyaXR5X2xldmVsIjogMTUwLAogICAgICAgICAgInBsYXlfZW5hYmxlcnMiOiBbCiAgICAgICAgICAgICI3ODY2MjdEOC1DMkE2LTQ0QkUtOEY4OC0wOEFFMjU1QjAxQTciCiAgICAgICAgICBdCiAgICAgICAgfQogICAgICB9CiAgICBdCiAgfQp9.KpLCxibrW87lZwA_CSuZdqj7u0L-lnt-e3z_M1Toas0" + }, + "httpTimeout": 5000 + }, + "com.microsoft.playready": { + "serverURL": "https://7975c5c6.drm-playready-licensing.axtest.net/AcquireLicense", + "httpRequestHeaders": { + "X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICI0MjZkMWEzMi03OGZkLTRmMjItODczMC02OGRiMzk3NGRkYTkiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAiZjFsLy95M0dnN3pFVE9qM1ZQTXovQT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAiaWQiOiAiOWRjOGU4MGEtY2JmYS00MWMzLTk4NGYtYjYwNDM0NDAzOTFhIiwKICAgICAgICAgICJlbmNyeXB0ZWRfa2V5IjogInlxOW9pSjJ0QnQ1bkpFM1VENE53bXc9PSIsCiAgICAgICAgICAidXNhZ2VfcG9saWN5IjogIlBvbGljeSBBIgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgImlkIjogIjQxYmFhNTk5LTY5MDUtNGZjMC1hOGM2LTM1NWRjZDFhYjM5ZiIsCiAgICAgICAgICAiZW5jcnlwdGVkX2tleSI6ICJ0ZWhGVGhwK2RpMUFHSHM2eGdySjBRPT0iLAogICAgICAgICAgInVzYWdlX3BvbGljeSI6ICJQb2xpY3kgQSIKICAgICAgICB9CiAgICAgIF0KICAgIH0sCiAgICAiY29udGVudF9rZXlfdXNhZ2VfcG9saWNpZXMiOiBbCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJQb2xpY3kgQSIsCiAgICAgICAgInBsYXlyZWFkeSI6IHsKICAgICAgICAgICJtaW5fZGV2aWNlX3NlY3VyaXR5X2xldmVsIjogMTUwLAogICAgICAgICAgInBsYXlfZW5hYmxlcnMiOiBbCiAgICAgICAgICAgICI3ODY2MjdEOC1DMkE2LTQ0QkUtOEY4OC0wOEFFMjU1QjAxQTciCiAgICAgICAgICBdCiAgICAgICAgfQogICAgICB9CiAgICBdCiAgfQp9.KpLCxibrW87lZwA_CSuZdqj7u0L-lnt-e3z_M1Toas0" + }, + "httpTimeout": 5000 + } + }, + "moreInfo": "https://github.com/Axinom/public-test-vectors", + "provider": "axinom" + }, + { + "name": "Single-period, 1080p, H.265, 5 video, 3 audio, 3 text tracks, DASH, cenc encryption, single key, Widevine+PlayReady", + "url": "https://media.axprod.net/TestVectors/H265/protected_dash_1080p_h265_singlekey/manifest.mpd", + "protData": { + "com.widevine.alpha": { + "serverURL": "https://7975c5c6.drm-widevine-licensing.axtest.net/AcquireLicense", + "httpRequestHeaders": { + "X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICI5ZmQzODVkNS1mMzg5LTQ4YjUtYjdjMy1iMTg2M2VlMTA4ODgiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAiS3ZhaytZZVF1NGU2QnRvcEQ2Wm1JUT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ.CNEEm6UhOFiXadbcxQrs64NEb9ys7YdPZ7TmTO8aTbg" + }, + "httpTimeout": 5000 + }, + "com.microsoft.playready": { + "serverURL": "https://7975c5c6.drm-playready-licensing.axtest.net/AcquireLicense", + "httpRequestHeaders": { + "X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICI5ZmQzODVkNS1mMzg5LTQ4YjUtYjdjMy1iMTg2M2VlMTA4ODgiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAiS3ZhaytZZVF1NGU2QnRvcEQ2Wm1JUT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ.CNEEm6UhOFiXadbcxQrs64NEb9ys7YdPZ7TmTO8aTbg" + }, + "httpTimeout": 5000 + } + }, + "moreInfo": "https://github.com/Axinom/public-test-vectors", + "provider": "axinom" + }, { "name": "1080p with Widevine DRM, license expired after 60s", "url": "https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p.mpd", @@ -729,29 +990,9 @@ }, "provider": "microsoft" }, - { - "name": "Source: XBox One commercial video", - "url": "https://profficialsite.origin.mediaservices.windows.net/9cc5e871-68ec-42c2-9fc7-fda95521f17d/dayoneplayready.ism/manifest(format=mpd-time-csf)", - "protData": { - "com.microsoft.playready": { - "serverURL": "https://test.playready.microsoft.com/service/rightsmanager.asmx" - } - }, - "provider": "microsoft" - }, - { - "name": "AZURE MEDIA SERVICES LIVE PLAYREADY 2.0", - "url": "https://profficialsite.origin.mediaservices.windows.net/9cc5e871-68ec-42c2-9fc7-fda95521f17d/dayoneplayready.ism/manifest(format=mpd-time-csf)", - "protData": { - "com.microsoft.playready": { - "serverURL": "https://test.playready.microsoft.com/service/rightsmanager.asmx" - } - }, - "provider": "microsoft" - }, { "name": "Unified Streaming (Widevine, persistent)", - "url": "//demo.unified-streaming.com/video/tears-of-steel/tears-of-steel-dash-widevine.ism/.mpd", + "url": "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-dash-widevine.ism/.mpd", "protData": { "com.widevine.alpha": { "serverURL": "https://cwip-shaka-proxy.appspot.com/no_auth", @@ -761,64 +1002,34 @@ "provider": "unified" }, { - "name": "Widevine Dynamic SegmentTimeline", - "url": "https://wowzaec2demo.streamlock.net/live/bigbuckbunny-enc-wv.stream/manifest_mvtime.mpd", - "protData": { - "com.widevine.alpha": { - "serverURL": "https://widevine-proxy.appspot.com/proxy" - } - }, - "provider": "wowza" - }, - { - "name": "Widevine Dynamic SegmentTemplate", - "url": "https://wowzaec2demo.streamlock.net/live/bigbuckbunny-enc-wv.stream/manifest_mvnumber.mpd", + "name": "Live Dash WV and PR with unencrypted ad breaks -- Always starts in encrypted content - Keys never change", + "url": "https://content.uplynk.com/playlist/6c526d97954b41deb90fe64328647a71.mpd?ad=bbbads&delay=25", "protData": { "com.widevine.alpha": { - "serverURL": "https://widevine-proxy.appspot.com/proxy" - } - }, - "provider": "wowza" - }, - { - "name": "Widevine Dynamic SegmentList", - "url": "https://wowzaec2demo.streamlock.net/live/bigbuckbunny-enc-wv.stream/manifest_mvlist.mpd", - "protData": { - "com.widevine.alpha": { - "serverURL": "https://widevine-proxy.appspot.com/proxy" - } - }, - "provider": "wowza" - }, - { - "name": "Widevine Static SegmentTimeline", - "url": "https://wowzaec2demo.streamlock.net/vod/elephantsdream_1100kbps-enc-wv.mp4/manifest_mvtime.mpd", - "protData": { - "com.widevine.alpha": { - "serverURL": "https://widevine-proxy.appspot.com/proxy" - } - }, - "provider": "wowza" - }, - { - "name": "Widevine Static SegmentTemplate", - "url": "https://wowzaec2demo.streamlock.net/vod/elephantsdream_1100kbps-enc-wv.mp4/manifest_mvnumber.mpd", - "protData": { - "com.widevine.alpha": { - "serverURL": "https://widevine-proxy.appspot.com/proxy" + "serverURL": "https://content.uplynk.com/wv", + "httpTimeout": 5000 + }, + "com.microsoft.playready": { + "serverURL": "https://content.uplynk.com/pr", + "httpTimeout": 5000 } }, - "provider": "wowza" + "provider": "vdms" }, { - "name": "Widevine Static SegmentList", - "url": "https://wowzaec2demo.streamlock.net/vod/elephantsdream_1100kbps-enc-wv.mp4/manifest_mvlist.mpd ", + "name": "Live Dash WV and PR - Starting in unencrypted ad (preroll) - Moving into encrypted content - Keys never change", + "url": "https://content.uplynk.com/playlist/4f1a9815a1af43d5ba64465d85bf11cf.mpd?ad=sintelads", "protData": { "com.widevine.alpha": { - "serverURL": "https://widevine-proxy.appspot.com/proxy" + "serverURL": "https://content.uplynk.com/wv", + "httpTimeout": 5000 + }, + "com.microsoft.playready": { + "serverURL": "https://content.uplynk.com/pr", + "httpTimeout": 5000 } }, - "provider": "wowza" + "provider": "vdms" } ] }, @@ -988,19 +1199,14 @@ "url": "//livesim.dashif.org/livesim/testpic_2s/Manifest_thumbs.mpd", "provider": "dashif" }, - { - "name": "SegmentBase, Single adaption set, 3x4 tiles", - "url": "https://demo.unified-streaming.com/video/tears-of-steel/tears-of-steel-tiled-thumbnails-static.mpd", - "provider": "unified" - }, { "name": "SegmentTemplate with SegmentTimeline", - "url": "https://demo.unified-streaming.com/video/tears-of-steel/tears-of-steel-tiled-thumbnails-timeline.ism/.mpd", + "url": "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-tiled-thumbnails-timeline.ism/.mpd", "provider": "unified" }, { "name": "SegmentNumber", - "url": "https://demo.unified-streaming.com/video/tears-of-steel/tears-of-steel-tiled-thumbnails-numbered.ism/.mpd", + "url": "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-tiled-thumbnails-numbered.ism/.mpd", "provider": "unified" } ] diff --git a/samples/dash-if-reference-player/dashjs_config.json b/samples/dash-if-reference-player/dashjs_config.json index d4355a3c85..49aa67c987 100644 --- a/samples/dash-if-reference-player/dashjs_config.json +++ b/samples/dash-if-reference-player/dashjs_config.json @@ -1,72 +1,5 @@ { "debug": { - "logLevel": 5 - }, - "streaming": { - "metricsMaxListDepth": 50, - "abandonLoadTimeout": 10000, - "liveDelayFragmentCount": null, - "liveDelay": null, - "scheduleWhilePaused": true, - "fastSwitchEnabled": true, - "flushBufferAtTrackSwitch": false, - "bufferPruningInterval": 10, - "bufferToKeep": 20, - "jumpGaps": true, - "smallGapLimit": 1.5, - "stableBufferTime": -1, - "bufferTimeAtTopQuality": 30, - "bufferTimeAtTopQualityLongForm": 60, - "longFormContentDurationThreshold": 600, - "wallclockTimeUpdateInterval": 50, - "lowLatencyEnabled": false, - "keepProtectionMediaKeys": false, - "useManifestDateHeaderTimeSource": true, - "useSuggestedPresentationDelay": true, - "manifestUpdateRetryInterval": 100, - "liveCatchup": { - "minDrift": 0.02, - "maxDrift": 0, - "playbackRate": 0.5, - "latencyThreshold": null, - "enabled": false - }, - "lastBitrateCachingInfo": { "enabled": true, "ttl": 360000}, - "lastMediaSettingsCachingInfo": { "enabled": true, "ttl": 360000}, - "cacheLoadThresholds": {"video": 50, "audio": 5}, - "retryIntervals": { - "MPD": 500, - "XLinkExpansion": 500, - "MediaSegment": 1000, - "InitializationSegment": 1000, - "BitstreamSwitchingSegment": 1000, - "IndexSegment": 1000, - "other": 1000 - }, - "retryAttempts": { - "MPD": 3, - "XLinkExpansion": 1, - "MediaSegment": 3, - "InitializationSegment": 3, - "BitstreamSwitchingSegment": 3, - "IndexSegment": 3, - "other": 3 - }, - "abr": { - "movingAverageMethod": "slidingWindow", - "ABRStrategy": "abrDynamic", - "bandwidthSafetyFactor": 0.9, - "useDefaultABRRules": true, - "useBufferOccupancyABR": false, - "useDeadTimeLatency": true, - "limitBitrateByPortal": false, - "usePixelRatioInLimitBitrateByPortal": false, - "maxBitrate": { "audio": -1, "video": -1 }, - "minBitrate": { "audio": -1, "video": -1 }, - "maxRepresentationRatio": { "audio": 1, "video": 1 }, - "initialBitrate": { "audio": -1, "video": -1 }, - "initialRepresentationRatio": { "audio": -1, "video": -1 }, - "autoSwitchBitrate": { "audio": true, "video": true } - } + "logLevel": 4 } } \ No newline at end of file diff --git a/samples/dash-if-reference-player/index.html b/samples/dash-if-reference-player/index.html index c623e409df..40d71329bc 100644 --- a/samples/dash-if-reference-player/index.html +++ b/samples/dash-if-reference-player/index.html @@ -92,7 +92,6 @@
-
@@ -132,8 +131,13 @@ ng-cloak>{{getOptionsButtonLabel()}} + +
+
+
URL Copied!
+
@@ -141,20 +145,6 @@
Playback
-
@@ -282,13 +307,15 @@
@@ -299,17 +326,29 @@ +
@@ -318,53 +357,348 @@
DRM Options
-
-
Initial Settings
+
Live delay
- + + + + + +
+
+
+
Initial Settings
+
- + - + - + + ng-model="initialSettings.audio" ng-change="updateInitialLanguageAudio()"> + ng-model="initialSettings.video" ng-change="updateInitialRoleVideo()"> + ng-model="initialSettings.text" ng-change="updateInitialLanguageText()"> + ng-model="initialSettings.textRole" ng-change="updateInitialRoleText()"> @@ -377,26 +711,32 @@
@@ -408,36 +748,42 @@ @@ -453,29 +799,40 @@ Enable CMCD Reporting - + - + - + - + + +
@@ -538,6 +895,20 @@
+ +
+
UpdatedExport settings
+ Our export settings feature creates shorter URLs now. + Click on "Copy Settings URL" on the top right and paste the URL in the address bar of your browser. The + current settings are compared to the default settings and the difference is stored using query parameters. +
+
+ + Additional samples can be found in the Sample Section. +
{{videoIndex}} / {{videoMaxIndex}}
@@ -633,6 +1004,14 @@ title="Difference between live time and current playback position in seconds. This latency estimate does not include the time taken by the encoder to encode the content">Live Latency: {{videoLiveLatency}}
+
+ + {{videoPlaybackRate}} +
@@ -714,6 +1093,14 @@ title="Number of seconds of difference between the real live and the playing live">Live Latency: {{audioLiveLatency}}
+
+ + {{audioPlaybackRate}} +
diff --git a/samples/drm/clearkey.html b/samples/drm/clearkey.html index f40bc38bfe..816be7ae6f 100644 --- a/samples/drm/clearkey.html +++ b/samples/drm/clearkey.html @@ -1,13 +1,21 @@ - + - + Clearkey DRM instantiation example - - + + + + + + - - + -
- -
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+

Clearkey DRM instantiation example

+

This example shows how to use dash.js to play streams with Clearkey DRM protection.

For a detailed explanation on DRM playback in dash.js checkout the + Wiki.

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + - - diff --git a/samples/drm/dashif-laurl.html b/samples/drm/dashif-laurl.html new file mode 100644 index 0000000000..76f9f135f8 --- /dev/null +++ b/samples/drm/dashif-laurl.html @@ -0,0 +1,112 @@ + + + + + License server via MPD example + + + + + + + + + + + + + +
+
+
+ +
+
+
+ +
+
+
+
+
+

License server via MPD example

+

This example shows how to specify the license server url as part of the MPD using + 'dashif:laurl'.

+

For a detailed explanation on this checkout the + Wiki.

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/drm/keepProtectionKeys.html b/samples/drm/keepProtectionKeys.html new file mode 100644 index 0000000000..dcddf64b36 --- /dev/null +++ b/samples/drm/keepProtectionKeys.html @@ -0,0 +1,136 @@ + + + + + DRM - Keep MediaKeySession example + + + + + + + + + + + + + +
+
+
+ +
+
+
+ +
+
+
+
+
+

DRM - Keep MediaKeySession

+

The ProtectionController and the created MediaKeys and MediaKeySessions will be preserved during + the MediaPlayer lifetime.

+

To observe the effect of the "keepProtectionMediaKeys" + property open the developer tools and check the network requests for license request. Only for + the first playback attempt license requests will be visible. For any subsequent playback attempt + the existing MediaKeySession is reused and no additional license requests are performed.

+

For a detailed explanation on DRM playback in dash.js checkout the + Wiki.

+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/drm/license-wrapping.html b/samples/drm/license-wrapping.html index 5093278ad4..f953fe7c87 100644 --- a/samples/drm/license-wrapping.html +++ b/samples/drm/license-wrapping.html @@ -1,27 +1,36 @@ - + - - Widevine DRM instantiation example + + License wrapping example - - + + + + + + - - + -
- -
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+

License wrapping example

+

This example shows how to use dash.js to modify the license request and the license + repsonse.

+

For a detailed explanation on DRM playback in dash.js checkout the + Wiki.

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + - - diff --git a/samples/drm/mpds/laurl.mpd b/samples/drm/mpds/laurl.mpd new file mode 100644 index 0000000000..3793e7f4a5 --- /dev/null +++ b/samples/drm/mpds/laurl.mpd @@ -0,0 +1,87 @@ + + + + + https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/ + + + + + AAAB5HBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAcTEAQAAAQABALoBPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgBEAFEAVwAwAG4AawB2AGsAQQBrAGkAVABMAGkAZgBYAFUASQBQAGkAWgBnAD0APQA8AC8ASwBJAEQAPgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA== + + + xAEAAAEAAQC6ATwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4ARABRAFcAMABuAGsAdgBrAEEAawBpAFQATABpAGYAWABVAEkAUABpAFoAZwA9AD0APAAvAEsASQBEAD4APAAvAEQAQQBUAEEAPgA8AC8AVwBSAE0ASABFAEEARABFAFIAPgA= + + https://drm-playready-licensing.axtest.net/AcquireLicense + + + https://drm-widevine-licensing.axtest.net/AcquireLicense + AAAANHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABQIARIQnrQFDeRLSAKTLifXUIPiZg== + + + + + + + + + + + + + + AAAB5HBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAcTEAQAAAQABALoBPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgBEAFEAVwAwAG4AawB2AGsAQQBrAGkAVABMAGkAZgBYAFUASQBQAGkAWgBnAD0APQA8AC8ASwBJAEQAPgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA== + + + xAEAAAEAAQC6ATwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4ARABRAFcAMABuAGsAdgBrAEEAawBpAFQATABpAGYAWABVAEkAUABpAFoAZwA9AD0APAAvAEsASQBEAD4APAAvAEQAQQBUAEEAPgA8AC8AVwBSAE0ASABFAEEARABFAFIAPgA= + + https://drm-playready-licensing.axtest.net/AcquireLicense + + + https://drm-widevine-licensing.axtest.net/AcquireLicense + AAAANHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABQIARIQnrQFDeRLSAKTLifXUIPiZg== + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/drm/playready.html b/samples/drm/playready.html index 2614ce230a..6f7ea3e283 100644 --- a/samples/drm/playready.html +++ b/samples/drm/playready.html @@ -1,13 +1,21 @@ - + - - Playready DRM instantiation example + + PlayReady DRM instantiation example - - + + + + + + - - + -
- -
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+

PlayReady DRM instantiation example

+

This example shows how to use dash.js to play streams with PlayReady DRM protection.

For a detailed explanation on DRM playback in dash.js checkout the + Wiki.

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + - - diff --git a/samples/drm/robustness-level.html b/samples/drm/robustness-level.html new file mode 100644 index 0000000000..6cab61a992 --- /dev/null +++ b/samples/drm/robustness-level.html @@ -0,0 +1,105 @@ + + + + + DRM Robustness level example + + + + + + + + + + + + + +
+
+
+ +
+
+
+ +
+
+
+
+
+

DRM Robustness level example

+

This example shows how to define a robustness level to be used by dash.js in the requestMediaKeySystemAccess + call.

+

For a detailed explanation on DRM playback in dash.js checkout the + Wiki.

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/drm/system-priority.html b/samples/drm/system-priority.html new file mode 100644 index 0000000000..1f22df5797 --- /dev/null +++ b/samples/drm/system-priority.html @@ -0,0 +1,110 @@ + + + + + DRM system priority example + + + + + + + + + + + + + +
+
+
+ +
+
+
+ +
+
+
+
+
+

DRM system priority example

+

This example shows how to specify a DRM system priority in case the underlying platform supports + multiple DRM systems. In this example, dash.js checks for the support of Widevine before + Playready.

+

For a detailed explanation on DRM playback in dash.js checkout the + Wiki.

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/drm/system-string-priority.html b/samples/drm/system-string-priority.html new file mode 100644 index 0000000000..de5e9bfd27 --- /dev/null +++ b/samples/drm/system-string-priority.html @@ -0,0 +1,119 @@ + + + + + DRM system string priority example + + + + + + + + + + + + + +
+
+
+ +
+
+
+ +
+
+
+
+
+

DRM system string priority example

+

This example shows how to specify the system string priority for the call to + requestMediaKeySystemAccess. For example, Playready might be supported + with the system strings "com.microsoft.playready.recommendation" and + "com.microsoft.playready".

+

For a detailed explanation on DRM playback in dash.js checkout the + Wiki.

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/drm/widevine.html b/samples/drm/widevine.html index d517f21cbe..bf266305de 100644 --- a/samples/drm/widevine.html +++ b/samples/drm/widevine.html @@ -1,22 +1,31 @@ - + - + Widevine DRM instantiation example - - + + + + + + - + -
- -
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+

Widevine DRM instantiation example

+

This example shows how to use dash.js to play streams with Widevine DRM protection.

+

For a detailed explanation on DRM playback in dash.js checkout the + Wiki.

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + - - diff --git a/samples/getting-started/auto-load-multi-video.html b/samples/getting-started/auto-load-multi-video.html index d7ee8330e9..a2cc25d841 100644 --- a/samples/getting-started/auto-load-multi-video.html +++ b/samples/getting-started/auto-load-multi-video.html @@ -1,13 +1,14 @@ - + - - Auto-player instantiation example + + Auto load multi video - - + + + + - - -
- -
This is a dash.js player that should autostart -
-
- -
This is a standard mp4 (non-dash) -
-
- -
This is a dash.js player that should not autostart + + + +
+
+
+ +
+
+
+
+

Auto load multi video

+

This example shows how to auto-embed multiple instances of dash.js players in a page. To make it + more difficult, one of the available video elements specifies a non-DASH source..

+
+
+
+
+
+
This is a dash.js player that should autostart
+
+ +
+
+
+
+
+
+
This is a dash.js player that should not autostart
+
+ +
+
+
+
+
+
+
This is a dash.js player that where the manifest is defined via the src attribute of + the video element.
+
+ +
+
+
+
-
- -
This is a dash.js player that where the manifest is defined via the src attribute of the video element. +
+
+
+
- - +
+ © DASH-IF +
+
+
+ + + diff --git a/samples/getting-started/auto-load-single-video-src.html b/samples/getting-started/auto-load-single-video-src.html index 394e060be9..72aed9063d 100644 --- a/samples/getting-started/auto-load-single-video-src.html +++ b/samples/getting-started/auto-load-single-video-src.html @@ -1,13 +1,14 @@ - - + - - Auto-player instantiation example, single videoElement, using src attribute + + Auto load single video src + - - + + + + - - -
- + + + +
+
+
+ +
+
+
+
+

Auto load single video src

+

The simplest means of using a dash.js player in a web page. The mpd src is specified within the + @src attribute of the video element. The "auto-load" refers to the fact that this page calls the + Dash.createAll() method onLoad in order to automatically convert all video elements of class + 'dashjs-player' in to a functioning DASH player.

+
+
+
+
+
+ +
+
+
+
+
+
+
+
- - +
+ © DASH-IF +
+
+
+ + + diff --git a/samples/getting-started/auto-load-single-video-with-context-and-source.html b/samples/getting-started/auto-load-single-video-with-context-and-source.html deleted file mode 100644 index da559f6616..0000000000 --- a/samples/getting-started/auto-load-single-video-with-context-and-source.html +++ /dev/null @@ -1,71 +0,0 @@ - - - - - Auto-player instantiation example, single videoElement, supplying context and source - - - - - - - - - -
- -

-

- -

- -

- - - diff --git a/samples/getting-started/auto-load-single-video-with-reference.html b/samples/getting-started/auto-load-single-video-with-reference.html deleted file mode 100644 index c12521aeb8..0000000000 --- a/samples/getting-started/auto-load-single-video-with-reference.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - Auto-player instantiation example, showing how to obtain a reference to the MediaPlayer - - - - - - - - - -
- -
-
- -
- - - - - diff --git a/samples/getting-started/auto-load-single-video.html b/samples/getting-started/auto-load-single-video.html index 083c93b32f..f9a00a017f 100644 --- a/samples/getting-started/auto-load-single-video.html +++ b/samples/getting-started/auto-load-single-video.html @@ -1,13 +1,14 @@ - + - - Auto-player instantiation example, single videoElement + + Auto load single video - - + + + + - - -
- + + + +
+
+
+ +
+
+
+
+

Auto load single video

+

The mpd source is specified within the child Source element of the video element. Note that the + Source@type attribute must be set to "application/dash+xml" in order for it to be automatically + used.

+
+
+
+
+ +
+
+
+
+
+
+
- - +
+ © DASH-IF +
+
+
+ + diff --git a/samples/getting-started/controlbar.html b/samples/getting-started/controlbar.html new file mode 100644 index 0000000000..0590b575ee --- /dev/null +++ b/samples/getting-started/controlbar.html @@ -0,0 +1,108 @@ + + + + + Controlbar sample + + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Using the Control Bar

+

This example shows how to add and configure the control bar with the dash.js player

+
+
+
+
+
+ +
+
+ +
+ 00:00:00 +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+ 00:00:00 +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + diff --git a/samples/getting-started/listening-to-events.html b/samples/getting-started/listening-to-events.html index dd9ce1b21c..381f39a932 100644 --- a/samples/getting-started/listening-to-events.html +++ b/samples/getting-started/listening-to-events.html @@ -1,15 +1,33 @@ - + - - Events example + + Listening to events example - - - + + - - - -
- This sample allows you to explore the various external events that are accessible from MediaPlayer. Events can be dynamically added and removed.
- Choose the events you would like to monitor before starting playback: - -

- +

+
+
+ +
+
+
+
+

Listening to events

+

Example showing how to listen to events raised by dash.js. This sample allows you to explore the various external events that are accessible from MediaPlayer. Events can be dynamically added and removed.
+ Choose the events you would like to monitor before starting playback:

+
+ +
+
+
+
+
+

Active events

+ +
+
+
+ +
+ +
+
- -
- - -

- Events actively being listened to are shown below. Remove the listener by clicking on the event button.
+

+
+
+ + +
+
- - +
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + diff --git a/samples/getting-started/load-with-url-params.html b/samples/getting-started/load-with-url-params.html new file mode 100644 index 0000000000..9974233d48 --- /dev/null +++ b/samples/getting-started/load-with-url-params.html @@ -0,0 +1,114 @@ + + + + + Load with url parameters + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Load with url parameters

+

A demo page that uses url query parameters to configure the playback. The supported parameters + are:

+ + + + + + + + + + + + + + + + +
ParameterDescriptionDefault
autoplayEnables autoplay. Set to "true" or "false""true"
urlSpecify MPD urlhttps://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/getting-started/logging.html b/samples/getting-started/logging.html new file mode 100644 index 0000000000..94c6bb09a8 --- /dev/null +++ b/samples/getting-started/logging.html @@ -0,0 +1,77 @@ + + + + + Log Levels example + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Log levels

+

This examples shows how to configure dash.js logging levels.

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/getting-started/manual-load-single-video.html b/samples/getting-started/manual-load-single-video.html index bcd56c89ea..dd87257ae5 100644 --- a/samples/getting-started/manual-load-single-video.html +++ b/samples/getting-started/manual-load-single-video.html @@ -1,43 +1,71 @@ - + - + Manual-player instantiation example - - + + + + + + + + - - - -
- +
+
+
+ +
+
+
+
+

Manual load single video

+

A sample showing how to load a single video. Autoplay is set to false.

+
+
+
+ +
+
+
+
+
+
- - - +
+ © DASH-IF +
+
+
+ + + + + diff --git a/samples/getting-started/manual-load-with-custom-settings.html b/samples/getting-started/manual-load-with-custom-settings.html index 89a37a8da7..70c1920e03 100644 --- a/samples/getting-started/manual-load-with-custom-settings.html +++ b/samples/getting-started/manual-load-with-custom-settings.html @@ -1,13 +1,21 @@ - + - - Manual-player instantiation example + + Manual load with custom settings example - - + + + + + + + + - - - -
- +
+
+
+ +
+
+
+
+

Manual load with custom settings

+

A sample showing how to load a video using custom settings.

+
+
+
+ +
- - - +
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + diff --git a/samples/getting-started/pre-load-video.html b/samples/getting-started/pre-load-video.html deleted file mode 100644 index 360abbe36a..0000000000 --- a/samples/getting-started/pre-load-video.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - Preload Video example - - - - - - - - - - - -
- This sample shows how to use preload feature of dash.js. In this sample, streaming is initialized and dash.js starts downloading stream segments as soon as the page is loaded. Right when the user clicks "Attach View" the video element is created, - attached to the already initialized dash.js MediaPlayer and then playback starts. -
-
- -
-
-
- - - - - \ No newline at end of file diff --git a/samples/highlighter.js b/samples/highlighter.js index 38dfd9e9d5..a670dd2899 100644 --- a/samples/highlighter.js +++ b/samples/highlighter.js @@ -1,5 +1,5 @@ var head = document.head || document.getElementsByTagName('head')[0]; -var body = document.body || document.getElementsByTagName('body')[0]; +var codeOutput = document.getElementById('code-output'); var style = document.createElement('link'); style.setAttribute('rel', 'stylesheet'); style.setAttribute('href', '//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/tomorrow.min.css'); @@ -9,7 +9,7 @@ script.setAttribute('src', '//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0 head.append(style); head.append(script); -body.innerHTML += '

Source code

'; +codeOutput.innerHTML += '

Source code

'; /** * This helper functions checks how many whitespaces preceed the last tag, which should have 0 diff --git a/samples/index.html b/samples/index.html index 311e8d8ffb..cecd14527c 100644 --- a/samples/index.html +++ b/samples/index.html @@ -1,136 +1,225 @@ - - - - - Samples players for dash.js - - + + + + + - - - +
+
-
- -

Dash.js Samples

-

Dash.js is a framework which enables the creation of many different MSE/EME players. This page provides a starting point to examine all the various samples available. Many samples ship with this code base, others are hosted elsewhere.

-
+
+ +
+ +

Samples

+ +

dash.js is a + reference client implementation by the DASH Industry + Forum (DASH-IF) for the playback of MPEG-DASH via JavaScript + and compliant + MSE/EME platforms. This page provides a starting point with multiple samples to explore the various + dash.js features and settings.

+ +

A reference UI encapsulating the main functionality of dash.js is available here . +

-

- - Reference Player: - - The DASH IF Reference player. The DASH Industry Forum is - a non-profit industry forum formed to catalyze the adoption of MPEG-DASH. They define common versions of DASH which other standards bodies (such as DVB and HbbTV) then formalize. - This player is intended to provide a reference implementation. Note the player is just a UI on top of the same framework used in all these samples. In using dash.js you are inheriting +

+

- -
-
-
-
-
- -
Card title
-
-

Some quick example text to build on the card title and make up the bulk of the card's content.

+
+
+ +
-

- Dash.js also provides examples of how to integrate it with different module bundler and devices. Check Typescript, - Webpack or Chromecast integrations. -

+
- - - - - + - function drawSectionSamples(section) { - var $tabContent = $('
'); - $tabContent.attr('id', nameToId(section.section)); - $tabContent.attr('aria-labelledby', nameToId(section.section) + '-tab'); - $('#content').append($tabContent); - - section.samples.forEach(function(sample) { - var $card = $('#card-sample').clone(true); - $card.attr('id', ''); - $card.find('h5').text(sample.title); - $card.find('p').html(sample.description); - $card.find('a').attr('href', sample.href); - $card.css('width', sample.width || '18rem'); - $tabContent.append($card); - }); - - if (section.active) { - $tabContent.addClass('active show'); - } - } - function nameToId(name) { - return name.replace(/[^a-z0-9]/gmi, ""); +
+
+
+ +
+
+

+
+ +
+
+
+ + - - \ No newline at end of file + + $card.find('span').append(labels); + + $tabContainerRow.append($card); + }); + + if (section.active) { + $tabContainer.addClass('active show'); + } + } + + function nameToId(name) { + return name.replace(/[^a-z0-9]/gmi, ''); + } + + + diff --git a/samples/lib/bootstrap/bootstrap.bundle.min.js b/samples/lib/bootstrap/bootstrap.bundle.min.js new file mode 100644 index 0000000000..2168d63301 --- /dev/null +++ b/samples/lib/bootstrap/bootstrap.bundle.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v5.0.0-beta3 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t=t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t},e=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i="#"+i.split("#")[1]),e=i&&"#"!==i?i.trim():null}return e},i=t=>{const i=e(t);return i&&document.querySelector(i)?i:null},s=t=>{const i=e(t);return i?document.querySelector(i):null},n=t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const s=Number.parseFloat(e),n=Number.parseFloat(i);return s||n?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0},o=t=>{t.dispatchEvent(new Event("transitionend"))},r=t=>(t[0]||t).nodeType,a=(t,e)=>{let i=!1;const s=e+5;t.addEventListener("transitionend",(function e(){i=!0,t.removeEventListener("transitionend",e)})),setTimeout(()=>{i||o(t)},s)},l=(t,e,i)=>{Object.keys(i).forEach(s=>{const n=i[s],o=e[s],a=o&&r(o)?"element":null==(l=o)?""+l:{}.toString.call(l).match(/\s([a-z]+)/i)[1].toLowerCase();var l;if(!new RegExp(n).test(a))throw new TypeError(t.toUpperCase()+": "+`Option "${s}" provided type "${a}" `+`but expected type "${n}".`)})},c=t=>{if(!t)return!1;if(t.style&&t.parentNode&&t.parentNode.style){const e=getComputedStyle(t),i=getComputedStyle(t.parentNode);return"none"!==e.display&&"none"!==i.display&&"hidden"!==e.visibility}return!1},d=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),h=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?h(t.parentNode):null},f=()=>function(){},u=t=>t.offsetHeight,p=()=>{const{jQuery:t}=window;return t&&!document.body.hasAttribute("data-bs-no-jquery")?t:null},g=()=>"rtl"===document.documentElement.dir,m=(t,e)=>{var i;i=()=>{const i=p();if(i){const s=i.fn[t];i.fn[t]=e.jQueryInterface,i.fn[t].Constructor=e,i.fn[t].noConflict=()=>(i.fn[t]=s,e.jQueryInterface)}},"loading"===document.readyState?document.addEventListener("DOMContentLoaded",i):i()},_=new Map;var b={set(t,e,i){_.has(t)||_.set(t,new Map);const s=_.get(t);s.has(e)||0===s.size?s.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(t,e)=>_.has(t)&&_.get(t).get(e)||null,remove(t,e){if(!_.has(t))return;const i=_.get(t);i.delete(e),0===i.size&&_.delete(t)}};const v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,E={};let T=1;const A={mouseenter:"mouseover",mouseleave:"mouseout"},L=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${T++}`||t.uidEvent||T++}function k(t){const e=O(t);return t.uidEvent=e,E[e]=E[e]||{},E[e]}function D(t,e,i=null){const s=Object.keys(t);for(let n=0,o=s.length;n{!function(t,e,i,s){const n=e[i]||{};Object.keys(n).forEach(o=>{if(o.includes(s)){const s=n[o];S(t,e,i,s.originalHandler,s.delegationSelector)}})}(t,l,i,e.slice(1))});const d=l[r]||{};Object.keys(d).forEach(i=>{const s=i.replace(w,"");if(!a||e.includes(s)){const e=d[i];S(t,l,r,e.originalHandler,e.delegationSelector)}})},trigger(t,e,i){if("string"!=typeof e||!t)return null;const s=p(),n=e.replace(y,""),o=e!==n,r=L.has(n);let a,l=!0,c=!0,d=!1,h=null;return o&&s&&(a=s.Event(e,i),s(t).trigger(a),l=!a.isPropagationStopped(),c=!a.isImmediatePropagationStopped(),d=a.isDefaultPrevented()),r?(h=document.createEvent("HTMLEvents"),h.initEvent(n,l,!0)):h=new CustomEvent(e,{bubbles:l,cancelable:!0}),void 0!==i&&Object.keys(i).forEach(t=>{Object.defineProperty(h,t,{get:()=>i[t]})}),d&&h.preventDefault(),c&&t.dispatchEvent(h),h.defaultPrevented&&void 0!==a&&a.preventDefault(),h}};class j{constructor(t){(t="string"==typeof t?document.querySelector(t):t)&&(this._element=t,b.set(this._element,this.constructor.DATA_KEY,this))}dispose(){b.remove(this._element,this.constructor.DATA_KEY),this._element=null}static getInstance(t){return b.get(t,this.DATA_KEY)}static get VERSION(){return"5.0.0-beta3"}}class P extends j{static get DATA_KEY(){return"bs.alert"}close(t){const e=t?this._getRootElement(t):this._element,i=this._triggerCloseEvent(e);null===i||i.defaultPrevented||this._removeElement(e)}_getRootElement(t){return s(t)||t.closest(".alert")}_triggerCloseEvent(t){return N.trigger(t,"close.bs.alert")}_removeElement(t){if(t.classList.remove("show"),!t.classList.contains("fade"))return void this._destroyElement(t);const e=n(t);N.one(t,"transitionend",()=>this._destroyElement(t)),a(t,e)}_destroyElement(t){t.parentNode&&t.parentNode.removeChild(t),N.trigger(t,"closed.bs.alert")}static jQueryInterface(t){return this.each((function(){let e=b.get(this,"bs.alert");e||(e=new P(this)),"close"===t&&e[t](this)}))}static handleDismiss(t){return function(e){e&&e.preventDefault(),t.close(this)}}}N.on(document,"click.bs.alert.data-api",'[data-bs-dismiss="alert"]',P.handleDismiss(new P)),m("alert",P);class I extends j{static get DATA_KEY(){return"bs.button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){let e=b.get(this,"bs.button");e||(e=new I(this)),"toggle"===t&&e[t]()}))}}function M(t){return"true"===t||"false"!==t&&(t===Number(t).toString()?Number(t):""===t||"null"===t?null:t)}function R(t){return t.replace(/[A-Z]/g,t=>"-"+t.toLowerCase())}N.on(document,"click.bs.button.data-api",'[data-bs-toggle="button"]',t=>{t.preventDefault();const e=t.target.closest('[data-bs-toggle="button"]');let i=b.get(e,"bs.button");i||(i=new I(e)),i.toggle()}),m("button",I);const B={setDataAttribute(t,e,i){t.setAttribute("data-bs-"+R(e),i)},removeDataAttribute(t,e){t.removeAttribute("data-bs-"+R(e))},getDataAttributes(t){if(!t)return{};const e={};return Object.keys(t.dataset).filter(t=>t.startsWith("bs")).forEach(i=>{let s=i.replace(/^bs/,"");s=s.charAt(0).toLowerCase()+s.slice(1,s.length),e[s]=M(t.dataset[i])}),e},getDataAttribute:(t,e)=>M(t.getAttribute("data-bs-"+R(e))),offset(t){const e=t.getBoundingClientRect();return{top:e.top+document.body.scrollTop,left:e.left+document.body.scrollLeft}},position:t=>({top:t.offsetTop,left:t.offsetLeft})},H={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter(t=>t.matches(e)),parents(t,e){const i=[];let s=t.parentNode;for(;s&&s.nodeType===Node.ELEMENT_NODE&&3!==s.nodeType;)s.matches(e)&&i.push(s),s=s.parentNode;return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]}},W={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0,touch:!0},U={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean",touch:"boolean"},$="next",F="prev",z="left",K="right";class Y extends j{constructor(t,e){super(t),this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this.touchStartX=0,this.touchDeltaX=0,this._config=this._getConfig(e),this._indicatorsElement=H.findOne(".carousel-indicators",this._element),this._touchSupported="ontouchstart"in document.documentElement||navigator.maxTouchPoints>0,this._pointerEvent=Boolean(window.PointerEvent),this._addEventListeners()}static get Default(){return W}static get DATA_KEY(){return"bs.carousel"}next(){this._isSliding||this._slide($)}nextWhenVisible(){!document.hidden&&c(this._element)&&this.next()}prev(){this._isSliding||this._slide(F)}pause(t){t||(this._isPaused=!0),H.findOne(".carousel-item-next, .carousel-item-prev",this._element)&&(o(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null}cycle(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config&&this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))}to(t){this._activeElement=H.findOne(".active.carousel-item",this._element);const e=this._getItemIndex(this._activeElement);if(t>this._items.length-1||t<0)return;if(this._isSliding)return void N.one(this._element,"slid.bs.carousel",()=>this.to(t));if(e===t)return this.pause(),void this.cycle();const i=t>e?$:F;this._slide(i,this._items[t])}dispose(){N.off(this._element,".bs.carousel"),this._items=null,this._config=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null,super.dispose()}_getConfig(t){return t={...W,...t},l("carousel",t,U),t}_handleSwipe(){const t=Math.abs(this.touchDeltaX);if(t<=40)return;const e=t/this.touchDeltaX;this.touchDeltaX=0,e&&this._slide(e>0?K:z)}_addEventListeners(){this._config.keyboard&&N.on(this._element,"keydown.bs.carousel",t=>this._keydown(t)),"hover"===this._config.pause&&(N.on(this._element,"mouseenter.bs.carousel",t=>this.pause(t)),N.on(this._element,"mouseleave.bs.carousel",t=>this.cycle(t))),this._config.touch&&this._touchSupported&&this._addTouchEventListeners()}_addTouchEventListeners(){const t=t=>{!this._pointerEvent||"pen"!==t.pointerType&&"touch"!==t.pointerType?this._pointerEvent||(this.touchStartX=t.touches[0].clientX):this.touchStartX=t.clientX},e=t=>{this.touchDeltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this.touchStartX},i=t=>{!this._pointerEvent||"pen"!==t.pointerType&&"touch"!==t.pointerType||(this.touchDeltaX=t.clientX-this.touchStartX),this._handleSwipe(),"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout(t=>this.cycle(t),500+this._config.interval))};H.find(".carousel-item img",this._element).forEach(t=>{N.on(t,"dragstart.bs.carousel",t=>t.preventDefault())}),this._pointerEvent?(N.on(this._element,"pointerdown.bs.carousel",e=>t(e)),N.on(this._element,"pointerup.bs.carousel",t=>i(t)),this._element.classList.add("pointer-event")):(N.on(this._element,"touchstart.bs.carousel",e=>t(e)),N.on(this._element,"touchmove.bs.carousel",t=>e(t)),N.on(this._element,"touchend.bs.carousel",t=>i(t)))}_keydown(t){/input|textarea/i.test(t.target.tagName)||("ArrowLeft"===t.key?(t.preventDefault(),this._slide(z)):"ArrowRight"===t.key&&(t.preventDefault(),this._slide(K)))}_getItemIndex(t){return this._items=t&&t.parentNode?H.find(".carousel-item",t.parentNode):[],this._items.indexOf(t)}_getItemByOrder(t,e){const i=t===$,s=t===F,n=this._getItemIndex(e),o=this._items.length-1;if((s&&0===n||i&&n===o)&&!this._config.wrap)return e;const r=(n+(s?-1:1))%this._items.length;return-1===r?this._items[this._items.length-1]:this._items[r]}_triggerSlideEvent(t,e){const i=this._getItemIndex(t),s=this._getItemIndex(H.findOne(".active.carousel-item",this._element));return N.trigger(this._element,"slide.bs.carousel",{relatedTarget:t,direction:e,from:s,to:i})}_setActiveIndicatorElement(t){if(this._indicatorsElement){const e=H.findOne(".active",this._indicatorsElement);e.classList.remove("active"),e.removeAttribute("aria-current");const i=H.find("[data-bs-target]",this._indicatorsElement);for(let e=0;e{r.classList.remove(h,f),r.classList.add("active"),s.classList.remove("active",f,h),this._isSliding=!1,setTimeout(()=>{N.trigger(this._element,"slid.bs.carousel",{relatedTarget:r,direction:p,from:o,to:l})},0)}),a(s,t)}else s.classList.remove("active"),r.classList.add("active"),this._isSliding=!1,N.trigger(this._element,"slid.bs.carousel",{relatedTarget:r,direction:p,from:o,to:l});c&&this.cycle()}}_directionToOrder(t){return[K,z].includes(t)?g()?t===K?F:$:t===K?$:F:t}_orderToDirection(t){return[$,F].includes(t)?g()?t===$?z:K:t===$?K:z:t}static carouselInterface(t,e){let i=b.get(t,"bs.carousel"),s={...W,...B.getDataAttributes(t)};"object"==typeof e&&(s={...s,...e});const n="string"==typeof e?e:s.slide;if(i||(i=new Y(t,s)),"number"==typeof e)i.to(e);else if("string"==typeof n){if(void 0===i[n])throw new TypeError(`No method named "${n}"`);i[n]()}else s.interval&&s.ride&&(i.pause(),i.cycle())}static jQueryInterface(t){return this.each((function(){Y.carouselInterface(this,t)}))}static dataApiClickHandler(t){const e=s(this);if(!e||!e.classList.contains("carousel"))return;const i={...B.getDataAttributes(e),...B.getDataAttributes(this)},n=this.getAttribute("data-bs-slide-to");n&&(i.interval=!1),Y.carouselInterface(e,i),n&&b.get(e,"bs.carousel").to(n),t.preventDefault()}}N.on(document,"click.bs.carousel.data-api","[data-bs-slide], [data-bs-slide-to]",Y.dataApiClickHandler),N.on(window,"load.bs.carousel.data-api",()=>{const t=H.find('[data-bs-ride="carousel"]');for(let e=0,i=t.length;et===this._element);null!==n&&o.length&&(this._selector=n,this._triggerArray.push(e))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}static get Default(){return q}static get DATA_KEY(){return"bs.collapse"}toggle(){this._element.classList.contains("show")?this.hide():this.show()}show(){if(this._isTransitioning||this._element.classList.contains("show"))return;let t,e;this._parent&&(t=H.find(".show, .collapsing",this._parent).filter(t=>"string"==typeof this._config.parent?t.getAttribute("data-bs-parent")===this._config.parent:t.classList.contains("collapse")),0===t.length&&(t=null));const i=H.findOne(this._selector);if(t){const s=t.find(t=>i!==t);if(e=s?b.get(s,"bs.collapse"):null,e&&e._isTransitioning)return}if(N.trigger(this._element,"show.bs.collapse").defaultPrevented)return;t&&t.forEach(t=>{i!==t&&X.collapseInterface(t,"hide"),e||b.set(t,"bs.collapse",null)});const s=this._getDimension();this._element.classList.remove("collapse"),this._element.classList.add("collapsing"),this._element.style[s]=0,this._triggerArray.length&&this._triggerArray.forEach(t=>{t.classList.remove("collapsed"),t.setAttribute("aria-expanded",!0)}),this.setTransitioning(!0);const o="scroll"+(s[0].toUpperCase()+s.slice(1)),r=n(this._element);N.one(this._element,"transitionend",()=>{this._element.classList.remove("collapsing"),this._element.classList.add("collapse","show"),this._element.style[s]="",this.setTransitioning(!1),N.trigger(this._element,"shown.bs.collapse")}),a(this._element,r),this._element.style[s]=this._element[o]+"px"}hide(){if(this._isTransitioning||!this._element.classList.contains("show"))return;if(N.trigger(this._element,"hide.bs.collapse").defaultPrevented)return;const t=this._getDimension();this._element.style[t]=this._element.getBoundingClientRect()[t]+"px",u(this._element),this._element.classList.add("collapsing"),this._element.classList.remove("collapse","show");const e=this._triggerArray.length;if(e>0)for(let t=0;t{this.setTransitioning(!1),this._element.classList.remove("collapsing"),this._element.classList.add("collapse"),N.trigger(this._element,"hidden.bs.collapse")}),a(this._element,i)}setTransitioning(t){this._isTransitioning=t}dispose(){super.dispose(),this._config=null,this._parent=null,this._triggerArray=null,this._isTransitioning=null}_getConfig(t){return(t={...q,...t}).toggle=Boolean(t.toggle),l("collapse",t,V),t}_getDimension(){return this._element.classList.contains("width")?"width":"height"}_getParent(){let{parent:t}=this._config;r(t)?void 0===t.jquery&&void 0===t[0]||(t=t[0]):t=H.findOne(t);const e=`[data-bs-toggle="collapse"][data-bs-parent="${t}"]`;return H.find(e,t).forEach(t=>{const e=s(t);this._addAriaAndCollapsedClass(e,[t])}),t}_addAriaAndCollapsedClass(t,e){if(!t||!e.length)return;const i=t.classList.contains("show");e.forEach(t=>{i?t.classList.remove("collapsed"):t.classList.add("collapsed"),t.setAttribute("aria-expanded",i)})}static collapseInterface(t,e){let i=b.get(t,"bs.collapse");const s={...q,...B.getDataAttributes(t),..."object"==typeof e&&e?e:{}};if(!i&&s.toggle&&"string"==typeof e&&/show|hide/.test(e)&&(s.toggle=!1),i||(i=new X(t,s)),"string"==typeof e){if(void 0===i[e])throw new TypeError(`No method named "${e}"`);i[e]()}}static jQueryInterface(t){return this.each((function(){X.collapseInterface(this,t)}))}}N.on(document,"click.bs.collapse.data-api",'[data-bs-toggle="collapse"]',(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();const e=B.getDataAttributes(this),s=i(this);H.find(s).forEach(t=>{const i=b.get(t,"bs.collapse");let s;i?(null===i._parent&&"string"==typeof e.parent&&(i._config.parent=e.parent,i._parent=i._getParent()),s="toggle"):s=e,X.collapseInterface(t,s)})})),m("collapse",X);var Q="top",G="bottom",Z="right",J="left",tt=[Q,G,Z,J],et=tt.reduce((function(t,e){return t.concat([e+"-start",e+"-end"])}),[]),it=[].concat(tt,["auto"]).reduce((function(t,e){return t.concat([e,e+"-start",e+"-end"])}),[]),st=["beforeRead","read","afterRead","beforeMain","main","afterMain","beforeWrite","write","afterWrite"];function nt(t){return t?(t.nodeName||"").toLowerCase():null}function ot(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function rt(t){return t instanceof ot(t).Element||t instanceof Element}function at(t){return t instanceof ot(t).HTMLElement||t instanceof HTMLElement}function lt(t){return"undefined"!=typeof ShadowRoot&&(t instanceof ot(t).ShadowRoot||t instanceof ShadowRoot)}var ct={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},s=e.attributes[t]||{},n=e.elements[t];at(n)&&nt(n)&&(Object.assign(n.style,i),Object.keys(s).forEach((function(t){var e=s[t];!1===e?n.removeAttribute(t):n.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var s=e.elements[t],n=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});at(s)&&nt(s)&&(Object.assign(s.style,o),Object.keys(n).forEach((function(t){s.removeAttribute(t)})))}))}},requires:["computeStyles"]};function dt(t){return t.split("-")[0]}function ht(t){var e=t.getBoundingClientRect();return{width:e.width,height:e.height,top:e.top,right:e.right,bottom:e.bottom,left:e.left,x:e.left,y:e.top}}function ft(t){var e=ht(t),i=t.offsetWidth,s=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-s)<=1&&(s=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:s}}function ut(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&<(i)){var s=e;do{if(s&&t.isSameNode(s))return!0;s=s.parentNode||s.host}while(s)}return!1}function pt(t){return ot(t).getComputedStyle(t)}function gt(t){return["table","td","th"].indexOf(nt(t))>=0}function mt(t){return((rt(t)?t.ownerDocument:t.document)||window.document).documentElement}function _t(t){return"html"===nt(t)?t:t.assignedSlot||t.parentNode||(lt(t)?t.host:null)||mt(t)}function bt(t){return at(t)&&"fixed"!==pt(t).position?t.offsetParent:null}function vt(t){for(var e=ot(t),i=bt(t);i&>(i)&&"static"===pt(i).position;)i=bt(i);return i&&("html"===nt(i)||"body"===nt(i)&&"static"===pt(i).position)?e:i||function(t){for(var e=-1!==navigator.userAgent.toLowerCase().indexOf("firefox"),i=_t(t);at(i)&&["html","body"].indexOf(nt(i))<0;){var s=pt(i);if("none"!==s.transform||"none"!==s.perspective||"paint"===s.contain||-1!==["transform","perspective"].indexOf(s.willChange)||e&&"filter"===s.willChange||e&&s.filter&&"none"!==s.filter)return i;i=i.parentNode}return null}(t)||e}function yt(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}var wt=Math.max,Et=Math.min,Tt=Math.round;function At(t,e,i){return wt(t,Et(e,i))}function Lt(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function Ot(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}var kt={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,s=t.name,n=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=dt(i.placement),l=yt(a),c=[J,Z].indexOf(a)>=0?"height":"width";if(o&&r){var d=function(t,e){return Lt("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:Ot(t,tt))}(n.padding,i),h=ft(o),f="y"===l?Q:J,u="y"===l?G:Z,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],g=r[l]-i.rects.reference[l],m=vt(o),_=m?"y"===l?m.clientHeight||0:m.clientWidth||0:0,b=p/2-g/2,v=d[f],y=_-h[c]-d[u],w=_/2-h[c]/2+b,E=At(v,w,y),T=l;i.modifiersData[s]=((e={})[T]=E,e.centerOffset=E-w,e)}},effect:function(t){var e=t.state,i=t.options.element,s=void 0===i?"[data-popper-arrow]":i;null!=s&&("string"!=typeof s||(s=e.elements.popper.querySelector(s)))&&ut(e.elements.popper,s)&&(e.elements.arrow=s)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]},Dt={top:"auto",right:"auto",bottom:"auto",left:"auto"};function xt(t){var e,i=t.popper,s=t.popperRect,n=t.placement,o=t.offsets,r=t.position,a=t.gpuAcceleration,l=t.adaptive,c=t.roundOffsets,d=!0===c?function(t){var e=t.x,i=t.y,s=window.devicePixelRatio||1;return{x:Tt(Tt(e*s)/s)||0,y:Tt(Tt(i*s)/s)||0}}(o):"function"==typeof c?c(o):o,h=d.x,f=void 0===h?0:h,u=d.y,p=void 0===u?0:u,g=o.hasOwnProperty("x"),m=o.hasOwnProperty("y"),_=J,b=Q,v=window;if(l){var y=vt(i),w="clientHeight",E="clientWidth";y===ot(i)&&"static"!==pt(y=mt(i)).position&&(w="scrollHeight",E="scrollWidth"),y=y,n===Q&&(b=G,p-=y[w]-s.height,p*=a?1:-1),n===J&&(_=Z,f-=y[E]-s.width,f*=a?1:-1)}var T,A=Object.assign({position:r},l&&Dt);return a?Object.assign({},A,((T={})[b]=m?"0":"",T[_]=g?"0":"",T.transform=(v.devicePixelRatio||1)<2?"translate("+f+"px, "+p+"px)":"translate3d("+f+"px, "+p+"px, 0)",T)):Object.assign({},A,((e={})[b]=m?p+"px":"",e[_]=g?f+"px":"",e.transform="",e))}var Ct={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,s=i.gpuAcceleration,n=void 0===s||s,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:dt(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:n};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,xt(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,xt(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}},St={passive:!0},Nt={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,s=t.options,n=s.scroll,o=void 0===n||n,r=s.resize,a=void 0===r||r,l=ot(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,St)})),a&&l.addEventListener("resize",i.update,St),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,St)})),a&&l.removeEventListener("resize",i.update,St)}},data:{}},jt={left:"right",right:"left",bottom:"top",top:"bottom"};function Pt(t){return t.replace(/left|right|bottom|top/g,(function(t){return jt[t]}))}var It={start:"end",end:"start"};function Mt(t){return t.replace(/start|end/g,(function(t){return It[t]}))}function Rt(t){var e=ot(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Bt(t){return ht(mt(t)).left+Rt(t).scrollLeft}function Ht(t){var e=pt(t),i=e.overflow,s=e.overflowX,n=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+n+s)}function Wt(t,e){var i;void 0===e&&(e=[]);var s=function t(e){return["html","body","#document"].indexOf(nt(e))>=0?e.ownerDocument.body:at(e)&&Ht(e)?e:t(_t(e))}(t),n=s===(null==(i=t.ownerDocument)?void 0:i.body),o=ot(s),r=n?[o].concat(o.visualViewport||[],Ht(s)?s:[]):s,a=e.concat(r);return n?a:a.concat(Wt(_t(r)))}function Ut(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function $t(t,e){return"viewport"===e?Ut(function(t){var e=ot(t),i=mt(t),s=e.visualViewport,n=i.clientWidth,o=i.clientHeight,r=0,a=0;return s&&(n=s.width,o=s.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(r=s.offsetLeft,a=s.offsetTop)),{width:n,height:o,x:r+Bt(t),y:a}}(t)):at(e)?function(t){var e=ht(t);return e.top=e.top+t.clientTop,e.left=e.left+t.clientLeft,e.bottom=e.top+t.clientHeight,e.right=e.left+t.clientWidth,e.width=t.clientWidth,e.height=t.clientHeight,e.x=e.left,e.y=e.top,e}(e):Ut(function(t){var e,i=mt(t),s=Rt(t),n=null==(e=t.ownerDocument)?void 0:e.body,o=wt(i.scrollWidth,i.clientWidth,n?n.scrollWidth:0,n?n.clientWidth:0),r=wt(i.scrollHeight,i.clientHeight,n?n.scrollHeight:0,n?n.clientHeight:0),a=-s.scrollLeft+Bt(t),l=-s.scrollTop;return"rtl"===pt(n||i).direction&&(a+=wt(i.clientWidth,n?n.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(mt(t)))}function Ft(t){return t.split("-")[1]}function zt(t){var e,i=t.reference,s=t.element,n=t.placement,o=n?dt(n):null,r=n?Ft(n):null,a=i.x+i.width/2-s.width/2,l=i.y+i.height/2-s.height/2;switch(o){case Q:e={x:a,y:i.y-s.height};break;case G:e={x:a,y:i.y+i.height};break;case Z:e={x:i.x+i.width,y:l};break;case J:e={x:i.x-s.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?yt(o):null;if(null!=c){var d="y"===c?"height":"width";switch(r){case"start":e[c]=e[c]-(i[d]/2-s[d]/2);break;case"end":e[c]=e[c]+(i[d]/2-s[d]/2)}}return e}function Kt(t,e){void 0===e&&(e={});var i=e,s=i.placement,n=void 0===s?t.placement:s,o=i.boundary,r=void 0===o?"clippingParents":o,a=i.rootBoundary,l=void 0===a?"viewport":a,c=i.elementContext,d=void 0===c?"popper":c,h=i.altBoundary,f=void 0!==h&&h,u=i.padding,p=void 0===u?0:u,g=Lt("number"!=typeof p?p:Ot(p,tt)),m="popper"===d?"reference":"popper",_=t.elements.reference,b=t.rects.popper,v=t.elements[f?m:d],y=function(t,e,i){var s="clippingParents"===e?function(t){var e=Wt(_t(t)),i=["absolute","fixed"].indexOf(pt(t).position)>=0&&at(t)?vt(t):t;return rt(i)?e.filter((function(t){return rt(t)&&ut(t,i)&&"body"!==nt(t)})):[]}(t):[].concat(e),n=[].concat(s,[i]),o=n[0],r=n.reduce((function(e,i){var s=$t(t,i);return e.top=wt(s.top,e.top),e.right=Et(s.right,e.right),e.bottom=Et(s.bottom,e.bottom),e.left=wt(s.left,e.left),e}),$t(t,o));return r.width=r.right-r.left,r.height=r.bottom-r.top,r.x=r.left,r.y=r.top,r}(rt(v)?v:v.contextElement||mt(t.elements.popper),r,l),w=ht(_),E=zt({reference:w,element:b,strategy:"absolute",placement:n}),T=Ut(Object.assign({},b,E)),A="popper"===d?T:w,L={top:y.top-A.top+g.top,bottom:A.bottom-y.bottom+g.bottom,left:y.left-A.left+g.left,right:A.right-y.right+g.right},O=t.modifiersData.offset;if("popper"===d&&O){var k=O[n];Object.keys(L).forEach((function(t){var e=[Z,G].indexOf(t)>=0?1:-1,i=[Q,G].indexOf(t)>=0?"y":"x";L[t]+=k[i]*e}))}return L}function Yt(t,e){void 0===e&&(e={});var i=e,s=i.placement,n=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?it:l,d=Ft(s),h=d?a?et:et.filter((function(t){return Ft(t)===d})):tt,f=h.filter((function(t){return c.indexOf(t)>=0}));0===f.length&&(f=h);var u=f.reduce((function(e,i){return e[i]=Kt(t,{placement:i,boundary:n,rootBoundary:o,padding:r})[dt(i)],e}),{});return Object.keys(u).sort((function(t,e){return u[t]-u[e]}))}var qt={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,s=t.name;if(!e.modifiersData[s]._skip){for(var n=i.mainAxis,o=void 0===n||n,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,d=i.boundary,h=i.rootBoundary,f=i.altBoundary,u=i.flipVariations,p=void 0===u||u,g=i.allowedAutoPlacements,m=e.options.placement,_=dt(m),b=l||(_!==m&&p?function(t){if("auto"===dt(t))return[];var e=Pt(t);return[Mt(t),e,Mt(e)]}(m):[Pt(m)]),v=[m].concat(b).reduce((function(t,i){return t.concat("auto"===dt(i)?Yt(e,{placement:i,boundary:d,rootBoundary:h,padding:c,flipVariations:p,allowedAutoPlacements:g}):i)}),[]),y=e.rects.reference,w=e.rects.popper,E=new Map,T=!0,A=v[0],L=0;L=0,C=x?"width":"height",S=Kt(e,{placement:O,boundary:d,rootBoundary:h,altBoundary:f,padding:c}),N=x?D?Z:J:D?G:Q;y[C]>w[C]&&(N=Pt(N));var j=Pt(N),P=[];if(o&&P.push(S[k]<=0),a&&P.push(S[N]<=0,S[j]<=0),P.every((function(t){return t}))){A=O,T=!1;break}E.set(O,P)}if(T)for(var I=function(t){var e=v.find((function(e){var i=E.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return A=e,"break"},M=p?3:1;M>0&&"break"!==I(M);M--);e.placement!==A&&(e.modifiersData[s]._skip=!0,e.placement=A,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function Vt(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function Xt(t){return[Q,Z,G,J].some((function(e){return t[e]>=0}))}var Qt={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,s=e.rects.reference,n=e.rects.popper,o=e.modifiersData.preventOverflow,r=Kt(e,{elementContext:"reference"}),a=Kt(e,{altBoundary:!0}),l=Vt(r,s),c=Vt(a,n,o),d=Xt(l),h=Xt(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:d,hasPopperEscaped:h},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":d,"data-popper-escaped":h})}},Gt={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,s=t.name,n=i.offset,o=void 0===n?[0,0]:n,r=it.reduce((function(t,i){return t[i]=function(t,e,i){var s=dt(t),n=[J,Q].indexOf(s)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*n,[J,Z].indexOf(s)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[s]=r}},Zt={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=zt({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},Jt={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,s=t.name,n=i.mainAxis,o=void 0===n||n,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,d=i.altBoundary,h=i.padding,f=i.tether,u=void 0===f||f,p=i.tetherOffset,g=void 0===p?0:p,m=Kt(e,{boundary:l,rootBoundary:c,padding:h,altBoundary:d}),_=dt(e.placement),b=Ft(e.placement),v=!b,y=yt(_),w="x"===y?"y":"x",E=e.modifiersData.popperOffsets,T=e.rects.reference,A=e.rects.popper,L="function"==typeof g?g(Object.assign({},e.rects,{placement:e.placement})):g,O={x:0,y:0};if(E){if(o||a){var k="y"===y?Q:J,D="y"===y?G:Z,x="y"===y?"height":"width",C=E[y],S=E[y]+m[k],N=E[y]-m[D],j=u?-A[x]/2:0,P="start"===b?T[x]:A[x],I="start"===b?-A[x]:-T[x],M=e.elements.arrow,R=u&&M?ft(M):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},H=B[k],W=B[D],U=At(0,T[x],R[x]),$=v?T[x]/2-j-U-H-L:P-U-H-L,F=v?-T[x]/2+j+U+W+L:I+U+W+L,z=e.elements.arrow&&vt(e.elements.arrow),K=z?"y"===y?z.clientTop||0:z.clientLeft||0:0,Y=e.modifiersData.offset?e.modifiersData.offset[e.placement][y]:0,q=E[y]+$-Y-K,V=E[y]+F-Y;if(o){var X=At(u?Et(S,q):S,C,u?wt(N,V):N);E[y]=X,O[y]=X-C}if(a){var tt="x"===y?Q:J,et="x"===y?G:Z,it=E[w],st=it+m[tt],nt=it-m[et],ot=At(u?Et(st,q):st,it,u?wt(nt,V):nt);E[w]=ot,O[w]=ot-it}}e.modifiersData[s]=O}},requiresIfExists:["offset"]};function te(t,e,i){void 0===i&&(i=!1);var s,n,o=mt(e),r=ht(t),a=at(e),l={scrollLeft:0,scrollTop:0},c={x:0,y:0};return(a||!a&&!i)&&(("body"!==nt(e)||Ht(o))&&(l=(s=e)!==ot(s)&&at(s)?{scrollLeft:(n=s).scrollLeft,scrollTop:n.scrollTop}:Rt(s)),at(e)?((c=ht(e)).x+=e.clientLeft,c.y+=e.clientTop):o&&(c.x=Bt(o))),{x:r.left+l.scrollLeft-c.x,y:r.top+l.scrollTop-c.y,width:r.width,height:r.height}}var ee={placement:"bottom",modifiers:[],strategy:"absolute"};function ie(){for(var t=arguments.length,e=new Array(t),i=0;i"applyStyles"===t.name&&!1===t.enabled);this._popper=re(e,this._menu,i),s&&B.setDataAttribute(this._menu,"popper","static")}"ontouchstart"in document.documentElement&&!t.closest(".navbar-nav")&&[].concat(...document.body.children).forEach(t=>N.on(t,"mouseover",null,(function(){}))),this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.toggle("show"),this._element.classList.toggle("show"),N.trigger(this._element,"shown.bs.dropdown",e)}}hide(){if(this._element.disabled||this._element.classList.contains("disabled")||!this._menu.classList.contains("show"))return;const t={relatedTarget:this._element};N.trigger(this._element,"hide.bs.dropdown",t).defaultPrevented||(this._popper&&this._popper.destroy(),this._menu.classList.toggle("show"),this._element.classList.toggle("show"),B.removeDataAttribute(this._menu,"popper"),N.trigger(this._element,"hidden.bs.dropdown",t))}dispose(){N.off(this._element,".bs.dropdown"),this._menu=null,this._popper&&(this._popper.destroy(),this._popper=null),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_addEventListeners(){N.on(this._element,"click.bs.dropdown",t=>{t.preventDefault(),this.toggle()})}_getConfig(t){if(t={...this.constructor.Default,...B.getDataAttributes(this._element),...t},l("dropdown",t,this.constructor.DefaultType),"object"==typeof t.reference&&!r(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError("dropdown".toUpperCase()+': Option "reference" provided type "object" without a required "getBoundingClientRect" method.');return t}_getMenuElement(){return H.next(this._element,".dropdown-menu")[0]}_getPlacement(){const t=this._element.parentNode;if(t.classList.contains("dropend"))return ue;if(t.classList.contains("dropstart"))return pe;const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?de:ce:e?fe:he}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return"static"===this._config.display&&(t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..."function"==typeof this._config.popperConfig?this._config.popperConfig(t):this._config.popperConfig}}static dropdownInterface(t,e){let i=b.get(t,"bs.dropdown");if(i||(i=new _e(t,"object"==typeof e?e:null)),"string"==typeof e){if(void 0===i[e])throw new TypeError(`No method named "${e}"`);i[e]()}}static jQueryInterface(t){return this.each((function(){_e.dropdownInterface(this,t)}))}static clearMenus(t){if(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;if(/input|select|textarea|form/i.test(t.target.tagName))return}const e=H.find('[data-bs-toggle="dropdown"]');for(let i=0,s=e.length;it.composedPath().includes(e)))continue;if("keyup"===t.type&&"Tab"===t.key&&o.contains(t.target))continue}N.trigger(e[i],"hide.bs.dropdown",n).defaultPrevented||("ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>N.off(t,"mouseover",null,(function(){}))),e[i].setAttribute("aria-expanded","false"),s._popper&&s._popper.destroy(),o.classList.remove("show"),e[i].classList.remove("show"),B.removeDataAttribute(o,"popper"),N.trigger(e[i],"hidden.bs.dropdown",n))}}}static getParentFromElement(t){return s(t)||t.parentNode}static dataApiKeydownHandler(t){if(/input|textarea/i.test(t.target.tagName)?"Space"===t.key||"Escape"!==t.key&&("ArrowDown"!==t.key&&"ArrowUp"!==t.key||t.target.closest(".dropdown-menu")):!le.test(t.key))return;if(t.preventDefault(),t.stopPropagation(),this.disabled||this.classList.contains("disabled"))return;const e=_e.getParentFromElement(this),i=this.classList.contains("show");if("Escape"===t.key)return(this.matches('[data-bs-toggle="dropdown"]')?this:H.prev(this,'[data-bs-toggle="dropdown"]')[0]).focus(),void _e.clearMenus();if(!i&&("ArrowUp"===t.key||"ArrowDown"===t.key))return void(this.matches('[data-bs-toggle="dropdown"]')?this:H.prev(this,'[data-bs-toggle="dropdown"]')[0]).click();if(!i||"Space"===t.key)return void _e.clearMenus();const s=H.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",e).filter(c);if(!s.length)return;let n=s.indexOf(t.target);"ArrowUp"===t.key&&n>0&&n--,"ArrowDown"===t.key&&nthis.hide(t)),N.on(this._dialog,"mousedown.dismiss.bs.modal",()=>{N.one(this._element,"mouseup.dismiss.bs.modal",t=>{t.target===this._element&&(this._ignoreBackdropClick=!0)})}),this._showBackdrop(()=>this._showElement(t)))}hide(t){if(t&&t.preventDefault(),!this._isShown||this._isTransitioning)return;if(N.trigger(this._element,"hide.bs.modal").defaultPrevented)return;this._isShown=!1;const e=this._isAnimated();if(e&&(this._isTransitioning=!0),this._setEscapeEvent(),this._setResizeEvent(),N.off(document,"focusin.bs.modal"),this._element.classList.remove("show"),N.off(this._element,"click.dismiss.bs.modal"),N.off(this._dialog,"mousedown.dismiss.bs.modal"),e){const t=n(this._element);N.one(this._element,"transitionend",t=>this._hideModal(t)),a(this._element,t)}else this._hideModal()}dispose(){[window,this._element,this._dialog].forEach(t=>N.off(t,".bs.modal")),super.dispose(),N.off(document,"focusin.bs.modal"),this._config=null,this._dialog=null,this._backdrop=null,this._isShown=null,this._isBodyOverflowing=null,this._ignoreBackdropClick=null,this._isTransitioning=null,this._scrollbarWidth=null}handleUpdate(){this._adjustDialog()}_getConfig(t){return t={...be,...t},l("modal",t,ve),t}_showElement(t){const e=this._isAnimated(),i=H.findOne(".modal-body",this._dialog);this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.appendChild(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0,i&&(i.scrollTop=0),e&&u(this._element),this._element.classList.add("show"),this._config.focus&&this._enforceFocus();const s=()=>{this._config.focus&&this._element.focus(),this._isTransitioning=!1,N.trigger(this._element,"shown.bs.modal",{relatedTarget:t})};if(e){const t=n(this._dialog);N.one(this._dialog,"transitionend",s),a(this._dialog,t)}else s()}_enforceFocus(){N.off(document,"focusin.bs.modal"),N.on(document,"focusin.bs.modal",t=>{document===t.target||this._element===t.target||this._element.contains(t.target)||this._element.focus()})}_setEscapeEvent(){this._isShown?N.on(this._element,"keydown.dismiss.bs.modal",t=>{this._config.keyboard&&"Escape"===t.key?(t.preventDefault(),this.hide()):this._config.keyboard||"Escape"!==t.key||this._triggerBackdropTransition()}):N.off(this._element,"keydown.dismiss.bs.modal")}_setResizeEvent(){this._isShown?N.on(window,"resize.bs.modal",()=>this._adjustDialog()):N.off(window,"resize.bs.modal")}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._showBackdrop(()=>{document.body.classList.remove("modal-open"),this._resetAdjustments(),this._resetScrollbar(),N.trigger(this._element,"hidden.bs.modal")})}_removeBackdrop(){this._backdrop.parentNode.removeChild(this._backdrop),this._backdrop=null}_showBackdrop(t){const e=this._isAnimated();if(this._isShown&&this._config.backdrop){if(this._backdrop=document.createElement("div"),this._backdrop.className="modal-backdrop",e&&this._backdrop.classList.add("fade"),document.body.appendChild(this._backdrop),N.on(this._element,"click.dismiss.bs.modal",t=>{this._ignoreBackdropClick?this._ignoreBackdropClick=!1:t.target===t.currentTarget&&("static"===this._config.backdrop?this._triggerBackdropTransition():this.hide())}),e&&u(this._backdrop),this._backdrop.classList.add("show"),!e)return void t();const i=n(this._backdrop);N.one(this._backdrop,"transitionend",t),a(this._backdrop,i)}else if(!this._isShown&&this._backdrop){this._backdrop.classList.remove("show");const i=()=>{this._removeBackdrop(),t()};if(e){const t=n(this._backdrop);N.one(this._backdrop,"transitionend",i),a(this._backdrop,t)}else i()}else t()}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(N.trigger(this._element,"hidePrevented.bs.modal").defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight;t||(this._element.style.overflowY="hidden"),this._element.classList.add("modal-static");const e=n(this._dialog);N.off(this._element,"transitionend"),N.one(this._element,"transitionend",()=>{this._element.classList.remove("modal-static"),t||(N.one(this._element,"transitionend",()=>{this._element.style.overflowY=""}),a(this._element,e))}),a(this._element,e),this._element.focus()}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight;(!this._isBodyOverflowing&&t&&!g()||this._isBodyOverflowing&&!t&&g())&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),(this._isBodyOverflowing&&!t&&!g()||!this._isBodyOverflowing&&t&&g())&&(this._element.style.paddingRight=this._scrollbarWidth+"px")}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}_checkScrollbar(){const t=document.body.getBoundingClientRect();this._isBodyOverflowing=Math.round(t.left+t.right)t+this._scrollbarWidth),this._setElementAttributes(".sticky-top","marginRight",t=>t-this._scrollbarWidth),this._setElementAttributes("body","paddingRight",t=>t+this._scrollbarWidth)),document.body.classList.add("modal-open")}_setElementAttributes(t,e,i){H.find(t).forEach(t=>{if(t!==document.body&&window.innerWidth>t.clientWidth+this._scrollbarWidth)return;const s=t.style[e],n=window.getComputedStyle(t)[e];B.setDataAttribute(t,e,s),t.style[e]=i(Number.parseFloat(n))+"px"})}_resetScrollbar(){this._resetElementAttributes(".fixed-top, .fixed-bottom, .is-fixed, .sticky-top","paddingRight"),this._resetElementAttributes(".sticky-top","marginRight"),this._resetElementAttributes("body","paddingRight")}_resetElementAttributes(t,e){H.find(t).forEach(t=>{const i=B.getDataAttribute(t,e);void 0===i&&t===document.body?t.style[e]="":(B.removeDataAttribute(t,e),t.style[e]=i)})}_getScrollbarWidth(){const t=document.createElement("div");t.className="modal-scrollbar-measure",document.body.appendChild(t);const e=t.getBoundingClientRect().width-t.clientWidth;return document.body.removeChild(t),e}static jQueryInterface(t,e){return this.each((function(){let i=b.get(this,"bs.modal");const s={...be,...B.getDataAttributes(this),..."object"==typeof t&&t?t:{}};if(i||(i=new ye(this,s)),"string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}N.on(document,"click.bs.modal.data-api",'[data-bs-toggle="modal"]',(function(t){const e=s(this);"A"!==this.tagName&&"AREA"!==this.tagName||t.preventDefault(),N.one(e,"show.bs.modal",t=>{t.defaultPrevented||N.one(e,"hidden.bs.modal",()=>{c(this)&&this.focus()})});let i=b.get(e,"bs.modal");if(!i){const t={...B.getDataAttributes(e),...B.getDataAttributes(this)};i=new ye(e,t)}i.toggle(this)})),m("modal",ye);const we=()=>{const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)},Ee=(t,e,i)=>{const s=we();H.find(t).forEach(t=>{if(t!==document.body&&window.innerWidth>t.clientWidth+s)return;const n=t.style[e],o=window.getComputedStyle(t)[e];B.setDataAttribute(t,e,n),t.style[e]=i(Number.parseFloat(o))+"px"})},Te=(t,e)=>{H.find(t).forEach(t=>{const i=B.getDataAttribute(t,e);void 0===i&&t===document.body?t.style.removeProperty(e):(B.removeDataAttribute(t,e),t.style[e]=i)})},Ae={backdrop:!0,keyboard:!0,scroll:!1},Le={backdrop:"boolean",keyboard:"boolean",scroll:"boolean"};class Oe extends j{constructor(t,e){super(t),this._config=this._getConfig(e),this._isShown=!1,this._addEventListeners()}static get Default(){return Ae}static get DATA_KEY(){return"bs.offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||N.trigger(this._element,"show.bs.offcanvas",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._element.style.visibility="visible",this._config.backdrop&&document.body.classList.add("offcanvas-backdrop"),this._config.scroll||((t=we())=>{document.body.style.overflow="hidden",Ee(".fixed-top, .fixed-bottom, .is-fixed","paddingRight",e=>e+t),Ee(".sticky-top","marginRight",e=>e-t),Ee("body","paddingRight",e=>e+t)})(),this._element.classList.add("offcanvas-toggling"),this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add("show"),setTimeout(()=>{this._element.classList.remove("offcanvas-toggling"),N.trigger(this._element,"shown.bs.offcanvas",{relatedTarget:t}),this._enforceFocusOnElement(this._element)},n(this._element)))}hide(){this._isShown&&(N.trigger(this._element,"hide.bs.offcanvas").defaultPrevented||(this._element.classList.add("offcanvas-toggling"),N.off(document,"focusin.bs.offcanvas"),this._element.blur(),this._isShown=!1,this._element.classList.remove("show"),setTimeout(()=>{this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._element.style.visibility="hidden",this._config.backdrop&&document.body.classList.remove("offcanvas-backdrop"),this._config.scroll||(document.body.style.overflow="auto",Te(".fixed-top, .fixed-bottom, .is-fixed","paddingRight"),Te(".sticky-top","marginRight"),Te("body","paddingRight")),N.trigger(this._element,"hidden.bs.offcanvas"),this._element.classList.remove("offcanvas-toggling")},n(this._element))))}_getConfig(t){return t={...Ae,...B.getDataAttributes(this._element),..."object"==typeof t?t:{}},l("offcanvas",t,Le),t}_enforceFocusOnElement(t){N.off(document,"focusin.bs.offcanvas"),N.on(document,"focusin.bs.offcanvas",e=>{document===e.target||t===e.target||t.contains(e.target)||t.focus()}),t.focus()}_addEventListeners(){N.on(this._element,"click.dismiss.bs.offcanvas",'[data-bs-dismiss="offcanvas"]',()=>this.hide()),N.on(document,"keydown",t=>{this._config.keyboard&&"Escape"===t.key&&this.hide()}),N.on(document,"click.bs.offcanvas.data-api",t=>{const e=H.findOne(i(t.target));this._element.contains(t.target)||e===this._element||this.hide()})}static jQueryInterface(t){return this.each((function(){const e=b.get(this,"bs.offcanvas")||new Oe(this,"object"==typeof t?t:{});if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}N.on(document,"click.bs.offcanvas.data-api",'[data-bs-toggle="offcanvas"]',(function(t){const e=s(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),d(this))return;N.one(e,"hidden.bs.offcanvas",()=>{c(this)&&this.focus()});const i=H.findOne(".offcanvas.show, .offcanvas-toggling");i&&i!==e||(b.get(e,"bs.offcanvas")||new Oe(e)).toggle(this)})),N.on(window,"load.bs.offcanvas.data-api",()=>{H.find(".offcanvas.show").forEach(t=>(b.get(t,"bs.offcanvas")||new Oe(t)).show())}),m("offcanvas",Oe);const ke=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),De=/^(?:(?:https?|mailto|ftp|tel|file):|[^#&/:?]*(?:[#/?]|$))/i,xe=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,Ce=(t,e)=>{const i=t.nodeName.toLowerCase();if(e.includes(i))return!ke.has(i)||Boolean(De.test(t.nodeValue)||xe.test(t.nodeValue));const s=e.filter(t=>t instanceof RegExp);for(let t=0,e=s.length;t{Ce(t,a)||i.removeAttribute(t.nodeName)})}return s.body.innerHTML}const Ne=new RegExp("(^|\\s)bs-tooltip\\S+","g"),je=new Set(["sanitize","allowList","sanitizeFn"]),Pe={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(array|string|function)",container:"(string|element|boolean)",fallbackPlacements:"array",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",allowList:"object",popperConfig:"(null|object|function)"},Ie={AUTO:"auto",TOP:"top",RIGHT:g()?"left":"right",BOTTOM:"bottom",LEFT:g()?"right":"left"},Me={animation:!0,template:'',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:[0,0],container:!1,fallbackPlacements:["top","right","bottom","left"],boundary:"clippingParents",customClass:"",sanitize:!0,sanitizeFn:null,allowList:{"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},popperConfig:null},Re={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"};class Be extends j{constructor(t,e){if(void 0===ae)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t),this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.config=this._getConfig(e),this.tip=null,this._setListeners()}static get Default(){return Me}static get NAME(){return"tooltip"}static get DATA_KEY(){return"bs.tooltip"}static get Event(){return Re}static get EVENT_KEY(){return".bs.tooltip"}static get DefaultType(){return Pe}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(t){if(this._isEnabled)if(t){const e=this._initializeOnDelegatedTarget(t);e._activeTrigger.click=!e._activeTrigger.click,e._isWithActiveTrigger()?e._enter(null,e):e._leave(null,e)}else{if(this.getTipElement().classList.contains("show"))return void this._leave(null,this);this._enter(null,this)}}dispose(){clearTimeout(this._timeout),N.off(this._element,this.constructor.EVENT_KEY),N.off(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this.tip&&this.tip.parentNode&&this.tip.parentNode.removeChild(this.tip),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.config=null,this.tip=null,super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this.isWithContent()||!this._isEnabled)return;const e=N.trigger(this._element,this.constructor.Event.SHOW),i=h(this._element),s=null===i?this._element.ownerDocument.documentElement.contains(this._element):i.contains(this._element);if(e.defaultPrevented||!s)return;const o=this.getTipElement(),r=t(this.constructor.NAME);o.setAttribute("id",r),this._element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&o.classList.add("fade");const l="function"==typeof this.config.placement?this.config.placement.call(this,o,this._element):this.config.placement,c=this._getAttachment(l);this._addAttachmentClass(c);const d=this._getContainer();b.set(o,this.constructor.DATA_KEY,this),this._element.ownerDocument.documentElement.contains(this.tip)||(d.appendChild(o),N.trigger(this._element,this.constructor.Event.INSERTED)),this._popper?this._popper.update():this._popper=re(this._element,o,this._getPopperConfig(c)),o.classList.add("show");const f="function"==typeof this.config.customClass?this.config.customClass():this.config.customClass;f&&o.classList.add(...f.split(" ")),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>{N.on(t,"mouseover",(function(){}))});const u=()=>{const t=this._hoverState;this._hoverState=null,N.trigger(this._element,this.constructor.Event.SHOWN),"out"===t&&this._leave(null,this)};if(this.tip.classList.contains("fade")){const t=n(this.tip);N.one(this.tip,"transitionend",u),a(this.tip,t)}else u()}hide(){if(!this._popper)return;const t=this.getTipElement(),e=()=>{this._isWithActiveTrigger()||("show"!==this._hoverState&&t.parentNode&&t.parentNode.removeChild(t),this._cleanTipClass(),this._element.removeAttribute("aria-describedby"),N.trigger(this._element,this.constructor.Event.HIDDEN),this._popper&&(this._popper.destroy(),this._popper=null))};if(!N.trigger(this._element,this.constructor.Event.HIDE).defaultPrevented){if(t.classList.remove("show"),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>N.off(t,"mouseover",f)),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,this.tip.classList.contains("fade")){const i=n(t);N.one(t,"transitionend",e),a(t,i)}else e();this._hoverState=""}}update(){null!==this._popper&&this._popper.update()}isWithContent(){return Boolean(this.getTitle())}getTipElement(){if(this.tip)return this.tip;const t=document.createElement("div");return t.innerHTML=this.config.template,this.tip=t.children[0],this.tip}setContent(){const t=this.getTipElement();this.setElementContent(H.findOne(".tooltip-inner",t),this.getTitle()),t.classList.remove("fade","show")}setElementContent(t,e){if(null!==t)return"object"==typeof e&&r(e)?(e.jquery&&(e=e[0]),void(this.config.html?e.parentNode!==t&&(t.innerHTML="",t.appendChild(e)):t.textContent=e.textContent)):void(this.config.html?(this.config.sanitize&&(e=Se(e,this.config.allowList,this.config.sanitizeFn)),t.innerHTML=e):t.textContent=e)}getTitle(){let t=this._element.getAttribute("data-bs-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this._element):this.config.title),t}updateAttachment(t){return"right"===t?"end":"left"===t?"start":t}_initializeOnDelegatedTarget(t,e){const i=this.constructor.DATA_KEY;return(e=e||b.get(t.delegateTarget,i))||(e=new this.constructor(t.delegateTarget,this._getDelegateConfig()),b.set(t.delegateTarget,i,e)),e}_getOffset(){const{offset:t}=this.config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{altBoundary:!0,fallbackPlacements:this.config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this.config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"onChange",enabled:!0,phase:"afterWrite",fn:t=>this._handlePopperPlacementChange(t)}],onFirstUpdate:t=>{t.options.placement!==t.placement&&this._handlePopperPlacementChange(t)}};return{...e,..."function"==typeof this.config.popperConfig?this.config.popperConfig(e):this.config.popperConfig}}_addAttachmentClass(t){this.getTipElement().classList.add("bs-tooltip-"+this.updateAttachment(t))}_getContainer(){return!1===this.config.container?document.body:r(this.config.container)?this.config.container:H.findOne(this.config.container)}_getAttachment(t){return Ie[t.toUpperCase()]}_setListeners(){this.config.trigger.split(" ").forEach(t=>{if("click"===t)N.on(this._element,this.constructor.Event.CLICK,this.config.selector,t=>this.toggle(t));else if("manual"!==t){const e="hover"===t?this.constructor.Event.MOUSEENTER:this.constructor.Event.FOCUSIN,i="hover"===t?this.constructor.Event.MOUSELEAVE:this.constructor.Event.FOCUSOUT;N.on(this._element,e,this.config.selector,t=>this._enter(t)),N.on(this._element,i,this.config.selector,t=>this._leave(t))}}),this._hideModalHandler=()=>{this._element&&this.hide()},N.on(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this.config.selector?this.config={...this.config,trigger:"manual",selector:""}:this._fixTitle()}_fixTitle(){const t=this._element.getAttribute("title"),e=typeof this._element.getAttribute("data-bs-original-title");(t||"string"!==e)&&(this._element.setAttribute("data-bs-original-title",t||""),!t||this._element.getAttribute("aria-label")||this._element.textContent||this._element.setAttribute("aria-label",t),this._element.setAttribute("title",""))}_enter(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusin"===t.type?"focus":"hover"]=!0),e.getTipElement().classList.contains("show")||"show"===e._hoverState?e._hoverState="show":(clearTimeout(e._timeout),e._hoverState="show",e.config.delay&&e.config.delay.show?e._timeout=setTimeout(()=>{"show"===e._hoverState&&e.show()},e.config.delay.show):e.show())}_leave(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusout"===t.type?"focus":"hover"]=e._element.contains(t.relatedTarget)),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState="out",e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(()=>{"out"===e._hoverState&&e.hide()},e.config.delay.hide):e.hide())}_isWithActiveTrigger(){for(const t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1}_getConfig(t){const e=B.getDataAttributes(this._element);return Object.keys(e).forEach(t=>{je.has(t)&&delete e[t]}),t&&"object"==typeof t.container&&t.container.jquery&&(t.container=t.container[0]),"number"==typeof(t={...this.constructor.Default,...e,..."object"==typeof t&&t?t:{}}).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),l("tooltip",t,this.constructor.DefaultType),t.sanitize&&(t.template=Se(t.template,t.allowList,t.sanitizeFn)),t}_getDelegateConfig(){const t={};if(this.config)for(const e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t}_cleanTipClass(){const t=this.getTipElement(),e=t.getAttribute("class").match(Ne);null!==e&&e.length>0&&e.map(t=>t.trim()).forEach(e=>t.classList.remove(e))}_handlePopperPlacementChange(t){const{state:e}=t;e&&(this.tip=e.elements.popper,this._cleanTipClass(),this._addAttachmentClass(this._getAttachment(e.placement)))}static jQueryInterface(t){return this.each((function(){let e=b.get(this,"bs.tooltip");const i="object"==typeof t&&t;if((e||!/dispose|hide/.test(t))&&(e||(e=new Be(this,i)),"string"==typeof t)){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m("tooltip",Be);const He=new RegExp("(^|\\s)bs-popover\\S+","g"),We={...Be.Default,placement:"right",offset:[0,8],trigger:"click",content:"",template:''},Ue={...Be.DefaultType,content:"(string|element|function)"},$e={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"};class Fe extends Be{static get Default(){return We}static get NAME(){return"popover"}static get DATA_KEY(){return"bs.popover"}static get Event(){return $e}static get EVENT_KEY(){return".bs.popover"}static get DefaultType(){return Ue}isWithContent(){return this.getTitle()||this._getContent()}setContent(){const t=this.getTipElement();this.setElementContent(H.findOne(".popover-header",t),this.getTitle());let e=this._getContent();"function"==typeof e&&(e=e.call(this._element)),this.setElementContent(H.findOne(".popover-body",t),e),t.classList.remove("fade","show")}_addAttachmentClass(t){this.getTipElement().classList.add("bs-popover-"+this.updateAttachment(t))}_getContent(){return this._element.getAttribute("data-bs-content")||this.config.content}_cleanTipClass(){const t=this.getTipElement(),e=t.getAttribute("class").match(He);null!==e&&e.length>0&&e.map(t=>t.trim()).forEach(e=>t.classList.remove(e))}static jQueryInterface(t){return this.each((function(){let e=b.get(this,"bs.popover");const i="object"==typeof t?t:null;if((e||!/dispose|hide/.test(t))&&(e||(e=new Fe(this,i),b.set(this,"bs.popover",e)),"string"==typeof t)){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m("popover",Fe);const ze={offset:10,method:"auto",target:""},Ke={offset:"number",method:"string",target:"(string|element)"};class Ye extends j{constructor(t,e){super(t),this._scrollElement="BODY"===this._element.tagName?window:this._element,this._config=this._getConfig(e),this._selector=`${this._config.target} .nav-link, ${this._config.target} .list-group-item, ${this._config.target} .dropdown-item`,this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,N.on(this._scrollElement,"scroll.bs.scrollspy",()=>this._process()),this.refresh(),this._process()}static get Default(){return ze}static get DATA_KEY(){return"bs.scrollspy"}refresh(){const t=this._scrollElement===this._scrollElement.window?"offset":"position",e="auto"===this._config.method?t:this._config.method,s="position"===e?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),H.find(this._selector).map(t=>{const n=i(t),o=n?H.findOne(n):null;if(o){const t=o.getBoundingClientRect();if(t.width||t.height)return[B[e](o).top+s,n]}return null}).filter(t=>t).sort((t,e)=>t[0]-e[0]).forEach(t=>{this._offsets.push(t[0]),this._targets.push(t[1])})}dispose(){super.dispose(),N.off(this._scrollElement,".bs.scrollspy"),this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null}_getConfig(e){if("string"!=typeof(e={...ze,..."object"==typeof e&&e?e:{}}).target&&r(e.target)){let{id:i}=e.target;i||(i=t("scrollspy"),e.target.id=i),e.target="#"+i}return l("scrollspy",e,Ke),e}_getScrollTop(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop}_getScrollHeight(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)}_getOffsetHeight(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height}_process(){const t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),i=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=i){const t=this._targets[this._targets.length-1];this._activeTarget!==t&&this._activate(t)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(let e=this._offsets.length;e--;)this._activeTarget!==this._targets[e]&&t>=this._offsets[e]&&(void 0===this._offsets[e+1]||t`${e}[data-bs-target="${t}"],${e}[href="${t}"]`),i=H.findOne(e.join(","));i.classList.contains("dropdown-item")?(H.findOne(".dropdown-toggle",i.closest(".dropdown")).classList.add("active"),i.classList.add("active")):(i.classList.add("active"),H.parents(i,".nav, .list-group").forEach(t=>{H.prev(t,".nav-link, .list-group-item").forEach(t=>t.classList.add("active")),H.prev(t,".nav-item").forEach(t=>{H.children(t,".nav-link").forEach(t=>t.classList.add("active"))})})),N.trigger(this._scrollElement,"activate.bs.scrollspy",{relatedTarget:t})}_clear(){H.find(this._selector).filter(t=>t.classList.contains("active")).forEach(t=>t.classList.remove("active"))}static jQueryInterface(t){return this.each((function(){let e=b.get(this,"bs.scrollspy");if(e||(e=new Ye(this,"object"==typeof t&&t)),"string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(window,"load.bs.scrollspy.data-api",()=>{H.find('[data-bs-spy="scroll"]').forEach(t=>new Ye(t,B.getDataAttributes(t)))}),m("scrollspy",Ye);class qe extends j{static get DATA_KEY(){return"bs.tab"}show(){if(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&this._element.classList.contains("active")||d(this._element))return;let t;const e=s(this._element),i=this._element.closest(".nav, .list-group");if(i){const e="UL"===i.nodeName||"OL"===i.nodeName?":scope > li > .active":".active";t=H.find(e,i),t=t[t.length-1]}const n=t?N.trigger(t,"hide.bs.tab",{relatedTarget:this._element}):null;if(N.trigger(this._element,"show.bs.tab",{relatedTarget:t}).defaultPrevented||null!==n&&n.defaultPrevented)return;this._activate(this._element,i);const o=()=>{N.trigger(t,"hidden.bs.tab",{relatedTarget:this._element}),N.trigger(this._element,"shown.bs.tab",{relatedTarget:t})};e?this._activate(e,e.parentNode,o):o()}_activate(t,e,i){const s=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?H.children(e,".active"):H.find(":scope > li > .active",e))[0],o=i&&s&&s.classList.contains("fade"),r=()=>this._transitionComplete(t,s,i);if(s&&o){const t=n(s);s.classList.remove("show"),N.one(s,"transitionend",r),a(s,t)}else r()}_transitionComplete(t,e,i){if(e){e.classList.remove("active");const t=H.findOne(":scope > .dropdown-menu .active",e.parentNode);t&&t.classList.remove("active"),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}t.classList.add("active"),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),u(t),t.classList.contains("fade")&&t.classList.add("show"),t.parentNode&&t.parentNode.classList.contains("dropdown-menu")&&(t.closest(".dropdown")&&H.find(".dropdown-toggle").forEach(t=>t.classList.add("active")),t.setAttribute("aria-expanded",!0)),i&&i()}static jQueryInterface(t){return this.each((function(){const e=b.get(this,"bs.tab")||new qe(this);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(document,"click.bs.tab.data-api",'[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',(function(t){t.preventDefault(),(b.get(this,"bs.tab")||new qe(this)).show()})),m("tab",qe);const Ve={animation:"boolean",autohide:"boolean",delay:"number"},Xe={animation:!0,autohide:!0,delay:5e3};class Qe extends j{constructor(t,e){super(t),this._config=this._getConfig(e),this._timeout=null,this._setListeners()}static get DefaultType(){return Ve}static get Default(){return Xe}static get DATA_KEY(){return"bs.toast"}show(){if(N.trigger(this._element,"show.bs.toast").defaultPrevented)return;this._clearTimeout(),this._config.animation&&this._element.classList.add("fade");const t=()=>{this._element.classList.remove("showing"),this._element.classList.add("show"),N.trigger(this._element,"shown.bs.toast"),this._config.autohide&&(this._timeout=setTimeout(()=>{this.hide()},this._config.delay))};if(this._element.classList.remove("hide"),u(this._element),this._element.classList.add("showing"),this._config.animation){const e=n(this._element);N.one(this._element,"transitionend",t),a(this._element,e)}else t()}hide(){if(!this._element.classList.contains("show"))return;if(N.trigger(this._element,"hide.bs.toast").defaultPrevented)return;const t=()=>{this._element.classList.add("hide"),N.trigger(this._element,"hidden.bs.toast")};if(this._element.classList.remove("show"),this._config.animation){const e=n(this._element);N.one(this._element,"transitionend",t),a(this._element,e)}else t()}dispose(){this._clearTimeout(),this._element.classList.contains("show")&&this._element.classList.remove("show"),N.off(this._element,"click.dismiss.bs.toast"),super.dispose(),this._config=null}_getConfig(t){return t={...Xe,...B.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}},l("toast",t,this.constructor.DefaultType),t}_setListeners(){N.on(this._element,"click.dismiss.bs.toast",'[data-bs-dismiss="toast"]',()=>this.hide())}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){let e=b.get(this,"bs.toast");if(e||(e=new Qe(this,"object"==typeof t&&t)),"string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return m("toast",Qe),{Alert:P,Button:I,Carousel:Y,Collapse:X,Dropdown:_e,Modal:ye,Offcanvas:Oe,Popover:Fe,ScrollSpy:Ye,Tab:qe,Toast:Qe,Tooltip:Be}})); +//# sourceMappingURL=bootstrap.bundle.min.js.map \ No newline at end of file diff --git a/samples/lib/bootstrap/bootstrap.bundle.min.js.map b/samples/lib/bootstrap/bootstrap.bundle.min.js.map new file mode 100644 index 0000000000..6e1031c294 --- /dev/null +++ b/samples/lib/bootstrap/bootstrap.bundle.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../js/src/util/index.js","../../js/src/dom/data.js","../../js/src/dom/event-handler.js","../../js/src/base-component.js","../../js/src/alert.js","../../js/src/button.js","../../js/src/dom/manipulator.js","../../js/src/dom/selector-engine.js","../../js/src/carousel.js","../../js/src/collapse.js","../../node_modules/@popperjs/core/lib/enums.js","../../node_modules/@popperjs/core/lib/dom-utils/getNodeName.js","../../node_modules/@popperjs/core/lib/dom-utils/getWindow.js","../../node_modules/@popperjs/core/lib/dom-utils/instanceOf.js","../../node_modules/@popperjs/core/lib/modifiers/applyStyles.js","../../node_modules/@popperjs/core/lib/utils/getBasePlacement.js","../../node_modules/@popperjs/core/lib/dom-utils/getBoundingClientRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getLayoutRect.js","../../node_modules/@popperjs/core/lib/dom-utils/contains.js","../../node_modules/@popperjs/core/lib/dom-utils/getComputedStyle.js","../../node_modules/@popperjs/core/lib/dom-utils/isTableElement.js","../../node_modules/@popperjs/core/lib/dom-utils/getDocumentElement.js","../../node_modules/@popperjs/core/lib/dom-utils/getParentNode.js","../../node_modules/@popperjs/core/lib/dom-utils/getOffsetParent.js","../../node_modules/@popperjs/core/lib/utils/getMainAxisFromPlacement.js","../../node_modules/@popperjs/core/lib/utils/math.js","../../node_modules/@popperjs/core/lib/utils/within.js","../../node_modules/@popperjs/core/lib/utils/mergePaddingObject.js","../../node_modules/@popperjs/core/lib/utils/getFreshSideObject.js","../../node_modules/@popperjs/core/lib/utils/expandToHashMap.js","../../node_modules/@popperjs/core/lib/modifiers/arrow.js","../../node_modules/@popperjs/core/lib/modifiers/computeStyles.js","../../node_modules/@popperjs/core/lib/modifiers/eventListeners.js","../../node_modules/@popperjs/core/lib/utils/getOppositePlacement.js","../../node_modules/@popperjs/core/lib/utils/getOppositeVariationPlacement.js","../../node_modules/@popperjs/core/lib/dom-utils/getWindowScroll.js","../../node_modules/@popperjs/core/lib/dom-utils/getWindowScrollBarX.js","../../node_modules/@popperjs/core/lib/dom-utils/isScrollParent.js","../../node_modules/@popperjs/core/lib/dom-utils/listScrollParents.js","../../node_modules/@popperjs/core/lib/dom-utils/getScrollParent.js","../../node_modules/@popperjs/core/lib/utils/rectToClientRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getClippingRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getViewportRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getDocumentRect.js","../../node_modules/@popperjs/core/lib/utils/getVariation.js","../../node_modules/@popperjs/core/lib/utils/computeOffsets.js","../../node_modules/@popperjs/core/lib/utils/detectOverflow.js","../../node_modules/@popperjs/core/lib/utils/computeAutoPlacement.js","../../node_modules/@popperjs/core/lib/modifiers/flip.js","../../node_modules/@popperjs/core/lib/modifiers/hide.js","../../node_modules/@popperjs/core/lib/modifiers/offset.js","../../node_modules/@popperjs/core/lib/modifiers/popperOffsets.js","../../node_modules/@popperjs/core/lib/modifiers/preventOverflow.js","../../node_modules/@popperjs/core/lib/utils/getAltAxis.js","../../node_modules/@popperjs/core/lib/dom-utils/getCompositeRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getNodeScroll.js","../../node_modules/@popperjs/core/lib/dom-utils/getHTMLElementScroll.js","../../node_modules/@popperjs/core/lib/createPopper.js","../../node_modules/@popperjs/core/lib/utils/debounce.js","../../node_modules/@popperjs/core/lib/utils/mergeByName.js","../../node_modules/@popperjs/core/lib/utils/orderModifiers.js","../../node_modules/@popperjs/core/lib/popper-lite.js","../../node_modules/@popperjs/core/lib/popper.js","../../js/src/dropdown.js","../../js/src/modal.js","../../js/src/util/scrollbar.js","../../js/src/offcanvas.js","../../js/src/util/sanitizer.js","../../js/src/tooltip.js","../../js/src/popover.js","../../js/src/scrollspy.js","../../js/src/tab.js","../../js/src/toast.js","../../js/index.umd.js"],"names":["getUID","prefix","Math","floor","random","document","getElementById","getSelector","element","selector","getAttribute","hrefAttr","includes","startsWith","split","trim","getSelectorFromElement","querySelector","getElementFromSelector","getTransitionDurationFromElement","transitionDuration","transitionDelay","window","getComputedStyle","floatTransitionDuration","Number","parseFloat","floatTransitionDelay","triggerTransitionEnd","dispatchEvent","Event","isElement","obj","nodeType","emulateTransitionEnd","duration","called","emulatedDuration","addEventListener","listener","removeEventListener","setTimeout","typeCheckConfig","componentName","config","configTypes","Object","keys","forEach","property","expectedTypes","value","valueType","toString","call","match","toLowerCase","RegExp","test","TypeError","toUpperCase","isVisible","style","parentNode","elementStyle","parentNodeStyle","display","visibility","isDisabled","Node","ELEMENT_NODE","classList","contains","disabled","hasAttribute","findShadowRoot","documentElement","attachShadow","getRootNode","root","ShadowRoot","noop","reflow","offsetHeight","getjQuery","jQuery","body","isRTL","dir","defineJQueryPlugin","name","plugin","callback","$","JQUERY_NO_CONFLICT","fn","jQueryInterface","Constructor","noConflict","readyState","elementMap","Map","Data","set","key","instance","has","instanceMap","get","size","console","error","Array","from","remove","delete","namespaceRegex","stripNameRegex","stripUidRegex","eventRegistry","uidEvent","customEvents","mouseenter","mouseleave","nativeEvents","Set","getUidEvent","uid","getEvent","findHandler","events","handler","delegationSelector","uidEventList","i","len","length","event","originalHandler","normalizeParams","originalTypeEvent","delegationFn","delegation","typeEvent","replace","custom","addHandler","oneOff","handlers","previousFn","domElements","querySelectorAll","target","this","delegateTarget","EventHandler","off","type","apply","bootstrapDelegationHandler","bootstrapHandler","removeHandler","Boolean","on","one","inNamespace","isNamespace","elementEvent","namespace","storeElementEvent","handlerKey","removeNamespacedHandlers","slice","keyHandlers","trigger","args","isNative","jQueryEvent","bubbles","nativeDispatch","defaultPrevented","evt","isPropagationStopped","isImmediatePropagationStopped","isDefaultPrevented","createEvent","initEvent","CustomEvent","cancelable","defineProperty","preventDefault","BaseComponent","constructor","_element","DATA_KEY","dispose","[object Object]","VERSION","Alert","close","rootElement","_getRootElement","customEvent","_triggerCloseEvent","_removeElement","closest","_destroyElement","removeChild","each","data","alertInstance","handleDismiss","Button","toggle","setAttribute","normalizeData","val","normalizeDataKey","chr","button","Manipulator","setDataAttribute","removeDataAttribute","removeAttribute","getDataAttributes","attributes","dataset","filter","pureKey","charAt","getDataAttribute","offset","rect","getBoundingClientRect","top","scrollTop","left","scrollLeft","position","offsetTop","offsetLeft","SelectorEngine","find","concat","Element","prototype","findOne","children","child","matches","parents","ancestor","push","prev","previous","previousElementSibling","next","nextElementSibling","Default","interval","keyboard","slide","pause","wrap","touch","DefaultType","ORDER_NEXT","ORDER_PREV","DIRECTION_LEFT","DIRECTION_RIGHT","Carousel","super","_items","_interval","_activeElement","_isPaused","_isSliding","touchTimeout","touchStartX","touchDeltaX","_config","_getConfig","_indicatorsElement","_touchSupported","navigator","maxTouchPoints","_pointerEvent","PointerEvent","_addEventListeners","_slide","nextWhenVisible","hidden","cycle","clearInterval","_updateInterval","setInterval","visibilityState","bind","to","index","activeIndex","_getItemIndex","order","_handleSwipe","absDeltax","abs","direction","_keydown","_addTouchEventListeners","start","pointerType","touches","clientX","move","end","clearTimeout","itemImg","e","add","tagName","indexOf","_getItemByOrder","activeElement","isNext","isPrev","lastItemIndex","itemIndex","_triggerSlideEvent","relatedTarget","eventDirectionName","targetIndex","fromIndex","_setActiveIndicatorElement","activeIndicator","indicators","parseInt","elementInterval","defaultInterval","directionOrOrder","_directionToOrder","activeElementIndex","nextElement","nextElementIndex","isCycling","directionalClassName","orderClassName","_orderToDirection","action","ride","carouselInterface","slideIndex","dataApiClickHandler","carousels","parent","Collapse","_isTransitioning","_triggerArray","id","toggleList","elem","filterElement","foundElem","_selector","_parent","_getParent","_addAriaAndCollapsedClass","hide","show","actives","activesData","container","tempActiveData","elemActive","collapseInterface","dimension","_getDimension","setTransitioning","scrollSize","triggerArrayLength","isTransitioning","jquery","selected","triggerArray","isOpen","triggerData","bottom","right","basePlacements","variationPlacements","reduce","acc","placement","placements","modifierPhases","getNodeName","nodeName","getWindow","node","ownerDocument","defaultView","isHTMLElement","HTMLElement","isShadowRoot","applyStyles$1","enabled","phase","_ref","state","elements","styles","assign","effect","_ref2","initialStyles","popper","options","strategy","margin","arrow","reference","hasOwnProperty","attribute","requires","getBasePlacement","width","height","x","y","getLayoutRect","clientRect","offsetWidth","rootNode","isSameNode","host","isTableElement","getDocumentElement","getParentNode","assignedSlot","getTrueOffsetParent","offsetParent","getOffsetParent","isFirefox","userAgent","currentNode","css","transform","perspective","contain","willChange","getContainingBlock","getMainAxisFromPlacement","max","min","round","within","mathMax","mathMin","mergePaddingObject","paddingObject","expandToHashMap","hashMap","arrow$1","_state$modifiersData$","arrowElement","popperOffsets","modifiersData","basePlacement","axis","padding","rects","toPaddingObject","arrowRect","minProp","maxProp","endDiff","startDiff","arrowOffsetParent","clientSize","clientHeight","clientWidth","centerToReference","center","axisProp","centerOffset","_options$element","requiresIfExists","unsetSides","mapToStyles","_Object$assign2","popperRect","offsets","gpuAcceleration","adaptive","roundOffsets","_ref3","dpr","devicePixelRatio","roundOffsetsByDPR","_ref3$x","_ref3$y","hasX","hasY","sideX","sideY","win","heightProp","widthProp","_Object$assign","commonStyles","computeStyles$1","_ref4","_options$gpuAccelerat","_options$adaptive","_options$roundOffsets","data-popper-placement","passive","eventListeners","_options$scroll","scroll","_options$resize","resize","scrollParents","scrollParent","update","hash","getOppositePlacement","matched","getOppositeVariationPlacement","getWindowScroll","pageXOffset","pageYOffset","getWindowScrollBarX","isScrollParent","_getComputedStyle","overflow","overflowX","overflowY","listScrollParents","list","_element$ownerDocumen","getScrollParent","isBody","visualViewport","updatedList","rectToClientRect","getClientRectFromMixedType","clippingParent","html","getViewportRect","clientTop","clientLeft","getInnerBoundingClientRect","winScroll","scrollWidth","scrollHeight","getDocumentRect","getVariation","computeOffsets","variation","commonX","commonY","mainAxis","detectOverflow","_options","_options$placement","_options$boundary","boundary","_options$rootBoundary","rootBoundary","_options$elementConte","elementContext","_options$altBoundary","altBoundary","_options$padding","altContext","referenceElement","clippingClientRect","mainClippingParents","clippingParents","clipperElement","getClippingParents","firstClippingParent","clippingRect","accRect","getClippingRect","contextElement","referenceClientRect","popperClientRect","elementClientRect","overflowOffsets","offsetData","multiply","computeAutoPlacement","flipVariations","_options$allowedAutoP","allowedAutoPlacements","allPlacements","allowedPlacements","overflows","sort","a","b","flip$1","_skip","_options$mainAxis","checkMainAxis","_options$altAxis","altAxis","checkAltAxis","specifiedFallbackPlacements","fallbackPlacements","_options$flipVariatio","preferredPlacement","oppositePlacement","getExpandedFallbackPlacements","referenceRect","checksMap","makeFallbackChecks","firstFittingPlacement","_basePlacement","isStartVariation","isVertical","mainVariationSide","altVariationSide","checks","every","check","_loop","_i","fittingPlacement","reset","getSideOffsets","preventedOffsets","isAnySideFullyClipped","some","side","hide$2","preventOverflow","referenceOverflow","popperAltOverflow","referenceClippingOffsets","popperEscapeOffsets","isReferenceHidden","hasPopperEscaped","data-popper-reference-hidden","data-popper-escaped","offset$1","_options$offset","invertDistance","skidding","distance","distanceAndSkiddingToXY","_data$state$placement","popperOffsets$1","preventOverflow$1","_options$tether","tether","_options$tetherOffset","tetherOffset","isBasePlacement","tetherOffsetValue","mainSide","altSide","additive","minLen","maxLen","arrowPaddingObject","arrowPaddingMin","arrowPaddingMax","arrowLen","minOffset","maxOffset","clientOffset","offsetModifierValue","tetherMin","tetherMax","preventedOffset","_mainSide","_altSide","_offset","_min","_max","_preventedOffset","getCompositeRect","elementOrVirtualElement","isFixed","isOffsetParentAnElement","DEFAULT_OPTIONS","modifiers","areValidElements","_len","arguments","_key","popperGenerator","generatorOptions","_generatorOptions","_generatorOptions$def","defaultModifiers","_generatorOptions$def2","defaultOptions","pending","orderedModifiers","effectCleanupFns","isDestroyed","setOptions","cleanupModifierEffects","merged","map","visited","result","modifier","dep","depModifier","orderModifiers","current","existing","m","_ref3$options","cleanupFn","forceUpdate","_state$elements","_state$orderedModifie","_state$orderedModifie2","Promise","resolve","then","undefined","destroy","onFirstUpdate","createPopper","computeStyles","applyStyles","flip","REGEXP_KEYDOWN","PLACEMENT_TOP","PLACEMENT_TOPEND","PLACEMENT_BOTTOM","PLACEMENT_BOTTOMEND","PLACEMENT_RIGHT","PLACEMENT_LEFT","popperConfig","Dropdown","_popper","_menu","_getMenuElement","_inNavbar","_detectNavbar","isActive","clearMenus","getParentFromElement","Popper","_getPopperConfig","isDisplayStatic","focus","_getPlacement","parentDropdown","isEnd","getPropertyValue","_getOffset","popperData","defaultBsPopperConfig","dropdownInterface","toggles","context","clickEvent","dropdownMenu","composedPath","stopPropagation","click","items","dataApiKeydownHandler","backdrop","Modal","_dialog","_backdrop","_isShown","_isBodyOverflowing","_ignoreBackdropClick","_scrollbarWidth","_isAnimated","showEvent","_checkScrollbar","_setScrollbar","_adjustDialog","_setEscapeEvent","_setResizeEvent","_showBackdrop","_showElement","isAnimated","_hideModal","htmlElement","handleUpdate","modalBody","appendChild","_enforceFocus","transitionComplete","_triggerBackdropTransition","_resetAdjustments","_resetScrollbar","_removeBackdrop","createElement","className","currentTarget","backdropTransitionDuration","callbackRemove","isModalOverflowing","modalTransitionDuration","paddingLeft","paddingRight","innerWidth","_getScrollbarWidth","_setElementAttributes","calculatedValue","styleProp","actualValue","_resetElementAttributes","scrollDiv","scrollbarWidth","getWidth","documentWidth","removeProperty","Offcanvas","scrollBarHide","_enforceFocusOnElement","blur","allReadyOpen","el","uriAttrs","SAFE_URL_PATTERN","DATA_URL_PATTERN","allowedAttribute","attr","allowedAttributeList","attrName","nodeValue","regExp","attrRegex","sanitizeHtml","unsafeHtml","allowList","sanitizeFn","createdDocument","DOMParser","parseFromString","allowlistKeys","elName","attributeList","allowedAttributes","innerHTML","BSCLS_PREFIX_REGEX","DISALLOWED_ATTRIBUTES","animation","template","title","delay","customClass","sanitize","AttachmentMap","AUTO","TOP","RIGHT","BOTTOM","LEFT","*","area","br","col","code","div","em","hr","h1","h2","h3","h4","h5","h6","img","li","ol","p","pre","s","small","span","sub","sup","strong","u","ul","HIDE","HIDDEN","SHOW","SHOWN","INSERTED","CLICK","FOCUSIN","FOCUSOUT","MOUSEENTER","MOUSELEAVE","Tooltip","_isEnabled","_timeout","_hoverState","_activeTrigger","tip","_setListeners","NAME","EVENT_KEY","enable","disable","toggleEnabled","_initializeOnDelegatedTarget","_isWithActiveTrigger","_enter","_leave","getTipElement","_hideModalHandler","Error","isWithContent","shadowRoot","isInTheDom","tipId","setContent","attachment","_getAttachment","_addAttachmentClass","_getContainer","complete","prevHoverState","_cleanTipClass","getTitle","setElementContent","content","textContent","updateAttachment","dataKey","_getDelegateConfig","_handlePopperPlacementChange","eventIn","eventOut","_fixTitle","originalTitleType","dataAttributes","dataAttr","tabClass","token","tClass","Popover","_getContent","method","ScrollSpy","_scrollElement","_offsets","_targets","_activeTarget","_scrollHeight","_process","refresh","autoMethod","offsetMethod","offsetBase","_getScrollTop","_getScrollHeight","targetSelector","targetBCR","item","_getOffsetHeight","innerHeight","maxScroll","_activate","_clear","queries","link","join","listGroup","navItem","spy","Tab","listElement","itemSelector","hideEvent","active","_transitionComplete","dropdownChild","dropdown","autohide","Toast","_clearTimeout"],"mappings":"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","sourcesContent":["/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.0.0-beta3): util/index.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst MAX_UID = 1000000\nconst MILLISECONDS_MULTIPLIER = 1000\nconst TRANSITION_END = 'transitionend'\n\n// Shoutout AngusCroll (https://goo.gl/pxwQGp)\nconst toType = obj => {\n if (obj === null || obj === undefined) {\n return `${obj}`\n }\n\n return {}.toString.call(obj).match(/\\s([a-z]+)/i)[1].toLowerCase()\n}\n\n/**\n * --------------------------------------------------------------------------\n * Public Util Api\n * --------------------------------------------------------------------------\n */\n\nconst getUID = prefix => {\n do {\n prefix += Math.floor(Math.random() * MAX_UID)\n } while (document.getElementById(prefix))\n\n return prefix\n}\n\nconst getSelector = element => {\n let selector = element.getAttribute('data-bs-target')\n\n if (!selector || selector === '#') {\n let hrefAttr = element.getAttribute('href')\n\n // The only valid content that could double as a selector are IDs or classes,\n // so everything starting with `#` or `.`. If a \"real\" URL is used as the selector,\n // `document.querySelector` will rightfully complain it is invalid.\n // See https://github.com/twbs/bootstrap/issues/32273\n if (!hrefAttr || (!hrefAttr.includes('#') && !hrefAttr.startsWith('.'))) {\n return null\n }\n\n // Just in case some CMS puts out a full URL with the anchor appended\n if (hrefAttr.includes('#') && !hrefAttr.startsWith('#')) {\n hrefAttr = '#' + hrefAttr.split('#')[1]\n }\n\n selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : null\n }\n\n return selector\n}\n\nconst getSelectorFromElement = element => {\n const selector = getSelector(element)\n\n if (selector) {\n return document.querySelector(selector) ? selector : null\n }\n\n return null\n}\n\nconst getElementFromSelector = element => {\n const selector = getSelector(element)\n\n return selector ? document.querySelector(selector) : null\n}\n\nconst getTransitionDurationFromElement = element => {\n if (!element) {\n return 0\n }\n\n // Get transition-duration of the element\n let { transitionDuration, transitionDelay } = window.getComputedStyle(element)\n\n const floatTransitionDuration = Number.parseFloat(transitionDuration)\n const floatTransitionDelay = Number.parseFloat(transitionDelay)\n\n // Return 0 if element or transition duration is not found\n if (!floatTransitionDuration && !floatTransitionDelay) {\n return 0\n }\n\n // If multiple durations are defined, take the first\n transitionDuration = transitionDuration.split(',')[0]\n transitionDelay = transitionDelay.split(',')[0]\n\n return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER\n}\n\nconst triggerTransitionEnd = element => {\n element.dispatchEvent(new Event(TRANSITION_END))\n}\n\nconst isElement = obj => (obj[0] || obj).nodeType\n\nconst emulateTransitionEnd = (element, duration) => {\n let called = false\n const durationPadding = 5\n const emulatedDuration = duration + durationPadding\n\n function listener() {\n called = true\n element.removeEventListener(TRANSITION_END, listener)\n }\n\n element.addEventListener(TRANSITION_END, listener)\n setTimeout(() => {\n if (!called) {\n triggerTransitionEnd(element)\n }\n }, emulatedDuration)\n}\n\nconst typeCheckConfig = (componentName, config, configTypes) => {\n Object.keys(configTypes).forEach(property => {\n const expectedTypes = configTypes[property]\n const value = config[property]\n const valueType = value && isElement(value) ? 'element' : toType(value)\n\n if (!new RegExp(expectedTypes).test(valueType)) {\n throw new TypeError(\n `${componentName.toUpperCase()}: ` +\n `Option \"${property}\" provided type \"${valueType}\" ` +\n `but expected type \"${expectedTypes}\".`\n )\n }\n })\n}\n\nconst isVisible = element => {\n if (!element) {\n return false\n }\n\n if (element.style && element.parentNode && element.parentNode.style) {\n const elementStyle = getComputedStyle(element)\n const parentNodeStyle = getComputedStyle(element.parentNode)\n\n return elementStyle.display !== 'none' &&\n parentNodeStyle.display !== 'none' &&\n elementStyle.visibility !== 'hidden'\n }\n\n return false\n}\n\nconst isDisabled = element => {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n return true\n }\n\n if (element.classList.contains('disabled')) {\n return true\n }\n\n if (typeof element.disabled !== 'undefined') {\n return element.disabled\n }\n\n return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false'\n}\n\nconst findShadowRoot = element => {\n if (!document.documentElement.attachShadow) {\n return null\n }\n\n // Can find the shadow root otherwise it'll return the document\n if (typeof element.getRootNode === 'function') {\n const root = element.getRootNode()\n return root instanceof ShadowRoot ? root : null\n }\n\n if (element instanceof ShadowRoot) {\n return element\n }\n\n // when we don't find a shadow root\n if (!element.parentNode) {\n return null\n }\n\n return findShadowRoot(element.parentNode)\n}\n\nconst noop = () => function () {}\n\nconst reflow = element => element.offsetHeight\n\nconst getjQuery = () => {\n const { jQuery } = window\n\n if (jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {\n return jQuery\n }\n\n return null\n}\n\nconst onDOMContentLoaded = callback => {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', callback)\n } else {\n callback()\n }\n}\n\nconst isRTL = () => document.documentElement.dir === 'rtl'\n\nconst defineJQueryPlugin = (name, plugin) => {\n onDOMContentLoaded(() => {\n const $ = getjQuery()\n /* istanbul ignore if */\n if ($) {\n const JQUERY_NO_CONFLICT = $.fn[name]\n $.fn[name] = plugin.jQueryInterface\n $.fn[name].Constructor = plugin\n $.fn[name].noConflict = () => {\n $.fn[name] = JQUERY_NO_CONFLICT\n return plugin.jQueryInterface\n }\n }\n })\n}\n\nexport {\n getUID,\n getSelectorFromElement,\n getElementFromSelector,\n getTransitionDurationFromElement,\n triggerTransitionEnd,\n isElement,\n emulateTransitionEnd,\n typeCheckConfig,\n isVisible,\n isDisabled,\n findShadowRoot,\n noop,\n reflow,\n getjQuery,\n onDOMContentLoaded,\n isRTL,\n defineJQueryPlugin\n}\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.0.0-beta3): dom/data.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst elementMap = new Map()\n\nexport default {\n set(element, key, instance) {\n if (!elementMap.has(element)) {\n elementMap.set(element, new Map())\n }\n\n const instanceMap = elementMap.get(element)\n\n // make it clear we only want one instance per element\n // can be removed later when multiple key/instances are fine to be used\n if (!instanceMap.has(key) && instanceMap.size !== 0) {\n // eslint-disable-next-line no-console\n console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`)\n return\n }\n\n instanceMap.set(key, instance)\n },\n\n get(element, key) {\n if (elementMap.has(element)) {\n return elementMap.get(element).get(key) || null\n }\n\n return null\n },\n\n remove(element, key) {\n if (!elementMap.has(element)) {\n return\n }\n\n const instanceMap = elementMap.get(element)\n\n instanceMap.delete(key)\n\n // free up element references if there are no instances left for an element\n if (instanceMap.size === 0) {\n elementMap.delete(element)\n }\n }\n}\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.0.0-beta3): dom/event-handler.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport { getjQuery } from '../util/index'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst namespaceRegex = /[^.]*(?=\\..*)\\.|.*/\nconst stripNameRegex = /\\..*/\nconst stripUidRegex = /::\\d+$/\nconst eventRegistry = {} // Events storage\nlet uidEvent = 1\nconst customEvents = {\n mouseenter: 'mouseover',\n mouseleave: 'mouseout'\n}\nconst nativeEvents = new Set([\n 'click',\n 'dblclick',\n 'mouseup',\n 'mousedown',\n 'contextmenu',\n 'mousewheel',\n 'DOMMouseScroll',\n 'mouseover',\n 'mouseout',\n 'mousemove',\n 'selectstart',\n 'selectend',\n 'keydown',\n 'keypress',\n 'keyup',\n 'orientationchange',\n 'touchstart',\n 'touchmove',\n 'touchend',\n 'touchcancel',\n 'pointerdown',\n 'pointermove',\n 'pointerup',\n 'pointerleave',\n 'pointercancel',\n 'gesturestart',\n 'gesturechange',\n 'gestureend',\n 'focus',\n 'blur',\n 'change',\n 'reset',\n 'select',\n 'submit',\n 'focusin',\n 'focusout',\n 'load',\n 'unload',\n 'beforeunload',\n 'resize',\n 'move',\n 'DOMContentLoaded',\n 'readystatechange',\n 'error',\n 'abort',\n 'scroll'\n])\n\n/**\n * ------------------------------------------------------------------------\n * Private methods\n * ------------------------------------------------------------------------\n */\n\nfunction getUidEvent(element, uid) {\n return (uid && `${uid}::${uidEvent++}`) || element.uidEvent || uidEvent++\n}\n\nfunction getEvent(element) {\n const uid = getUidEvent(element)\n\n element.uidEvent = uid\n eventRegistry[uid] = eventRegistry[uid] || {}\n\n return eventRegistry[uid]\n}\n\nfunction bootstrapHandler(element, fn) {\n return function handler(event) {\n event.delegateTarget = element\n\n if (handler.oneOff) {\n EventHandler.off(element, event.type, fn)\n }\n\n return fn.apply(element, [event])\n }\n}\n\nfunction bootstrapDelegationHandler(element, selector, fn) {\n return function handler(event) {\n const domElements = element.querySelectorAll(selector)\n\n for (let { target } = event; target && target !== this; target = target.parentNode) {\n for (let i = domElements.length; i--;) {\n if (domElements[i] === target) {\n event.delegateTarget = target\n\n if (handler.oneOff) {\n // eslint-disable-next-line unicorn/consistent-destructuring\n EventHandler.off(element, event.type, fn)\n }\n\n return fn.apply(target, [event])\n }\n }\n }\n\n // To please ESLint\n return null\n }\n}\n\nfunction findHandler(events, handler, delegationSelector = null) {\n const uidEventList = Object.keys(events)\n\n for (let i = 0, len = uidEventList.length; i < len; i++) {\n const event = events[uidEventList[i]]\n\n if (event.originalHandler === handler && event.delegationSelector === delegationSelector) {\n return event\n }\n }\n\n return null\n}\n\nfunction normalizeParams(originalTypeEvent, handler, delegationFn) {\n const delegation = typeof handler === 'string'\n const originalHandler = delegation ? delegationFn : handler\n\n // allow to get the native events from namespaced events ('click.bs.button' --> 'click')\n let typeEvent = originalTypeEvent.replace(stripNameRegex, '')\n const custom = customEvents[typeEvent]\n\n if (custom) {\n typeEvent = custom\n }\n\n const isNative = nativeEvents.has(typeEvent)\n\n if (!isNative) {\n typeEvent = originalTypeEvent\n }\n\n return [delegation, originalHandler, typeEvent]\n}\n\nfunction addHandler(element, originalTypeEvent, handler, delegationFn, oneOff) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return\n }\n\n if (!handler) {\n handler = delegationFn\n delegationFn = null\n }\n\n const [delegation, originalHandler, typeEvent] = normalizeParams(originalTypeEvent, handler, delegationFn)\n const events = getEvent(element)\n const handlers = events[typeEvent] || (events[typeEvent] = {})\n const previousFn = findHandler(handlers, originalHandler, delegation ? handler : null)\n\n if (previousFn) {\n previousFn.oneOff = previousFn.oneOff && oneOff\n\n return\n }\n\n const uid = getUidEvent(originalHandler, originalTypeEvent.replace(namespaceRegex, ''))\n const fn = delegation ?\n bootstrapDelegationHandler(element, handler, delegationFn) :\n bootstrapHandler(element, handler)\n\n fn.delegationSelector = delegation ? handler : null\n fn.originalHandler = originalHandler\n fn.oneOff = oneOff\n fn.uidEvent = uid\n handlers[uid] = fn\n\n element.addEventListener(typeEvent, fn, delegation)\n}\n\nfunction removeHandler(element, events, typeEvent, handler, delegationSelector) {\n const fn = findHandler(events[typeEvent], handler, delegationSelector)\n\n if (!fn) {\n return\n }\n\n element.removeEventListener(typeEvent, fn, Boolean(delegationSelector))\n delete events[typeEvent][fn.uidEvent]\n}\n\nfunction removeNamespacedHandlers(element, events, typeEvent, namespace) {\n const storeElementEvent = events[typeEvent] || {}\n\n Object.keys(storeElementEvent).forEach(handlerKey => {\n if (handlerKey.includes(namespace)) {\n const event = storeElementEvent[handlerKey]\n\n removeHandler(element, events, typeEvent, event.originalHandler, event.delegationSelector)\n }\n })\n}\n\nconst EventHandler = {\n on(element, event, handler, delegationFn) {\n addHandler(element, event, handler, delegationFn, false)\n },\n\n one(element, event, handler, delegationFn) {\n addHandler(element, event, handler, delegationFn, true)\n },\n\n off(element, originalTypeEvent, handler, delegationFn) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return\n }\n\n const [delegation, originalHandler, typeEvent] = normalizeParams(originalTypeEvent, handler, delegationFn)\n const inNamespace = typeEvent !== originalTypeEvent\n const events = getEvent(element)\n const isNamespace = originalTypeEvent.startsWith('.')\n\n if (typeof originalHandler !== 'undefined') {\n // Simplest case: handler is passed, remove that listener ONLY.\n if (!events || !events[typeEvent]) {\n return\n }\n\n removeHandler(element, events, typeEvent, originalHandler, delegation ? handler : null)\n return\n }\n\n if (isNamespace) {\n Object.keys(events).forEach(elementEvent => {\n removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1))\n })\n }\n\n const storeElementEvent = events[typeEvent] || {}\n Object.keys(storeElementEvent).forEach(keyHandlers => {\n const handlerKey = keyHandlers.replace(stripUidRegex, '')\n\n if (!inNamespace || originalTypeEvent.includes(handlerKey)) {\n const event = storeElementEvent[keyHandlers]\n\n removeHandler(element, events, typeEvent, event.originalHandler, event.delegationSelector)\n }\n })\n },\n\n trigger(element, event, args) {\n if (typeof event !== 'string' || !element) {\n return null\n }\n\n const $ = getjQuery()\n const typeEvent = event.replace(stripNameRegex, '')\n const inNamespace = event !== typeEvent\n const isNative = nativeEvents.has(typeEvent)\n\n let jQueryEvent\n let bubbles = true\n let nativeDispatch = true\n let defaultPrevented = false\n let evt = null\n\n if (inNamespace && $) {\n jQueryEvent = $.Event(event, args)\n\n $(element).trigger(jQueryEvent)\n bubbles = !jQueryEvent.isPropagationStopped()\n nativeDispatch = !jQueryEvent.isImmediatePropagationStopped()\n defaultPrevented = jQueryEvent.isDefaultPrevented()\n }\n\n if (isNative) {\n evt = document.createEvent('HTMLEvents')\n evt.initEvent(typeEvent, bubbles, true)\n } else {\n evt = new CustomEvent(event, {\n bubbles,\n cancelable: true\n })\n }\n\n // merge custom information in our event\n if (typeof args !== 'undefined') {\n Object.keys(args).forEach(key => {\n Object.defineProperty(evt, key, {\n get() {\n return args[key]\n }\n })\n })\n }\n\n if (defaultPrevented) {\n evt.preventDefault()\n }\n\n if (nativeDispatch) {\n element.dispatchEvent(evt)\n }\n\n if (evt.defaultPrevented && typeof jQueryEvent !== 'undefined') {\n jQueryEvent.preventDefault()\n }\n\n return evt\n }\n}\n\nexport default EventHandler\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.0.0-beta3): base-component.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport Data from './dom/data'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst VERSION = '5.0.0-beta3'\n\nclass BaseComponent {\n constructor(element) {\n element = typeof element === 'string' ? document.querySelector(element) : element\n\n if (!element) {\n return\n }\n\n this._element = element\n Data.set(this._element, this.constructor.DATA_KEY, this)\n }\n\n dispose() {\n Data.remove(this._element, this.constructor.DATA_KEY)\n this._element = null\n }\n\n /** Static */\n\n static getInstance(element) {\n return Data.get(element, this.DATA_KEY)\n }\n\n static get VERSION() {\n return VERSION\n }\n}\n\nexport default BaseComponent\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.0.0-beta3): alert.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport {\n defineJQueryPlugin,\n emulateTransitionEnd,\n getElementFromSelector,\n getTransitionDurationFromElement\n} from './util/index'\nimport Data from './dom/data'\nimport EventHandler from './dom/event-handler'\nimport BaseComponent from './base-component'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'alert'\nconst DATA_KEY = 'bs.alert'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst SELECTOR_DISMISS = '[data-bs-dismiss=\"alert\"]'\n\nconst EVENT_CLOSE = `close${EVENT_KEY}`\nconst EVENT_CLOSED = `closed${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_ALERT = 'alert'\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_SHOW = 'show'\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Alert extends BaseComponent {\n // Getters\n\n static get DATA_KEY() {\n return DATA_KEY\n }\n\n // Public\n\n close(element) {\n const rootElement = element ? this._getRootElement(element) : this._element\n const customEvent = this._triggerCloseEvent(rootElement)\n\n if (customEvent === null || customEvent.defaultPrevented) {\n return\n }\n\n this._removeElement(rootElement)\n }\n\n // Private\n\n _getRootElement(element) {\n return getElementFromSelector(element) || element.closest(`.${CLASS_NAME_ALERT}`)\n }\n\n _triggerCloseEvent(element) {\n return EventHandler.trigger(element, EVENT_CLOSE)\n }\n\n _removeElement(element) {\n element.classList.remove(CLASS_NAME_SHOW)\n\n if (!element.classList.contains(CLASS_NAME_FADE)) {\n this._destroyElement(element)\n return\n }\n\n const transitionDuration = getTransitionDurationFromElement(element)\n\n EventHandler.one(element, 'transitionend', () => this._destroyElement(element))\n emulateTransitionEnd(element, transitionDuration)\n }\n\n _destroyElement(element) {\n if (element.parentNode) {\n element.parentNode.removeChild(element)\n }\n\n EventHandler.trigger(element, EVENT_CLOSED)\n }\n\n // Static\n\n static jQueryInterface(config) {\n return this.each(function () {\n let data = Data.get(this, DATA_KEY)\n\n if (!data) {\n data = new Alert(this)\n }\n\n if (config === 'close') {\n data[config](this)\n }\n })\n }\n\n static handleDismiss(alertInstance) {\n return function (event) {\n if (event) {\n event.preventDefault()\n }\n\n alertInstance.close(this)\n }\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DISMISS, Alert.handleDismiss(new Alert()))\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n * add .Alert to jQuery only if jQuery is present\n */\n\ndefineJQueryPlugin(NAME, Alert)\n\nexport default Alert\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.0.0-beta3): button.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport { defineJQueryPlugin } from './util/index'\nimport Data from './dom/data'\nimport EventHandler from './dom/event-handler'\nimport BaseComponent from './base-component'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'button'\nconst DATA_KEY = 'bs.button'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst CLASS_NAME_ACTIVE = 'active'\n\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"button\"]'\n\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Button extends BaseComponent {\n // Getters\n\n static get DATA_KEY() {\n return DATA_KEY\n }\n\n // Public\n\n toggle() {\n // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method\n this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE))\n }\n\n // Static\n\n static jQueryInterface(config) {\n return this.each(function () {\n let data = Data.get(this, DATA_KEY)\n\n if (!data) {\n data = new Button(this)\n }\n\n if (config === 'toggle') {\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => {\n event.preventDefault()\n\n const button = event.target.closest(SELECTOR_DATA_TOGGLE)\n\n let data = Data.get(button, DATA_KEY)\n if (!data) {\n data = new Button(button)\n }\n\n data.toggle()\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n * add .Button to jQuery only if jQuery is present\n */\n\ndefineJQueryPlugin(NAME, Button)\n\nexport default Button\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.0.0-beta3): dom/manipulator.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nfunction normalizeData(val) {\n if (val === 'true') {\n return true\n }\n\n if (val === 'false') {\n return false\n }\n\n if (val === Number(val).toString()) {\n return Number(val)\n }\n\n if (val === '' || val === 'null') {\n return null\n }\n\n return val\n}\n\nfunction normalizeDataKey(key) {\n return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`)\n}\n\nconst Manipulator = {\n setDataAttribute(element, key, value) {\n element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value)\n },\n\n removeDataAttribute(element, key) {\n element.removeAttribute(`data-bs-${normalizeDataKey(key)}`)\n },\n\n getDataAttributes(element) {\n if (!element) {\n return {}\n }\n\n const attributes = {}\n\n Object.keys(element.dataset)\n .filter(key => key.startsWith('bs'))\n .forEach(key => {\n let pureKey = key.replace(/^bs/, '')\n pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length)\n attributes[pureKey] = normalizeData(element.dataset[key])\n })\n\n return attributes\n },\n\n getDataAttribute(element, key) {\n return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`))\n },\n\n offset(element) {\n const rect = element.getBoundingClientRect()\n\n return {\n top: rect.top + document.body.scrollTop,\n left: rect.left + document.body.scrollLeft\n }\n },\n\n position(element) {\n return {\n top: element.offsetTop,\n left: element.offsetLeft\n }\n }\n}\n\nexport default Manipulator\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.0.0-beta3): dom/selector-engine.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NODE_TEXT = 3\n\nconst SelectorEngine = {\n find(selector, element = document.documentElement) {\n return [].concat(...Element.prototype.querySelectorAll.call(element, selector))\n },\n\n findOne(selector, element = document.documentElement) {\n return Element.prototype.querySelector.call(element, selector)\n },\n\n children(element, selector) {\n return [].concat(...element.children)\n .filter(child => child.matches(selector))\n },\n\n parents(element, selector) {\n const parents = []\n\n let ancestor = element.parentNode\n\n while (ancestor && ancestor.nodeType === Node.ELEMENT_NODE && ancestor.nodeType !== NODE_TEXT) {\n if (ancestor.matches(selector)) {\n parents.push(ancestor)\n }\n\n ancestor = ancestor.parentNode\n }\n\n return parents\n },\n\n prev(element, selector) {\n let previous = element.previousElementSibling\n\n while (previous) {\n if (previous.matches(selector)) {\n return [previous]\n }\n\n previous = previous.previousElementSibling\n }\n\n return []\n },\n\n next(element, selector) {\n let next = element.nextElementSibling\n\n while (next) {\n if (next.matches(selector)) {\n return [next]\n }\n\n next = next.nextElementSibling\n }\n\n return []\n }\n}\n\nexport default SelectorEngine\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.0.0-beta3): carousel.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport {\n defineJQueryPlugin,\n emulateTransitionEnd,\n getElementFromSelector,\n getTransitionDurationFromElement,\n isRTL,\n isVisible,\n reflow,\n triggerTransitionEnd,\n typeCheckConfig\n} from './util/index'\nimport Data from './dom/data'\nimport EventHandler from './dom/event-handler'\nimport Manipulator from './dom/manipulator'\nimport SelectorEngine from './dom/selector-engine'\nimport BaseComponent from './base-component'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'carousel'\nconst DATA_KEY = 'bs.carousel'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst ARROW_LEFT_KEY = 'ArrowLeft'\nconst ARROW_RIGHT_KEY = 'ArrowRight'\nconst TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch\nconst SWIPE_THRESHOLD = 40\n\nconst Default = {\n interval: 5000,\n keyboard: true,\n slide: false,\n pause: 'hover',\n wrap: true,\n touch: true\n}\n\nconst DefaultType = {\n interval: '(number|boolean)',\n keyboard: 'boolean',\n slide: '(boolean|string)',\n pause: '(string|boolean)',\n wrap: 'boolean',\n touch: 'boolean'\n}\n\nconst ORDER_NEXT = 'next'\nconst ORDER_PREV = 'prev'\nconst DIRECTION_LEFT = 'left'\nconst DIRECTION_RIGHT = 'right'\n\nconst EVENT_SLIDE = `slide${EVENT_KEY}`\nconst EVENT_SLID = `slid${EVENT_KEY}`\nconst EVENT_KEYDOWN = `keydown${EVENT_KEY}`\nconst EVENT_MOUSEENTER = `mouseenter${EVENT_KEY}`\nconst EVENT_MOUSELEAVE = `mouseleave${EVENT_KEY}`\nconst EVENT_TOUCHSTART = `touchstart${EVENT_KEY}`\nconst EVENT_TOUCHMOVE = `touchmove${EVENT_KEY}`\nconst EVENT_TOUCHEND = `touchend${EVENT_KEY}`\nconst EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}`\nconst EVENT_POINTERUP = `pointerup${EVENT_KEY}`\nconst EVENT_DRAG_START = `dragstart${EVENT_KEY}`\nconst EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_CAROUSEL = 'carousel'\nconst CLASS_NAME_ACTIVE = 'active'\nconst CLASS_NAME_SLIDE = 'slide'\nconst CLASS_NAME_END = 'carousel-item-end'\nconst CLASS_NAME_START = 'carousel-item-start'\nconst CLASS_NAME_NEXT = 'carousel-item-next'\nconst CLASS_NAME_PREV = 'carousel-item-prev'\nconst CLASS_NAME_POINTER_EVENT = 'pointer-event'\n\nconst SELECTOR_ACTIVE = '.active'\nconst SELECTOR_ACTIVE_ITEM = '.active.carousel-item'\nconst SELECTOR_ITEM = '.carousel-item'\nconst SELECTOR_ITEM_IMG = '.carousel-item img'\nconst SELECTOR_NEXT_PREV = '.carousel-item-next, .carousel-item-prev'\nconst SELECTOR_INDICATORS = '.carousel-indicators'\nconst SELECTOR_INDICATOR = '[data-bs-target]'\nconst SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]'\nconst SELECTOR_DATA_RIDE = '[data-bs-ride=\"carousel\"]'\n\nconst POINTER_TYPE_TOUCH = 'touch'\nconst POINTER_TYPE_PEN = 'pen'\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\nclass Carousel extends BaseComponent {\n constructor(element, config) {\n super(element)\n\n this._items = null\n this._interval = null\n this._activeElement = null\n this._isPaused = false\n this._isSliding = false\n this.touchTimeout = null\n this.touchStartX = 0\n this.touchDeltaX = 0\n\n this._config = this._getConfig(config)\n this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element)\n this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0\n this._pointerEvent = Boolean(window.PointerEvent)\n\n this._addEventListeners()\n }\n\n // Getters\n\n static get Default() {\n return Default\n }\n\n static get DATA_KEY() {\n return DATA_KEY\n }\n\n // Public\n\n next() {\n if (!this._isSliding) {\n this._slide(ORDER_NEXT)\n }\n }\n\n nextWhenVisible() {\n // Don't call next when the page isn't visible\n // or the carousel or its parent isn't visible\n if (!document.hidden && isVisible(this._element)) {\n this.next()\n }\n }\n\n prev() {\n if (!this._isSliding) {\n this._slide(ORDER_PREV)\n }\n }\n\n pause(event) {\n if (!event) {\n this._isPaused = true\n }\n\n if (SelectorEngine.findOne(SELECTOR_NEXT_PREV, this._element)) {\n triggerTransitionEnd(this._element)\n this.cycle(true)\n }\n\n clearInterval(this._interval)\n this._interval = null\n }\n\n cycle(event) {\n if (!event) {\n this._isPaused = false\n }\n\n if (this._interval) {\n clearInterval(this._interval)\n this._interval = null\n }\n\n if (this._config && this._config.interval && !this._isPaused) {\n this._updateInterval()\n\n this._interval = setInterval(\n (document.visibilityState ? this.nextWhenVisible : this.next).bind(this),\n this._config.interval\n )\n }\n }\n\n to(index) {\n this._activeElement = SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element)\n const activeIndex = this._getItemIndex(this._activeElement)\n\n if (index > this._items.length - 1 || index < 0) {\n return\n }\n\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.to(index))\n return\n }\n\n if (activeIndex === index) {\n this.pause()\n this.cycle()\n return\n }\n\n const order = index > activeIndex ?\n ORDER_NEXT :\n ORDER_PREV\n\n this._slide(order, this._items[index])\n }\n\n dispose() {\n EventHandler.off(this._element, EVENT_KEY)\n\n this._items = null\n this._config = null\n this._interval = null\n this._isPaused = null\n this._isSliding = null\n this._activeElement = null\n this._indicatorsElement = null\n\n super.dispose()\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _handleSwipe() {\n const absDeltax = Math.abs(this.touchDeltaX)\n\n if (absDeltax <= SWIPE_THRESHOLD) {\n return\n }\n\n const direction = absDeltax / this.touchDeltaX\n\n this.touchDeltaX = 0\n\n if (!direction) {\n return\n }\n\n this._slide(direction > 0 ? DIRECTION_RIGHT : DIRECTION_LEFT)\n }\n\n _addEventListeners() {\n if (this._config.keyboard) {\n EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event))\n }\n\n if (this._config.pause === 'hover') {\n EventHandler.on(this._element, EVENT_MOUSEENTER, event => this.pause(event))\n EventHandler.on(this._element, EVENT_MOUSELEAVE, event => this.cycle(event))\n }\n\n if (this._config.touch && this._touchSupported) {\n this._addTouchEventListeners()\n }\n }\n\n _addTouchEventListeners() {\n const start = event => {\n if (this._pointerEvent && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH)) {\n this.touchStartX = event.clientX\n } else if (!this._pointerEvent) {\n this.touchStartX = event.touches[0].clientX\n }\n }\n\n const move = event => {\n // ensure swiping with one touch and not pinching\n this.touchDeltaX = event.touches && event.touches.length > 1 ?\n 0 :\n event.touches[0].clientX - this.touchStartX\n }\n\n const end = event => {\n if (this._pointerEvent && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH)) {\n this.touchDeltaX = event.clientX - this.touchStartX\n }\n\n this._handleSwipe()\n if (this._config.pause === 'hover') {\n // If it's a touch-enabled device, mouseenter/leave are fired as\n // part of the mouse compatibility events on first tap - the carousel\n // would stop cycling until user tapped out of it;\n // here, we listen for touchend, explicitly pause the carousel\n // (as if it's the second time we tap on it, mouseenter compat event\n // is NOT fired) and after a timeout (to allow for mouse compatibility\n // events to fire) we explicitly restart cycling\n\n this.pause()\n if (this.touchTimeout) {\n clearTimeout(this.touchTimeout)\n }\n\n this.touchTimeout = setTimeout(event => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval)\n }\n }\n\n SelectorEngine.find(SELECTOR_ITEM_IMG, this._element).forEach(itemImg => {\n EventHandler.on(itemImg, EVENT_DRAG_START, e => e.preventDefault())\n })\n\n if (this._pointerEvent) {\n EventHandler.on(this._element, EVENT_POINTERDOWN, event => start(event))\n EventHandler.on(this._element, EVENT_POINTERUP, event => end(event))\n\n this._element.classList.add(CLASS_NAME_POINTER_EVENT)\n } else {\n EventHandler.on(this._element, EVENT_TOUCHSTART, event => start(event))\n EventHandler.on(this._element, EVENT_TOUCHMOVE, event => move(event))\n EventHandler.on(this._element, EVENT_TOUCHEND, event => end(event))\n }\n }\n\n _keydown(event) {\n if (/input|textarea/i.test(event.target.tagName)) {\n return\n }\n\n if (event.key === ARROW_LEFT_KEY) {\n event.preventDefault()\n this._slide(DIRECTION_LEFT)\n } else if (event.key === ARROW_RIGHT_KEY) {\n event.preventDefault()\n this._slide(DIRECTION_RIGHT)\n }\n }\n\n _getItemIndex(element) {\n this._items = element && element.parentNode ?\n SelectorEngine.find(SELECTOR_ITEM, element.parentNode) :\n []\n\n return this._items.indexOf(element)\n }\n\n _getItemByOrder(order, activeElement) {\n const isNext = order === ORDER_NEXT\n const isPrev = order === ORDER_PREV\n const activeIndex = this._getItemIndex(activeElement)\n const lastItemIndex = this._items.length - 1\n const isGoingToWrap = (isPrev && activeIndex === 0) || (isNext && activeIndex === lastItemIndex)\n\n if (isGoingToWrap && !this._config.wrap) {\n return activeElement\n }\n\n const delta = isPrev ? -1 : 1\n const itemIndex = (activeIndex + delta) % this._items.length\n\n return itemIndex === -1 ?\n this._items[this._items.length - 1] :\n this._items[itemIndex]\n }\n\n _triggerSlideEvent(relatedTarget, eventDirectionName) {\n const targetIndex = this._getItemIndex(relatedTarget)\n const fromIndex = this._getItemIndex(SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element))\n\n return EventHandler.trigger(this._element, EVENT_SLIDE, {\n relatedTarget,\n direction: eventDirectionName,\n from: fromIndex,\n to: targetIndex\n })\n }\n\n _setActiveIndicatorElement(element) {\n if (this._indicatorsElement) {\n const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement)\n\n activeIndicator.classList.remove(CLASS_NAME_ACTIVE)\n activeIndicator.removeAttribute('aria-current')\n\n const indicators = SelectorEngine.find(SELECTOR_INDICATOR, this._indicatorsElement)\n\n for (let i = 0; i < indicators.length; i++) {\n if (Number.parseInt(indicators[i].getAttribute('data-bs-slide-to'), 10) === this._getItemIndex(element)) {\n indicators[i].classList.add(CLASS_NAME_ACTIVE)\n indicators[i].setAttribute('aria-current', 'true')\n break\n }\n }\n }\n }\n\n _updateInterval() {\n const element = this._activeElement || SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element)\n\n if (!element) {\n return\n }\n\n const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10)\n\n if (elementInterval) {\n this._config.defaultInterval = this._config.defaultInterval || this._config.interval\n this._config.interval = elementInterval\n } else {\n this._config.interval = this._config.defaultInterval || this._config.interval\n }\n }\n\n _slide(directionOrOrder, element) {\n const order = this._directionToOrder(directionOrOrder)\n const activeElement = SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element)\n const activeElementIndex = this._getItemIndex(activeElement)\n const nextElement = element || this._getItemByOrder(order, activeElement)\n\n const nextElementIndex = this._getItemIndex(nextElement)\n const isCycling = Boolean(this._interval)\n\n const isNext = order === ORDER_NEXT\n const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END\n const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV\n const eventDirectionName = this._orderToDirection(order)\n\n if (nextElement && nextElement.classList.contains(CLASS_NAME_ACTIVE)) {\n this._isSliding = false\n return\n }\n\n const slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName)\n if (slideEvent.defaultPrevented) {\n return\n }\n\n if (!activeElement || !nextElement) {\n // Some weirdness is happening, so we bail\n return\n }\n\n this._isSliding = true\n\n if (isCycling) {\n this.pause()\n }\n\n this._setActiveIndicatorElement(nextElement)\n this._activeElement = nextElement\n\n if (this._element.classList.contains(CLASS_NAME_SLIDE)) {\n nextElement.classList.add(orderClassName)\n\n reflow(nextElement)\n\n activeElement.classList.add(directionalClassName)\n nextElement.classList.add(directionalClassName)\n\n const transitionDuration = getTransitionDurationFromElement(activeElement)\n\n EventHandler.one(activeElement, 'transitionend', () => {\n nextElement.classList.remove(directionalClassName, orderClassName)\n nextElement.classList.add(CLASS_NAME_ACTIVE)\n\n activeElement.classList.remove(CLASS_NAME_ACTIVE, orderClassName, directionalClassName)\n\n this._isSliding = false\n\n setTimeout(() => {\n EventHandler.trigger(this._element, EVENT_SLID, {\n relatedTarget: nextElement,\n direction: eventDirectionName,\n from: activeElementIndex,\n to: nextElementIndex\n })\n }, 0)\n })\n\n emulateTransitionEnd(activeElement, transitionDuration)\n } else {\n activeElement.classList.remove(CLASS_NAME_ACTIVE)\n nextElement.classList.add(CLASS_NAME_ACTIVE)\n\n this._isSliding = false\n EventHandler.trigger(this._element, EVENT_SLID, {\n relatedTarget: nextElement,\n direction: eventDirectionName,\n from: activeElementIndex,\n to: nextElementIndex\n })\n }\n\n if (isCycling) {\n this.cycle()\n }\n }\n\n _directionToOrder(direction) {\n if (![DIRECTION_RIGHT, DIRECTION_LEFT].includes(direction)) {\n return direction\n }\n\n if (isRTL()) {\n return direction === DIRECTION_RIGHT ? ORDER_PREV : ORDER_NEXT\n }\n\n return direction === DIRECTION_RIGHT ? ORDER_NEXT : ORDER_PREV\n }\n\n _orderToDirection(order) {\n if (![ORDER_NEXT, ORDER_PREV].includes(order)) {\n return order\n }\n\n if (isRTL()) {\n return order === ORDER_NEXT ? DIRECTION_LEFT : DIRECTION_RIGHT\n }\n\n return order === ORDER_NEXT ? DIRECTION_RIGHT : DIRECTION_LEFT\n }\n\n // Static\n\n static carouselInterface(element, config) {\n let data = Data.get(element, DATA_KEY)\n let _config = {\n ...Default,\n ...Manipulator.getDataAttributes(element)\n }\n\n if (typeof config === 'object') {\n _config = {\n ..._config,\n ...config\n }\n }\n\n const action = typeof config === 'string' ? config : _config.slide\n\n if (!data) {\n data = new Carousel(element, _config)\n }\n\n if (typeof config === 'number') {\n data.to(config)\n } else if (typeof action === 'string') {\n if (typeof data[action] === 'undefined') {\n throw new TypeError(`No method named \"${action}\"`)\n }\n\n data[action]()\n } else if (_config.interval && _config.ride) {\n data.pause()\n data.cycle()\n }\n }\n\n static jQueryInterface(config) {\n return this.each(function () {\n Carousel.carouselInterface(this, config)\n })\n }\n\n static dataApiClickHandler(event) {\n const target = getElementFromSelector(this)\n\n if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {\n return\n }\n\n const config = {\n ...Manipulator.getDataAttributes(target),\n ...Manipulator.getDataAttributes(this)\n }\n const slideIndex = this.getAttribute('data-bs-slide-to')\n\n if (slideIndex) {\n config.interval = false\n }\n\n Carousel.carouselInterface(target, config)\n\n if (slideIndex) {\n Data.get(target, DATA_KEY).to(slideIndex)\n }\n\n event.preventDefault()\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, Carousel.dataApiClickHandler)\n\nEventHandler.on(window, EVENT_LOAD_DATA_API, () => {\n const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE)\n\n for (let i = 0, len = carousels.length; i < len; i++) {\n Carousel.carouselInterface(carousels[i], Data.get(carousels[i], DATA_KEY))\n }\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n * add .Carousel to jQuery only if jQuery is present\n */\n\ndefineJQueryPlugin(NAME, Carousel)\n\nexport default Carousel\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.0.0-beta3): collapse.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport {\n defineJQueryPlugin,\n emulateTransitionEnd,\n getSelectorFromElement,\n getElementFromSelector,\n getTransitionDurationFromElement,\n isElement,\n reflow,\n typeCheckConfig\n} from './util/index'\nimport Data from './dom/data'\nimport EventHandler from './dom/event-handler'\nimport Manipulator from './dom/manipulator'\nimport SelectorEngine from './dom/selector-engine'\nimport BaseComponent from './base-component'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'collapse'\nconst DATA_KEY = 'bs.collapse'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst Default = {\n toggle: true,\n parent: ''\n}\n\nconst DefaultType = {\n toggle: 'boolean',\n parent: '(string|element)'\n}\n\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_COLLAPSE = 'collapse'\nconst CLASS_NAME_COLLAPSING = 'collapsing'\nconst CLASS_NAME_COLLAPSED = 'collapsed'\n\nconst WIDTH = 'width'\nconst HEIGHT = 'height'\n\nconst SELECTOR_ACTIVES = '.show, .collapsing'\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"collapse\"]'\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Collapse extends BaseComponent {\n constructor(element, config) {\n super(element)\n\n this._isTransitioning = false\n this._config = this._getConfig(config)\n this._triggerArray = SelectorEngine.find(\n `${SELECTOR_DATA_TOGGLE}[href=\"#${this._element.id}\"],` +\n `${SELECTOR_DATA_TOGGLE}[data-bs-target=\"#${this._element.id}\"]`\n )\n\n const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE)\n\n for (let i = 0, len = toggleList.length; i < len; i++) {\n const elem = toggleList[i]\n const selector = getSelectorFromElement(elem)\n const filterElement = SelectorEngine.find(selector)\n .filter(foundElem => foundElem === this._element)\n\n if (selector !== null && filterElement.length) {\n this._selector = selector\n this._triggerArray.push(elem)\n }\n }\n\n this._parent = this._config.parent ? this._getParent() : null\n\n if (!this._config.parent) {\n this._addAriaAndCollapsedClass(this._element, this._triggerArray)\n }\n\n if (this._config.toggle) {\n this.toggle()\n }\n }\n\n // Getters\n\n static get Default() {\n return Default\n }\n\n static get DATA_KEY() {\n return DATA_KEY\n }\n\n // Public\n\n toggle() {\n if (this._element.classList.contains(CLASS_NAME_SHOW)) {\n this.hide()\n } else {\n this.show()\n }\n }\n\n show() {\n if (this._isTransitioning || this._element.classList.contains(CLASS_NAME_SHOW)) {\n return\n }\n\n let actives\n let activesData\n\n if (this._parent) {\n actives = SelectorEngine.find(SELECTOR_ACTIVES, this._parent)\n .filter(elem => {\n if (typeof this._config.parent === 'string') {\n return elem.getAttribute('data-bs-parent') === this._config.parent\n }\n\n return elem.classList.contains(CLASS_NAME_COLLAPSE)\n })\n\n if (actives.length === 0) {\n actives = null\n }\n }\n\n const container = SelectorEngine.findOne(this._selector)\n if (actives) {\n const tempActiveData = actives.find(elem => container !== elem)\n activesData = tempActiveData ? Data.get(tempActiveData, DATA_KEY) : null\n\n if (activesData && activesData._isTransitioning) {\n return\n }\n }\n\n const startEvent = EventHandler.trigger(this._element, EVENT_SHOW)\n if (startEvent.defaultPrevented) {\n return\n }\n\n if (actives) {\n actives.forEach(elemActive => {\n if (container !== elemActive) {\n Collapse.collapseInterface(elemActive, 'hide')\n }\n\n if (!activesData) {\n Data.set(elemActive, DATA_KEY, null)\n }\n })\n }\n\n const dimension = this._getDimension()\n\n this._element.classList.remove(CLASS_NAME_COLLAPSE)\n this._element.classList.add(CLASS_NAME_COLLAPSING)\n\n this._element.style[dimension] = 0\n\n if (this._triggerArray.length) {\n this._triggerArray.forEach(element => {\n element.classList.remove(CLASS_NAME_COLLAPSED)\n element.setAttribute('aria-expanded', true)\n })\n }\n\n this.setTransitioning(true)\n\n const complete = () => {\n this._element.classList.remove(CLASS_NAME_COLLAPSING)\n this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW)\n\n this._element.style[dimension] = ''\n\n this.setTransitioning(false)\n\n EventHandler.trigger(this._element, EVENT_SHOWN)\n }\n\n const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1)\n const scrollSize = `scroll${capitalizedDimension}`\n const transitionDuration = getTransitionDurationFromElement(this._element)\n\n EventHandler.one(this._element, 'transitionend', complete)\n\n emulateTransitionEnd(this._element, transitionDuration)\n this._element.style[dimension] = `${this._element[scrollSize]}px`\n }\n\n hide() {\n if (this._isTransitioning || !this._element.classList.contains(CLASS_NAME_SHOW)) {\n return\n }\n\n const startEvent = EventHandler.trigger(this._element, EVENT_HIDE)\n if (startEvent.defaultPrevented) {\n return\n }\n\n const dimension = this._getDimension()\n\n this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`\n\n reflow(this._element)\n\n this._element.classList.add(CLASS_NAME_COLLAPSING)\n this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW)\n\n const triggerArrayLength = this._triggerArray.length\n if (triggerArrayLength > 0) {\n for (let i = 0; i < triggerArrayLength; i++) {\n const trigger = this._triggerArray[i]\n const elem = getElementFromSelector(trigger)\n\n if (elem && !elem.classList.contains(CLASS_NAME_SHOW)) {\n trigger.classList.add(CLASS_NAME_COLLAPSED)\n trigger.setAttribute('aria-expanded', false)\n }\n }\n }\n\n this.setTransitioning(true)\n\n const complete = () => {\n this.setTransitioning(false)\n this._element.classList.remove(CLASS_NAME_COLLAPSING)\n this._element.classList.add(CLASS_NAME_COLLAPSE)\n EventHandler.trigger(this._element, EVENT_HIDDEN)\n }\n\n this._element.style[dimension] = ''\n const transitionDuration = getTransitionDurationFromElement(this._element)\n\n EventHandler.one(this._element, 'transitionend', complete)\n emulateTransitionEnd(this._element, transitionDuration)\n }\n\n setTransitioning(isTransitioning) {\n this._isTransitioning = isTransitioning\n }\n\n dispose() {\n super.dispose()\n this._config = null\n this._parent = null\n this._triggerArray = null\n this._isTransitioning = null\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n config.toggle = Boolean(config.toggle) // Coerce string values\n typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _getDimension() {\n return this._element.classList.contains(WIDTH) ? WIDTH : HEIGHT\n }\n\n _getParent() {\n let { parent } = this._config\n\n if (isElement(parent)) {\n // it's a jQuery object\n if (typeof parent.jquery !== 'undefined' || typeof parent[0] !== 'undefined') {\n parent = parent[0]\n }\n } else {\n parent = SelectorEngine.findOne(parent)\n }\n\n const selector = `${SELECTOR_DATA_TOGGLE}[data-bs-parent=\"${parent}\"]`\n\n SelectorEngine.find(selector, parent)\n .forEach(element => {\n const selected = getElementFromSelector(element)\n\n this._addAriaAndCollapsedClass(\n selected,\n [element]\n )\n })\n\n return parent\n }\n\n _addAriaAndCollapsedClass(element, triggerArray) {\n if (!element || !triggerArray.length) {\n return\n }\n\n const isOpen = element.classList.contains(CLASS_NAME_SHOW)\n\n triggerArray.forEach(elem => {\n if (isOpen) {\n elem.classList.remove(CLASS_NAME_COLLAPSED)\n } else {\n elem.classList.add(CLASS_NAME_COLLAPSED)\n }\n\n elem.setAttribute('aria-expanded', isOpen)\n })\n }\n\n // Static\n\n static collapseInterface(element, config) {\n let data = Data.get(element, DATA_KEY)\n const _config = {\n ...Default,\n ...Manipulator.getDataAttributes(element),\n ...(typeof config === 'object' && config ? config : {})\n }\n\n if (!data && _config.toggle && typeof config === 'string' && /show|hide/.test(config)) {\n _config.toggle = false\n }\n\n if (!data) {\n data = new Collapse(element, _config)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config]()\n }\n }\n\n static jQueryInterface(config) {\n return this.each(function () {\n Collapse.collapseInterface(this, config)\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n // preventDefault only for elements (which change the URL) not inside the collapsible element\n if (event.target.tagName === 'A' || (event.delegateTarget && event.delegateTarget.tagName === 'A')) {\n event.preventDefault()\n }\n\n const triggerData = Manipulator.getDataAttributes(this)\n const selector = getSelectorFromElement(this)\n const selectorElements = SelectorEngine.find(selector)\n\n selectorElements.forEach(element => {\n const data = Data.get(element, DATA_KEY)\n let config\n if (data) {\n // update parent attribute\n if (data._parent === null && typeof triggerData.parent === 'string') {\n data._config.parent = triggerData.parent\n data._parent = data._getParent()\n }\n\n config = 'toggle'\n } else {\n config = triggerData\n }\n\n Collapse.collapseInterface(element, config)\n })\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n * add .Collapse to jQuery only if jQuery is present\n */\n\ndefineJQueryPlugin(NAME, Collapse)\n\nexport default Collapse\n","export var top = 'top';\nexport var bottom = 'bottom';\nexport var right = 'right';\nexport var left = 'left';\nexport var auto = 'auto';\nexport var basePlacements = [top, bottom, right, left];\nexport var start = 'start';\nexport var end = 'end';\nexport var clippingParents = 'clippingParents';\nexport var viewport = 'viewport';\nexport var popper = 'popper';\nexport var reference = 'reference';\nexport var variationPlacements = /*#__PURE__*/basePlacements.reduce(function (acc, placement) {\n return acc.concat([placement + \"-\" + start, placement + \"-\" + end]);\n}, []);\nexport var placements = /*#__PURE__*/[].concat(basePlacements, [auto]).reduce(function (acc, placement) {\n return acc.concat([placement, placement + \"-\" + start, placement + \"-\" + end]);\n}, []); // modifiers that need to read the DOM\n\nexport var beforeRead = 'beforeRead';\nexport var read = 'read';\nexport var afterRead = 'afterRead'; // pure-logic modifiers\n\nexport var beforeMain = 'beforeMain';\nexport var main = 'main';\nexport var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state)\n\nexport var beforeWrite = 'beforeWrite';\nexport var write = 'write';\nexport var afterWrite = 'afterWrite';\nexport var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite];","export default function getNodeName(element) {\n return element ? (element.nodeName || '').toLowerCase() : null;\n}","export default function getWindow(node) {\n if (node == null) {\n return window;\n }\n\n if (node.toString() !== '[object Window]') {\n var ownerDocument = node.ownerDocument;\n return ownerDocument ? ownerDocument.defaultView || window : window;\n }\n\n return node;\n}","import getWindow from \"./getWindow.js\";\n\nfunction isElement(node) {\n var OwnElement = getWindow(node).Element;\n return node instanceof OwnElement || node instanceof Element;\n}\n\nfunction isHTMLElement(node) {\n var OwnElement = getWindow(node).HTMLElement;\n return node instanceof OwnElement || node instanceof HTMLElement;\n}\n\nfunction isShadowRoot(node) {\n // IE 11 has no ShadowRoot\n if (typeof ShadowRoot === 'undefined') {\n return false;\n }\n\n var OwnElement = getWindow(node).ShadowRoot;\n return node instanceof OwnElement || node instanceof ShadowRoot;\n}\n\nexport { isElement, isHTMLElement, isShadowRoot };","import getNodeName from \"../dom-utils/getNodeName.js\";\nimport { isHTMLElement } from \"../dom-utils/instanceOf.js\"; // This modifier takes the styles prepared by the `computeStyles` modifier\n// and applies them to the HTMLElements such as popper and arrow\n\nfunction applyStyles(_ref) {\n var state = _ref.state;\n Object.keys(state.elements).forEach(function (name) {\n var style = state.styles[name] || {};\n var attributes = state.attributes[name] || {};\n var element = state.elements[name]; // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n } // Flow doesn't support to extend this property, but it's the most\n // effective way to apply styles to an HTMLElement\n // $FlowFixMe[cannot-write]\n\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (name) {\n var value = attributes[name];\n\n if (value === false) {\n element.removeAttribute(name);\n } else {\n element.setAttribute(name, value === true ? '' : value);\n }\n });\n });\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state;\n var initialStyles = {\n popper: {\n position: state.options.strategy,\n left: '0',\n top: '0',\n margin: '0'\n },\n arrow: {\n position: 'absolute'\n },\n reference: {}\n };\n Object.assign(state.elements.popper.style, initialStyles.popper);\n state.styles = initialStyles;\n\n if (state.elements.arrow) {\n Object.assign(state.elements.arrow.style, initialStyles.arrow);\n }\n\n return function () {\n Object.keys(state.elements).forEach(function (name) {\n var element = state.elements[name];\n var attributes = state.attributes[name] || {};\n var styleProperties = Object.keys(state.styles.hasOwnProperty(name) ? state.styles[name] : initialStyles[name]); // Set all values to an empty string to unset them\n\n var style = styleProperties.reduce(function (style, property) {\n style[property] = '';\n return style;\n }, {}); // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n }\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (attribute) {\n element.removeAttribute(attribute);\n });\n });\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'applyStyles',\n enabled: true,\n phase: 'write',\n fn: applyStyles,\n effect: effect,\n requires: ['computeStyles']\n};","import { auto } from \"../enums.js\";\nexport default function getBasePlacement(placement) {\n return placement.split('-')[0];\n}","export default function getBoundingClientRect(element) {\n var rect = element.getBoundingClientRect();\n return {\n width: rect.width,\n height: rect.height,\n top: rect.top,\n right: rect.right,\n bottom: rect.bottom,\n left: rect.left,\n x: rect.left,\n y: rect.top\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\"; // Returns the layout rect of an element relative to its offsetParent. Layout\n// means it doesn't take into account transforms.\n\nexport default function getLayoutRect(element) {\n var clientRect = getBoundingClientRect(element); // Use the clientRect sizes if it's not been transformed.\n // Fixes https://github.com/popperjs/popper-core/issues/1223\n\n var width = element.offsetWidth;\n var height = element.offsetHeight;\n\n if (Math.abs(clientRect.width - width) <= 1) {\n width = clientRect.width;\n }\n\n if (Math.abs(clientRect.height - height) <= 1) {\n height = clientRect.height;\n }\n\n return {\n x: element.offsetLeft,\n y: element.offsetTop,\n width: width,\n height: height\n };\n}","import { isShadowRoot } from \"./instanceOf.js\";\nexport default function contains(parent, child) {\n var rootNode = child.getRootNode && child.getRootNode(); // First, attempt with faster native method\n\n if (parent.contains(child)) {\n return true;\n } // then fallback to custom implementation with Shadow DOM support\n else if (rootNode && isShadowRoot(rootNode)) {\n var next = child;\n\n do {\n if (next && parent.isSameNode(next)) {\n return true;\n } // $FlowFixMe[prop-missing]: need a better way to handle this...\n\n\n next = next.parentNode || next.host;\n } while (next);\n } // Give up, the result is false\n\n\n return false;\n}","import getWindow from \"./getWindow.js\";\nexport default function getComputedStyle(element) {\n return getWindow(element).getComputedStyle(element);\n}","import getNodeName from \"./getNodeName.js\";\nexport default function isTableElement(element) {\n return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0;\n}","import { isElement } from \"./instanceOf.js\";\nexport default function getDocumentElement(element) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return ((isElement(element) ? element.ownerDocument : // $FlowFixMe[prop-missing]\n element.document) || window.document).documentElement;\n}","import getNodeName from \"./getNodeName.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport { isShadowRoot } from \"./instanceOf.js\";\nexport default function getParentNode(element) {\n if (getNodeName(element) === 'html') {\n return element;\n }\n\n return (// this is a quicker (but less type safe) way to save quite some bytes from the bundle\n // $FlowFixMe[incompatible-return]\n // $FlowFixMe[prop-missing]\n element.assignedSlot || // step into the shadow DOM of the parent of a slotted node\n element.parentNode || ( // DOM Element detected\n isShadowRoot(element) ? element.host : null) || // ShadowRoot detected\n // $FlowFixMe[incompatible-call]: HTMLElement is a Node\n getDocumentElement(element) // fallback\n\n );\n}","import getWindow from \"./getWindow.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport isTableElement from \"./isTableElement.js\";\nimport getParentNode from \"./getParentNode.js\";\n\nfunction getTrueOffsetParent(element) {\n if (!isHTMLElement(element) || // https://github.com/popperjs/popper-core/issues/837\n getComputedStyle(element).position === 'fixed') {\n return null;\n }\n\n return element.offsetParent;\n} // `.offsetParent` reports `null` for fixed elements, while absolute elements\n// return the containing block\n\n\nfunction getContainingBlock(element) {\n var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') !== -1;\n var currentNode = getParentNode(element);\n\n while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) {\n var css = getComputedStyle(currentNode); // This is non-exhaustive but covers the most common CSS properties that\n // create a containing block.\n // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\n\n if (css.transform !== 'none' || css.perspective !== 'none' || css.contain === 'paint' || ['transform', 'perspective'].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === 'filter' || isFirefox && css.filter && css.filter !== 'none') {\n return currentNode;\n } else {\n currentNode = currentNode.parentNode;\n }\n }\n\n return null;\n} // Gets the closest ancestor positioned element. Handles some edge cases,\n// such as table ancestors and cross browser bugs.\n\n\nexport default function getOffsetParent(element) {\n var window = getWindow(element);\n var offsetParent = getTrueOffsetParent(element);\n\n while (offsetParent && isTableElement(offsetParent) && getComputedStyle(offsetParent).position === 'static') {\n offsetParent = getTrueOffsetParent(offsetParent);\n }\n\n if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle(offsetParent).position === 'static')) {\n return window;\n }\n\n return offsetParent || getContainingBlock(element) || window;\n}","export default function getMainAxisFromPlacement(placement) {\n return ['top', 'bottom'].indexOf(placement) >= 0 ? 'x' : 'y';\n}","export var max = Math.max;\nexport var min = Math.min;\nexport var round = Math.round;","import { max as mathMax, min as mathMin } from \"./math.js\";\nexport default function within(min, value, max) {\n return mathMax(min, mathMin(value, max));\n}","import getFreshSideObject from \"./getFreshSideObject.js\";\nexport default function mergePaddingObject(paddingObject) {\n return Object.assign({}, getFreshSideObject(), paddingObject);\n}","export default function getFreshSideObject() {\n return {\n top: 0,\n right: 0,\n bottom: 0,\n left: 0\n };\n}","export default function expandToHashMap(value, keys) {\n return keys.reduce(function (hashMap, key) {\n hashMap[key] = value;\n return hashMap;\n }, {});\n}","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport contains from \"../dom-utils/contains.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport within from \"../utils/within.js\";\nimport mergePaddingObject from \"../utils/mergePaddingObject.js\";\nimport expandToHashMap from \"../utils/expandToHashMap.js\";\nimport { left, right, basePlacements, top, bottom } from \"../enums.js\";\nimport { isHTMLElement } from \"../dom-utils/instanceOf.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar toPaddingObject = function toPaddingObject(padding, state) {\n padding = typeof padding === 'function' ? padding(Object.assign({}, state.rects, {\n placement: state.placement\n })) : padding;\n return mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n};\n\nfunction arrow(_ref) {\n var _state$modifiersData$;\n\n var state = _ref.state,\n name = _ref.name,\n options = _ref.options;\n var arrowElement = state.elements.arrow;\n var popperOffsets = state.modifiersData.popperOffsets;\n var basePlacement = getBasePlacement(state.placement);\n var axis = getMainAxisFromPlacement(basePlacement);\n var isVertical = [left, right].indexOf(basePlacement) >= 0;\n var len = isVertical ? 'height' : 'width';\n\n if (!arrowElement || !popperOffsets) {\n return;\n }\n\n var paddingObject = toPaddingObject(options.padding, state);\n var arrowRect = getLayoutRect(arrowElement);\n var minProp = axis === 'y' ? top : left;\n var maxProp = axis === 'y' ? bottom : right;\n var endDiff = state.rects.reference[len] + state.rects.reference[axis] - popperOffsets[axis] - state.rects.popper[len];\n var startDiff = popperOffsets[axis] - state.rects.reference[axis];\n var arrowOffsetParent = getOffsetParent(arrowElement);\n var clientSize = arrowOffsetParent ? axis === 'y' ? arrowOffsetParent.clientHeight || 0 : arrowOffsetParent.clientWidth || 0 : 0;\n var centerToReference = endDiff / 2 - startDiff / 2; // Make sure the arrow doesn't overflow the popper if the center point is\n // outside of the popper bounds\n\n var min = paddingObject[minProp];\n var max = clientSize - arrowRect[len] - paddingObject[maxProp];\n var center = clientSize / 2 - arrowRect[len] / 2 + centerToReference;\n var offset = within(min, center, max); // Prevents breaking syntax highlighting...\n\n var axisProp = axis;\n state.modifiersData[name] = (_state$modifiersData$ = {}, _state$modifiersData$[axisProp] = offset, _state$modifiersData$.centerOffset = offset - center, _state$modifiersData$);\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state,\n options = _ref2.options;\n var _options$element = options.element,\n arrowElement = _options$element === void 0 ? '[data-popper-arrow]' : _options$element;\n\n if (arrowElement == null) {\n return;\n } // CSS selector\n\n\n if (typeof arrowElement === 'string') {\n arrowElement = state.elements.popper.querySelector(arrowElement);\n\n if (!arrowElement) {\n return;\n }\n }\n\n if (process.env.NODE_ENV !== \"production\") {\n if (!isHTMLElement(arrowElement)) {\n console.error(['Popper: \"arrow\" element must be an HTMLElement (not an SVGElement).', 'To use an SVG arrow, wrap it in an HTMLElement that will be used as', 'the arrow.'].join(' '));\n }\n }\n\n if (!contains(state.elements.popper, arrowElement)) {\n if (process.env.NODE_ENV !== \"production\") {\n console.error(['Popper: \"arrow\" modifier\\'s `element` must be a child of the popper', 'element.'].join(' '));\n }\n\n return;\n }\n\n state.elements.arrow = arrowElement;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'arrow',\n enabled: true,\n phase: 'main',\n fn: arrow,\n effect: effect,\n requires: ['popperOffsets'],\n requiresIfExists: ['preventOverflow']\n};","import { top, left, right, bottom } from \"../enums.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getWindow from \"../dom-utils/getWindow.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getComputedStyle from \"../dom-utils/getComputedStyle.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport { round } from \"../utils/math.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar unsetSides = {\n top: 'auto',\n right: 'auto',\n bottom: 'auto',\n left: 'auto'\n}; // Round the offsets to the nearest suitable subpixel based on the DPR.\n// Zooming can change the DPR, but it seems to report a value that will\n// cleanly divide the values into the appropriate subpixels.\n\nfunction roundOffsetsByDPR(_ref) {\n var x = _ref.x,\n y = _ref.y;\n var win = window;\n var dpr = win.devicePixelRatio || 1;\n return {\n x: round(round(x * dpr) / dpr) || 0,\n y: round(round(y * dpr) / dpr) || 0\n };\n}\n\nexport function mapToStyles(_ref2) {\n var _Object$assign2;\n\n var popper = _ref2.popper,\n popperRect = _ref2.popperRect,\n placement = _ref2.placement,\n offsets = _ref2.offsets,\n position = _ref2.position,\n gpuAcceleration = _ref2.gpuAcceleration,\n adaptive = _ref2.adaptive,\n roundOffsets = _ref2.roundOffsets;\n\n var _ref3 = roundOffsets === true ? roundOffsetsByDPR(offsets) : typeof roundOffsets === 'function' ? roundOffsets(offsets) : offsets,\n _ref3$x = _ref3.x,\n x = _ref3$x === void 0 ? 0 : _ref3$x,\n _ref3$y = _ref3.y,\n y = _ref3$y === void 0 ? 0 : _ref3$y;\n\n var hasX = offsets.hasOwnProperty('x');\n var hasY = offsets.hasOwnProperty('y');\n var sideX = left;\n var sideY = top;\n var win = window;\n\n if (adaptive) {\n var offsetParent = getOffsetParent(popper);\n var heightProp = 'clientHeight';\n var widthProp = 'clientWidth';\n\n if (offsetParent === getWindow(popper)) {\n offsetParent = getDocumentElement(popper);\n\n if (getComputedStyle(offsetParent).position !== 'static') {\n heightProp = 'scrollHeight';\n widthProp = 'scrollWidth';\n }\n } // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it\n\n\n offsetParent = offsetParent;\n\n if (placement === top) {\n sideY = bottom; // $FlowFixMe[prop-missing]\n\n y -= offsetParent[heightProp] - popperRect.height;\n y *= gpuAcceleration ? 1 : -1;\n }\n\n if (placement === left) {\n sideX = right; // $FlowFixMe[prop-missing]\n\n x -= offsetParent[widthProp] - popperRect.width;\n x *= gpuAcceleration ? 1 : -1;\n }\n }\n\n var commonStyles = Object.assign({\n position: position\n }, adaptive && unsetSides);\n\n if (gpuAcceleration) {\n var _Object$assign;\n\n return Object.assign({}, commonStyles, (_Object$assign = {}, _Object$assign[sideY] = hasY ? '0' : '', _Object$assign[sideX] = hasX ? '0' : '', _Object$assign.transform = (win.devicePixelRatio || 1) < 2 ? \"translate(\" + x + \"px, \" + y + \"px)\" : \"translate3d(\" + x + \"px, \" + y + \"px, 0)\", _Object$assign));\n }\n\n return Object.assign({}, commonStyles, (_Object$assign2 = {}, _Object$assign2[sideY] = hasY ? y + \"px\" : '', _Object$assign2[sideX] = hasX ? x + \"px\" : '', _Object$assign2.transform = '', _Object$assign2));\n}\n\nfunction computeStyles(_ref4) {\n var state = _ref4.state,\n options = _ref4.options;\n var _options$gpuAccelerat = options.gpuAcceleration,\n gpuAcceleration = _options$gpuAccelerat === void 0 ? true : _options$gpuAccelerat,\n _options$adaptive = options.adaptive,\n adaptive = _options$adaptive === void 0 ? true : _options$adaptive,\n _options$roundOffsets = options.roundOffsets,\n roundOffsets = _options$roundOffsets === void 0 ? true : _options$roundOffsets;\n\n if (process.env.NODE_ENV !== \"production\") {\n var transitionProperty = getComputedStyle(state.elements.popper).transitionProperty || '';\n\n if (adaptive && ['transform', 'top', 'right', 'bottom', 'left'].some(function (property) {\n return transitionProperty.indexOf(property) >= 0;\n })) {\n console.warn(['Popper: Detected CSS transitions on at least one of the following', 'CSS properties: \"transform\", \"top\", \"right\", \"bottom\", \"left\".', '\\n\\n', 'Disable the \"computeStyles\" modifier\\'s `adaptive` option to allow', 'for smooth transitions, or remove these properties from the CSS', 'transition declaration on the popper element if only transitioning', 'opacity or background-color for example.', '\\n\\n', 'We recommend using the popper element as a wrapper around an inner', 'element that can have any CSS property transitioned for animations.'].join(' '));\n }\n }\n\n var commonStyles = {\n placement: getBasePlacement(state.placement),\n popper: state.elements.popper,\n popperRect: state.rects.popper,\n gpuAcceleration: gpuAcceleration\n };\n\n if (state.modifiersData.popperOffsets != null) {\n state.styles.popper = Object.assign({}, state.styles.popper, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.popperOffsets,\n position: state.options.strategy,\n adaptive: adaptive,\n roundOffsets: roundOffsets\n })));\n }\n\n if (state.modifiersData.arrow != null) {\n state.styles.arrow = Object.assign({}, state.styles.arrow, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.arrow,\n position: 'absolute',\n adaptive: false,\n roundOffsets: roundOffsets\n })));\n }\n\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-placement': state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'computeStyles',\n enabled: true,\n phase: 'beforeWrite',\n fn: computeStyles,\n data: {}\n};","import getWindow from \"../dom-utils/getWindow.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar passive = {\n passive: true\n};\n\nfunction effect(_ref) {\n var state = _ref.state,\n instance = _ref.instance,\n options = _ref.options;\n var _options$scroll = options.scroll,\n scroll = _options$scroll === void 0 ? true : _options$scroll,\n _options$resize = options.resize,\n resize = _options$resize === void 0 ? true : _options$resize;\n var window = getWindow(state.elements.popper);\n var scrollParents = [].concat(state.scrollParents.reference, state.scrollParents.popper);\n\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.addEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.addEventListener('resize', instance.update, passive);\n }\n\n return function () {\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.removeEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.removeEventListener('resize', instance.update, passive);\n }\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'eventListeners',\n enabled: true,\n phase: 'write',\n fn: function fn() {},\n effect: effect,\n data: {}\n};","var hash = {\n left: 'right',\n right: 'left',\n bottom: 'top',\n top: 'bottom'\n};\nexport default function getOppositePlacement(placement) {\n return placement.replace(/left|right|bottom|top/g, function (matched) {\n return hash[matched];\n });\n}","var hash = {\n start: 'end',\n end: 'start'\n};\nexport default function getOppositeVariationPlacement(placement) {\n return placement.replace(/start|end/g, function (matched) {\n return hash[matched];\n });\n}","import getWindow from \"./getWindow.js\";\nexport default function getWindowScroll(node) {\n var win = getWindow(node);\n var scrollLeft = win.pageXOffset;\n var scrollTop = win.pageYOffset;\n return {\n scrollLeft: scrollLeft,\n scrollTop: scrollTop\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nexport default function getWindowScrollBarX(element) {\n // If has a CSS width greater than the viewport, then this will be\n // incorrect for RTL.\n // Popper 1 is broken in this case and never had a bug report so let's assume\n // it's not an issue. I don't think anyone ever specifies width on \n // anyway.\n // Browsers where the left scrollbar doesn't cause an issue report `0` for\n // this (e.g. Edge 2019, IE11, Safari)\n return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft;\n}","import getComputedStyle from \"./getComputedStyle.js\";\nexport default function isScrollParent(element) {\n // Firefox wants us to check `-x` and `-y` variations as well\n var _getComputedStyle = getComputedStyle(element),\n overflow = _getComputedStyle.overflow,\n overflowX = _getComputedStyle.overflowX,\n overflowY = _getComputedStyle.overflowY;\n\n return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX);\n}","import getScrollParent from \"./getScrollParent.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getWindow from \"./getWindow.js\";\nimport isScrollParent from \"./isScrollParent.js\";\n/*\ngiven a DOM element, return the list of all scroll parents, up the list of ancesors\nuntil we get to the top window object. This list is what we attach scroll listeners\nto, because if any of these parent elements scroll, we'll need to re-calculate the\nreference element's position.\n*/\n\nexport default function listScrollParents(element, list) {\n var _element$ownerDocumen;\n\n if (list === void 0) {\n list = [];\n }\n\n var scrollParent = getScrollParent(element);\n var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body);\n var win = getWindow(scrollParent);\n var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent;\n var updatedList = list.concat(target);\n return isBody ? updatedList : // $FlowFixMe[incompatible-call]: isBody tells us target will be an HTMLElement here\n updatedList.concat(listScrollParents(getParentNode(target)));\n}","import getParentNode from \"./getParentNode.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nexport default function getScrollParent(node) {\n if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return node.ownerDocument.body;\n }\n\n if (isHTMLElement(node) && isScrollParent(node)) {\n return node;\n }\n\n return getScrollParent(getParentNode(node));\n}","export default function rectToClientRect(rect) {\n return Object.assign({}, rect, {\n left: rect.x,\n top: rect.y,\n right: rect.x + rect.width,\n bottom: rect.y + rect.height\n });\n}","import { viewport } from \"../enums.js\";\nimport getViewportRect from \"./getViewportRect.js\";\nimport getDocumentRect from \"./getDocumentRect.js\";\nimport listScrollParents from \"./listScrollParents.js\";\nimport getOffsetParent from \"./getOffsetParent.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport contains from \"./contains.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport rectToClientRect from \"../utils/rectToClientRect.js\";\nimport { max, min } from \"../utils/math.js\";\n\nfunction getInnerBoundingClientRect(element) {\n var rect = getBoundingClientRect(element);\n rect.top = rect.top + element.clientTop;\n rect.left = rect.left + element.clientLeft;\n rect.bottom = rect.top + element.clientHeight;\n rect.right = rect.left + element.clientWidth;\n rect.width = element.clientWidth;\n rect.height = element.clientHeight;\n rect.x = rect.left;\n rect.y = rect.top;\n return rect;\n}\n\nfunction getClientRectFromMixedType(element, clippingParent) {\n return clippingParent === viewport ? rectToClientRect(getViewportRect(element)) : isHTMLElement(clippingParent) ? getInnerBoundingClientRect(clippingParent) : rectToClientRect(getDocumentRect(getDocumentElement(element)));\n} // A \"clipping parent\" is an overflowable container with the characteristic of\n// clipping (or hiding) overflowing elements with a position different from\n// `initial`\n\n\nfunction getClippingParents(element) {\n var clippingParents = listScrollParents(getParentNode(element));\n var canEscapeClipping = ['absolute', 'fixed'].indexOf(getComputedStyle(element).position) >= 0;\n var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element;\n\n if (!isElement(clipperElement)) {\n return [];\n } // $FlowFixMe[incompatible-return]: https://github.com/facebook/flow/issues/1414\n\n\n return clippingParents.filter(function (clippingParent) {\n return isElement(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== 'body';\n });\n} // Gets the maximum area that the element is visible in due to any number of\n// clipping parents\n\n\nexport default function getClippingRect(element, boundary, rootBoundary) {\n var mainClippingParents = boundary === 'clippingParents' ? getClippingParents(element) : [].concat(boundary);\n var clippingParents = [].concat(mainClippingParents, [rootBoundary]);\n var firstClippingParent = clippingParents[0];\n var clippingRect = clippingParents.reduce(function (accRect, clippingParent) {\n var rect = getClientRectFromMixedType(element, clippingParent);\n accRect.top = max(rect.top, accRect.top);\n accRect.right = min(rect.right, accRect.right);\n accRect.bottom = min(rect.bottom, accRect.bottom);\n accRect.left = max(rect.left, accRect.left);\n return accRect;\n }, getClientRectFromMixedType(element, firstClippingParent));\n clippingRect.width = clippingRect.right - clippingRect.left;\n clippingRect.height = clippingRect.bottom - clippingRect.top;\n clippingRect.x = clippingRect.left;\n clippingRect.y = clippingRect.top;\n return clippingRect;\n}","import getWindow from \"./getWindow.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nexport default function getViewportRect(element) {\n var win = getWindow(element);\n var html = getDocumentElement(element);\n var visualViewport = win.visualViewport;\n var width = html.clientWidth;\n var height = html.clientHeight;\n var x = 0;\n var y = 0; // NB: This isn't supported on iOS <= 12. If the keyboard is open, the popper\n // can be obscured underneath it.\n // Also, `html.clientHeight` adds the bottom bar height in Safari iOS, even\n // if it isn't open, so if this isn't available, the popper will be detected\n // to overflow the bottom of the screen too early.\n\n if (visualViewport) {\n width = visualViewport.width;\n height = visualViewport.height; // Uses Layout Viewport (like Chrome; Safari does not currently)\n // In Chrome, it returns a value very close to 0 (+/-) but contains rounding\n // errors due to floating point numbers, so we need to check precision.\n // Safari returns a number <= 0, usually < -1 when pinch-zoomed\n // Feature detection fails in mobile emulation mode in Chrome.\n // Math.abs(win.innerWidth / visualViewport.scale - visualViewport.width) <\n // 0.001\n // Fallback here: \"Not Safari\" userAgent\n\n if (!/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) {\n x = visualViewport.offsetLeft;\n y = visualViewport.offsetTop;\n }\n }\n\n return {\n width: width,\n height: height,\n x: x + getWindowScrollBarX(element),\n y: y\n };\n}","import getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nimport { max } from \"../utils/math.js\"; // Gets the entire size of the scrollable document area, even extending outside\n// of the `` and `` rect bounds if horizontally scrollable\n\nexport default function getDocumentRect(element) {\n var _element$ownerDocumen;\n\n var html = getDocumentElement(element);\n var winScroll = getWindowScroll(element);\n var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body;\n var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0);\n var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0);\n var x = -winScroll.scrollLeft + getWindowScrollBarX(element);\n var y = -winScroll.scrollTop;\n\n if (getComputedStyle(body || html).direction === 'rtl') {\n x += max(html.clientWidth, body ? body.clientWidth : 0) - width;\n }\n\n return {\n width: width,\n height: height,\n x: x,\n y: y\n };\n}","export default function getVariation(placement) {\n return placement.split('-')[1];\n}","import getBasePlacement from \"./getBasePlacement.js\";\nimport getVariation from \"./getVariation.js\";\nimport getMainAxisFromPlacement from \"./getMainAxisFromPlacement.js\";\nimport { top, right, bottom, left, start, end } from \"../enums.js\";\nexport default function computeOffsets(_ref) {\n var reference = _ref.reference,\n element = _ref.element,\n placement = _ref.placement;\n var basePlacement = placement ? getBasePlacement(placement) : null;\n var variation = placement ? getVariation(placement) : null;\n var commonX = reference.x + reference.width / 2 - element.width / 2;\n var commonY = reference.y + reference.height / 2 - element.height / 2;\n var offsets;\n\n switch (basePlacement) {\n case top:\n offsets = {\n x: commonX,\n y: reference.y - element.height\n };\n break;\n\n case bottom:\n offsets = {\n x: commonX,\n y: reference.y + reference.height\n };\n break;\n\n case right:\n offsets = {\n x: reference.x + reference.width,\n y: commonY\n };\n break;\n\n case left:\n offsets = {\n x: reference.x - element.width,\n y: commonY\n };\n break;\n\n default:\n offsets = {\n x: reference.x,\n y: reference.y\n };\n }\n\n var mainAxis = basePlacement ? getMainAxisFromPlacement(basePlacement) : null;\n\n if (mainAxis != null) {\n var len = mainAxis === 'y' ? 'height' : 'width';\n\n switch (variation) {\n case start:\n offsets[mainAxis] = offsets[mainAxis] - (reference[len] / 2 - element[len] / 2);\n break;\n\n case end:\n offsets[mainAxis] = offsets[mainAxis] + (reference[len] / 2 - element[len] / 2);\n break;\n\n default:\n }\n }\n\n return offsets;\n}","import getBoundingClientRect from \"../dom-utils/getBoundingClientRect.js\";\nimport getClippingRect from \"../dom-utils/getClippingRect.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport computeOffsets from \"./computeOffsets.js\";\nimport rectToClientRect from \"./rectToClientRect.js\";\nimport { clippingParents, reference, popper, bottom, top, right, basePlacements, viewport } from \"../enums.js\";\nimport { isElement } from \"../dom-utils/instanceOf.js\";\nimport mergePaddingObject from \"./mergePaddingObject.js\";\nimport expandToHashMap from \"./expandToHashMap.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport default function detectOverflow(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n _options$placement = _options.placement,\n placement = _options$placement === void 0 ? state.placement : _options$placement,\n _options$boundary = _options.boundary,\n boundary = _options$boundary === void 0 ? clippingParents : _options$boundary,\n _options$rootBoundary = _options.rootBoundary,\n rootBoundary = _options$rootBoundary === void 0 ? viewport : _options$rootBoundary,\n _options$elementConte = _options.elementContext,\n elementContext = _options$elementConte === void 0 ? popper : _options$elementConte,\n _options$altBoundary = _options.altBoundary,\n altBoundary = _options$altBoundary === void 0 ? false : _options$altBoundary,\n _options$padding = _options.padding,\n padding = _options$padding === void 0 ? 0 : _options$padding;\n var paddingObject = mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n var altContext = elementContext === popper ? reference : popper;\n var referenceElement = state.elements.reference;\n var popperRect = state.rects.popper;\n var element = state.elements[altBoundary ? altContext : elementContext];\n var clippingClientRect = getClippingRect(isElement(element) ? element : element.contextElement || getDocumentElement(state.elements.popper), boundary, rootBoundary);\n var referenceClientRect = getBoundingClientRect(referenceElement);\n var popperOffsets = computeOffsets({\n reference: referenceClientRect,\n element: popperRect,\n strategy: 'absolute',\n placement: placement\n });\n var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets));\n var elementClientRect = elementContext === popper ? popperClientRect : referenceClientRect; // positive = overflowing the clipping rect\n // 0 or negative = within the clipping rect\n\n var overflowOffsets = {\n top: clippingClientRect.top - elementClientRect.top + paddingObject.top,\n bottom: elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom,\n left: clippingClientRect.left - elementClientRect.left + paddingObject.left,\n right: elementClientRect.right - clippingClientRect.right + paddingObject.right\n };\n var offsetData = state.modifiersData.offset; // Offsets can be applied only to the popper element\n\n if (elementContext === popper && offsetData) {\n var offset = offsetData[placement];\n Object.keys(overflowOffsets).forEach(function (key) {\n var multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1;\n var axis = [top, bottom].indexOf(key) >= 0 ? 'y' : 'x';\n overflowOffsets[key] += offset[axis] * multiply;\n });\n }\n\n return overflowOffsets;\n}","import getVariation from \"./getVariation.js\";\nimport { variationPlacements, basePlacements, placements as allPlacements } from \"../enums.js\";\nimport detectOverflow from \"./detectOverflow.js\";\nimport getBasePlacement from \"./getBasePlacement.js\";\nexport default function computeAutoPlacement(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n placement = _options.placement,\n boundary = _options.boundary,\n rootBoundary = _options.rootBoundary,\n padding = _options.padding,\n flipVariations = _options.flipVariations,\n _options$allowedAutoP = _options.allowedAutoPlacements,\n allowedAutoPlacements = _options$allowedAutoP === void 0 ? allPlacements : _options$allowedAutoP;\n var variation = getVariation(placement);\n var placements = variation ? flipVariations ? variationPlacements : variationPlacements.filter(function (placement) {\n return getVariation(placement) === variation;\n }) : basePlacements;\n var allowedPlacements = placements.filter(function (placement) {\n return allowedAutoPlacements.indexOf(placement) >= 0;\n });\n\n if (allowedPlacements.length === 0) {\n allowedPlacements = placements;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.error(['Popper: The `allowedAutoPlacements` option did not allow any', 'placements. Ensure the `placement` option matches the variation', 'of the allowed placements.', 'For example, \"auto\" cannot be used to allow \"bottom-start\".', 'Use \"auto-start\" instead.'].join(' '));\n }\n } // $FlowFixMe[incompatible-type]: Flow seems to have problems with two array unions...\n\n\n var overflows = allowedPlacements.reduce(function (acc, placement) {\n acc[placement] = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding\n })[getBasePlacement(placement)];\n return acc;\n }, {});\n return Object.keys(overflows).sort(function (a, b) {\n return overflows[a] - overflows[b];\n });\n}","import getOppositePlacement from \"../utils/getOppositePlacement.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getOppositeVariationPlacement from \"../utils/getOppositeVariationPlacement.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport computeAutoPlacement from \"../utils/computeAutoPlacement.js\";\nimport { bottom, top, start, right, left, auto } from \"../enums.js\";\nimport getVariation from \"../utils/getVariation.js\"; // eslint-disable-next-line import/no-unused-modules\n\nfunction getExpandedFallbackPlacements(placement) {\n if (getBasePlacement(placement) === auto) {\n return [];\n }\n\n var oppositePlacement = getOppositePlacement(placement);\n return [getOppositeVariationPlacement(placement), oppositePlacement, getOppositeVariationPlacement(oppositePlacement)];\n}\n\nfunction flip(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n\n if (state.modifiersData[name]._skip) {\n return;\n }\n\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? true : _options$altAxis,\n specifiedFallbackPlacements = options.fallbackPlacements,\n padding = options.padding,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n _options$flipVariatio = options.flipVariations,\n flipVariations = _options$flipVariatio === void 0 ? true : _options$flipVariatio,\n allowedAutoPlacements = options.allowedAutoPlacements;\n var preferredPlacement = state.options.placement;\n var basePlacement = getBasePlacement(preferredPlacement);\n var isBasePlacement = basePlacement === preferredPlacement;\n var fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipVariations ? [getOppositePlacement(preferredPlacement)] : getExpandedFallbackPlacements(preferredPlacement));\n var placements = [preferredPlacement].concat(fallbackPlacements).reduce(function (acc, placement) {\n return acc.concat(getBasePlacement(placement) === auto ? computeAutoPlacement(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n flipVariations: flipVariations,\n allowedAutoPlacements: allowedAutoPlacements\n }) : placement);\n }, []);\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var checksMap = new Map();\n var makeFallbackChecks = true;\n var firstFittingPlacement = placements[0];\n\n for (var i = 0; i < placements.length; i++) {\n var placement = placements[i];\n\n var _basePlacement = getBasePlacement(placement);\n\n var isStartVariation = getVariation(placement) === start;\n var isVertical = [top, bottom].indexOf(_basePlacement) >= 0;\n var len = isVertical ? 'width' : 'height';\n var overflow = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n altBoundary: altBoundary,\n padding: padding\n });\n var mainVariationSide = isVertical ? isStartVariation ? right : left : isStartVariation ? bottom : top;\n\n if (referenceRect[len] > popperRect[len]) {\n mainVariationSide = getOppositePlacement(mainVariationSide);\n }\n\n var altVariationSide = getOppositePlacement(mainVariationSide);\n var checks = [];\n\n if (checkMainAxis) {\n checks.push(overflow[_basePlacement] <= 0);\n }\n\n if (checkAltAxis) {\n checks.push(overflow[mainVariationSide] <= 0, overflow[altVariationSide] <= 0);\n }\n\n if (checks.every(function (check) {\n return check;\n })) {\n firstFittingPlacement = placement;\n makeFallbackChecks = false;\n break;\n }\n\n checksMap.set(placement, checks);\n }\n\n if (makeFallbackChecks) {\n // `2` may be desired in some cases – research later\n var numberOfChecks = flipVariations ? 3 : 1;\n\n var _loop = function _loop(_i) {\n var fittingPlacement = placements.find(function (placement) {\n var checks = checksMap.get(placement);\n\n if (checks) {\n return checks.slice(0, _i).every(function (check) {\n return check;\n });\n }\n });\n\n if (fittingPlacement) {\n firstFittingPlacement = fittingPlacement;\n return \"break\";\n }\n };\n\n for (var _i = numberOfChecks; _i > 0; _i--) {\n var _ret = _loop(_i);\n\n if (_ret === \"break\") break;\n }\n }\n\n if (state.placement !== firstFittingPlacement) {\n state.modifiersData[name]._skip = true;\n state.placement = firstFittingPlacement;\n state.reset = true;\n }\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'flip',\n enabled: true,\n phase: 'main',\n fn: flip,\n requiresIfExists: ['offset'],\n data: {\n _skip: false\n }\n};","import { top, bottom, left, right } from \"../enums.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\n\nfunction getSideOffsets(overflow, rect, preventedOffsets) {\n if (preventedOffsets === void 0) {\n preventedOffsets = {\n x: 0,\n y: 0\n };\n }\n\n return {\n top: overflow.top - rect.height - preventedOffsets.y,\n right: overflow.right - rect.width + preventedOffsets.x,\n bottom: overflow.bottom - rect.height + preventedOffsets.y,\n left: overflow.left - rect.width - preventedOffsets.x\n };\n}\n\nfunction isAnySideFullyClipped(overflow) {\n return [top, right, bottom, left].some(function (side) {\n return overflow[side] >= 0;\n });\n}\n\nfunction hide(_ref) {\n var state = _ref.state,\n name = _ref.name;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var preventedOffsets = state.modifiersData.preventOverflow;\n var referenceOverflow = detectOverflow(state, {\n elementContext: 'reference'\n });\n var popperAltOverflow = detectOverflow(state, {\n altBoundary: true\n });\n var referenceClippingOffsets = getSideOffsets(referenceOverflow, referenceRect);\n var popperEscapeOffsets = getSideOffsets(popperAltOverflow, popperRect, preventedOffsets);\n var isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets);\n var hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets);\n state.modifiersData[name] = {\n referenceClippingOffsets: referenceClippingOffsets,\n popperEscapeOffsets: popperEscapeOffsets,\n isReferenceHidden: isReferenceHidden,\n hasPopperEscaped: hasPopperEscaped\n };\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-reference-hidden': isReferenceHidden,\n 'data-popper-escaped': hasPopperEscaped\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'hide',\n enabled: true,\n phase: 'main',\n requiresIfExists: ['preventOverflow'],\n fn: hide\n};","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport { top, left, right, placements } from \"../enums.js\";\nexport function distanceAndSkiddingToXY(placement, rects, offset) {\n var basePlacement = getBasePlacement(placement);\n var invertDistance = [left, top].indexOf(basePlacement) >= 0 ? -1 : 1;\n\n var _ref = typeof offset === 'function' ? offset(Object.assign({}, rects, {\n placement: placement\n })) : offset,\n skidding = _ref[0],\n distance = _ref[1];\n\n skidding = skidding || 0;\n distance = (distance || 0) * invertDistance;\n return [left, right].indexOf(basePlacement) >= 0 ? {\n x: distance,\n y: skidding\n } : {\n x: skidding,\n y: distance\n };\n}\n\nfunction offset(_ref2) {\n var state = _ref2.state,\n options = _ref2.options,\n name = _ref2.name;\n var _options$offset = options.offset,\n offset = _options$offset === void 0 ? [0, 0] : _options$offset;\n var data = placements.reduce(function (acc, placement) {\n acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset);\n return acc;\n }, {});\n var _data$state$placement = data[state.placement],\n x = _data$state$placement.x,\n y = _data$state$placement.y;\n\n if (state.modifiersData.popperOffsets != null) {\n state.modifiersData.popperOffsets.x += x;\n state.modifiersData.popperOffsets.y += y;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'offset',\n enabled: true,\n phase: 'main',\n requires: ['popperOffsets'],\n fn: offset\n};","import computeOffsets from \"../utils/computeOffsets.js\";\n\nfunction popperOffsets(_ref) {\n var state = _ref.state,\n name = _ref.name;\n // Offsets are the actual position the popper needs to have to be\n // properly positioned near its reference element\n // This is the most basic placement, and will be adjusted by\n // the modifiers in the next step\n state.modifiersData[name] = computeOffsets({\n reference: state.rects.reference,\n element: state.rects.popper,\n strategy: 'absolute',\n placement: state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'popperOffsets',\n enabled: true,\n phase: 'read',\n fn: popperOffsets,\n data: {}\n};","import { top, left, right, bottom, start } from \"../enums.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport getAltAxis from \"../utils/getAltAxis.js\";\nimport within from \"../utils/within.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport getFreshSideObject from \"../utils/getFreshSideObject.js\";\nimport { max as mathMax, min as mathMin } from \"../utils/math.js\";\n\nfunction preventOverflow(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? false : _options$altAxis,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n padding = options.padding,\n _options$tether = options.tether,\n tether = _options$tether === void 0 ? true : _options$tether,\n _options$tetherOffset = options.tetherOffset,\n tetherOffset = _options$tetherOffset === void 0 ? 0 : _options$tetherOffset;\n var overflow = detectOverflow(state, {\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n altBoundary: altBoundary\n });\n var basePlacement = getBasePlacement(state.placement);\n var variation = getVariation(state.placement);\n var isBasePlacement = !variation;\n var mainAxis = getMainAxisFromPlacement(basePlacement);\n var altAxis = getAltAxis(mainAxis);\n var popperOffsets = state.modifiersData.popperOffsets;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var tetherOffsetValue = typeof tetherOffset === 'function' ? tetherOffset(Object.assign({}, state.rects, {\n placement: state.placement\n })) : tetherOffset;\n var data = {\n x: 0,\n y: 0\n };\n\n if (!popperOffsets) {\n return;\n }\n\n if (checkMainAxis || checkAltAxis) {\n var mainSide = mainAxis === 'y' ? top : left;\n var altSide = mainAxis === 'y' ? bottom : right;\n var len = mainAxis === 'y' ? 'height' : 'width';\n var offset = popperOffsets[mainAxis];\n var min = popperOffsets[mainAxis] + overflow[mainSide];\n var max = popperOffsets[mainAxis] - overflow[altSide];\n var additive = tether ? -popperRect[len] / 2 : 0;\n var minLen = variation === start ? referenceRect[len] : popperRect[len];\n var maxLen = variation === start ? -popperRect[len] : -referenceRect[len]; // We need to include the arrow in the calculation so the arrow doesn't go\n // outside the reference bounds\n\n var arrowElement = state.elements.arrow;\n var arrowRect = tether && arrowElement ? getLayoutRect(arrowElement) : {\n width: 0,\n height: 0\n };\n var arrowPaddingObject = state.modifiersData['arrow#persistent'] ? state.modifiersData['arrow#persistent'].padding : getFreshSideObject();\n var arrowPaddingMin = arrowPaddingObject[mainSide];\n var arrowPaddingMax = arrowPaddingObject[altSide]; // If the reference length is smaller than the arrow length, we don't want\n // to include its full size in the calculation. If the reference is small\n // and near the edge of a boundary, the popper can overflow even if the\n // reference is not overflowing as well (e.g. virtual elements with no\n // width or height)\n\n var arrowLen = within(0, referenceRect[len], arrowRect[len]);\n var minOffset = isBasePlacement ? referenceRect[len] / 2 - additive - arrowLen - arrowPaddingMin - tetherOffsetValue : minLen - arrowLen - arrowPaddingMin - tetherOffsetValue;\n var maxOffset = isBasePlacement ? -referenceRect[len] / 2 + additive + arrowLen + arrowPaddingMax + tetherOffsetValue : maxLen + arrowLen + arrowPaddingMax + tetherOffsetValue;\n var arrowOffsetParent = state.elements.arrow && getOffsetParent(state.elements.arrow);\n var clientOffset = arrowOffsetParent ? mainAxis === 'y' ? arrowOffsetParent.clientTop || 0 : arrowOffsetParent.clientLeft || 0 : 0;\n var offsetModifierValue = state.modifiersData.offset ? state.modifiersData.offset[state.placement][mainAxis] : 0;\n var tetherMin = popperOffsets[mainAxis] + minOffset - offsetModifierValue - clientOffset;\n var tetherMax = popperOffsets[mainAxis] + maxOffset - offsetModifierValue;\n\n if (checkMainAxis) {\n var preventedOffset = within(tether ? mathMin(min, tetherMin) : min, offset, tether ? mathMax(max, tetherMax) : max);\n popperOffsets[mainAxis] = preventedOffset;\n data[mainAxis] = preventedOffset - offset;\n }\n\n if (checkAltAxis) {\n var _mainSide = mainAxis === 'x' ? top : left;\n\n var _altSide = mainAxis === 'x' ? bottom : right;\n\n var _offset = popperOffsets[altAxis];\n\n var _min = _offset + overflow[_mainSide];\n\n var _max = _offset - overflow[_altSide];\n\n var _preventedOffset = within(tether ? mathMin(_min, tetherMin) : _min, _offset, tether ? mathMax(_max, tetherMax) : _max);\n\n popperOffsets[altAxis] = _preventedOffset;\n data[altAxis] = _preventedOffset - _offset;\n }\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'preventOverflow',\n enabled: true,\n phase: 'main',\n fn: preventOverflow,\n requiresIfExists: ['offset']\n};","export default function getAltAxis(axis) {\n return axis === 'x' ? 'y' : 'x';\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getNodeScroll from \"./getNodeScroll.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport isScrollParent from \"./isScrollParent.js\"; // Returns the composite rect of an element relative to its offsetParent.\n// Composite means it takes into account transforms as well as layout.\n\nexport default function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) {\n if (isFixed === void 0) {\n isFixed = false;\n }\n\n var documentElement = getDocumentElement(offsetParent);\n var rect = getBoundingClientRect(elementOrVirtualElement);\n var isOffsetParentAnElement = isHTMLElement(offsetParent);\n var scroll = {\n scrollLeft: 0,\n scrollTop: 0\n };\n var offsets = {\n x: 0,\n y: 0\n };\n\n if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {\n if (getNodeName(offsetParent) !== 'body' || // https://github.com/popperjs/popper-core/issues/1078\n isScrollParent(documentElement)) {\n scroll = getNodeScroll(offsetParent);\n }\n\n if (isHTMLElement(offsetParent)) {\n offsets = getBoundingClientRect(offsetParent);\n offsets.x += offsetParent.clientLeft;\n offsets.y += offsetParent.clientTop;\n } else if (documentElement) {\n offsets.x = getWindowScrollBarX(documentElement);\n }\n }\n\n return {\n x: rect.left + scroll.scrollLeft - offsets.x,\n y: rect.top + scroll.scrollTop - offsets.y,\n width: rect.width,\n height: rect.height\n };\n}","import getWindowScroll from \"./getWindowScroll.js\";\nimport getWindow from \"./getWindow.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getHTMLElementScroll from \"./getHTMLElementScroll.js\";\nexport default function getNodeScroll(node) {\n if (node === getWindow(node) || !isHTMLElement(node)) {\n return getWindowScroll(node);\n } else {\n return getHTMLElementScroll(node);\n }\n}","export default function getHTMLElementScroll(element) {\n return {\n scrollLeft: element.scrollLeft,\n scrollTop: element.scrollTop\n };\n}","import getCompositeRect from \"./dom-utils/getCompositeRect.js\";\nimport getLayoutRect from \"./dom-utils/getLayoutRect.js\";\nimport listScrollParents from \"./dom-utils/listScrollParents.js\";\nimport getOffsetParent from \"./dom-utils/getOffsetParent.js\";\nimport getComputedStyle from \"./dom-utils/getComputedStyle.js\";\nimport orderModifiers from \"./utils/orderModifiers.js\";\nimport debounce from \"./utils/debounce.js\";\nimport validateModifiers from \"./utils/validateModifiers.js\";\nimport uniqueBy from \"./utils/uniqueBy.js\";\nimport getBasePlacement from \"./utils/getBasePlacement.js\";\nimport mergeByName from \"./utils/mergeByName.js\";\nimport detectOverflow from \"./utils/detectOverflow.js\";\nimport { isElement } from \"./dom-utils/instanceOf.js\";\nimport { auto } from \"./enums.js\";\nvar INVALID_ELEMENT_ERROR = 'Popper: Invalid reference or popper argument provided. They must be either a DOM element or virtual element.';\nvar INFINITE_LOOP_ERROR = 'Popper: An infinite loop in the modifiers cycle has been detected! The cycle has been interrupted to prevent a browser crash.';\nvar DEFAULT_OPTIONS = {\n placement: 'bottom',\n modifiers: [],\n strategy: 'absolute'\n};\n\nfunction areValidElements() {\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n return !args.some(function (element) {\n return !(element && typeof element.getBoundingClientRect === 'function');\n });\n}\n\nexport function popperGenerator(generatorOptions) {\n if (generatorOptions === void 0) {\n generatorOptions = {};\n }\n\n var _generatorOptions = generatorOptions,\n _generatorOptions$def = _generatorOptions.defaultModifiers,\n defaultModifiers = _generatorOptions$def === void 0 ? [] : _generatorOptions$def,\n _generatorOptions$def2 = _generatorOptions.defaultOptions,\n defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2;\n return function createPopper(reference, popper, options) {\n if (options === void 0) {\n options = defaultOptions;\n }\n\n var state = {\n placement: 'bottom',\n orderedModifiers: [],\n options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions),\n modifiersData: {},\n elements: {\n reference: reference,\n popper: popper\n },\n attributes: {},\n styles: {}\n };\n var effectCleanupFns = [];\n var isDestroyed = false;\n var instance = {\n state: state,\n setOptions: function setOptions(options) {\n cleanupModifierEffects();\n state.options = Object.assign({}, defaultOptions, state.options, options);\n state.scrollParents = {\n reference: isElement(reference) ? listScrollParents(reference) : reference.contextElement ? listScrollParents(reference.contextElement) : [],\n popper: listScrollParents(popper)\n }; // Orders the modifiers based on their dependencies and `phase`\n // properties\n\n var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers, state.options.modifiers))); // Strip out disabled modifiers\n\n state.orderedModifiers = orderedModifiers.filter(function (m) {\n return m.enabled;\n }); // Validate the provided modifiers so that the consumer will get warned\n // if one of the modifiers is invalid for any reason\n\n if (process.env.NODE_ENV !== \"production\") {\n var modifiers = uniqueBy([].concat(orderedModifiers, state.options.modifiers), function (_ref) {\n var name = _ref.name;\n return name;\n });\n validateModifiers(modifiers);\n\n if (getBasePlacement(state.options.placement) === auto) {\n var flipModifier = state.orderedModifiers.find(function (_ref2) {\n var name = _ref2.name;\n return name === 'flip';\n });\n\n if (!flipModifier) {\n console.error(['Popper: \"auto\" placements require the \"flip\" modifier be', 'present and enabled to work.'].join(' '));\n }\n }\n\n var _getComputedStyle = getComputedStyle(popper),\n marginTop = _getComputedStyle.marginTop,\n marginRight = _getComputedStyle.marginRight,\n marginBottom = _getComputedStyle.marginBottom,\n marginLeft = _getComputedStyle.marginLeft; // We no longer take into account `margins` on the popper, and it can\n // cause bugs with positioning, so we'll warn the consumer\n\n\n if ([marginTop, marginRight, marginBottom, marginLeft].some(function (margin) {\n return parseFloat(margin);\n })) {\n console.warn(['Popper: CSS \"margin\" styles cannot be used to apply padding', 'between the popper and its reference element or boundary.', 'To replicate margin, use the `offset` modifier, as well as', 'the `padding` option in the `preventOverflow` and `flip`', 'modifiers.'].join(' '));\n }\n }\n\n runModifierEffects();\n return instance.update();\n },\n // Sync update – it will always be executed, even if not necessary. This\n // is useful for low frequency updates where sync behavior simplifies the\n // logic.\n // For high frequency updates (e.g. `resize` and `scroll` events), always\n // prefer the async Popper#update method\n forceUpdate: function forceUpdate() {\n if (isDestroyed) {\n return;\n }\n\n var _state$elements = state.elements,\n reference = _state$elements.reference,\n popper = _state$elements.popper; // Don't proceed if `reference` or `popper` are not valid elements\n // anymore\n\n if (!areValidElements(reference, popper)) {\n if (process.env.NODE_ENV !== \"production\") {\n console.error(INVALID_ELEMENT_ERROR);\n }\n\n return;\n } // Store the reference and popper rects to be read by modifiers\n\n\n state.rects = {\n reference: getCompositeRect(reference, getOffsetParent(popper), state.options.strategy === 'fixed'),\n popper: getLayoutRect(popper)\n }; // Modifiers have the ability to reset the current update cycle. The\n // most common use case for this is the `flip` modifier changing the\n // placement, which then needs to re-run all the modifiers, because the\n // logic was previously ran for the previous placement and is therefore\n // stale/incorrect\n\n state.reset = false;\n state.placement = state.options.placement; // On each update cycle, the `modifiersData` property for each modifier\n // is filled with the initial data specified by the modifier. This means\n // it doesn't persist and is fresh on each update.\n // To ensure persistent data, use `${name}#persistent`\n\n state.orderedModifiers.forEach(function (modifier) {\n return state.modifiersData[modifier.name] = Object.assign({}, modifier.data);\n });\n var __debug_loops__ = 0;\n\n for (var index = 0; index < state.orderedModifiers.length; index++) {\n if (process.env.NODE_ENV !== \"production\") {\n __debug_loops__ += 1;\n\n if (__debug_loops__ > 100) {\n console.error(INFINITE_LOOP_ERROR);\n break;\n }\n }\n\n if (state.reset === true) {\n state.reset = false;\n index = -1;\n continue;\n }\n\n var _state$orderedModifie = state.orderedModifiers[index],\n fn = _state$orderedModifie.fn,\n _state$orderedModifie2 = _state$orderedModifie.options,\n _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2,\n name = _state$orderedModifie.name;\n\n if (typeof fn === 'function') {\n state = fn({\n state: state,\n options: _options,\n name: name,\n instance: instance\n }) || state;\n }\n }\n },\n // Async and optimistically optimized update – it will not be executed if\n // not necessary (debounced to run at most once-per-tick)\n update: debounce(function () {\n return new Promise(function (resolve) {\n instance.forceUpdate();\n resolve(state);\n });\n }),\n destroy: function destroy() {\n cleanupModifierEffects();\n isDestroyed = true;\n }\n };\n\n if (!areValidElements(reference, popper)) {\n if (process.env.NODE_ENV !== \"production\") {\n console.error(INVALID_ELEMENT_ERROR);\n }\n\n return instance;\n }\n\n instance.setOptions(options).then(function (state) {\n if (!isDestroyed && options.onFirstUpdate) {\n options.onFirstUpdate(state);\n }\n }); // Modifiers have the ability to execute arbitrary code before the first\n // update cycle runs. They will be executed in the same order as the update\n // cycle. This is useful when a modifier adds some persistent data that\n // other modifiers need to use, but the modifier is run after the dependent\n // one.\n\n function runModifierEffects() {\n state.orderedModifiers.forEach(function (_ref3) {\n var name = _ref3.name,\n _ref3$options = _ref3.options,\n options = _ref3$options === void 0 ? {} : _ref3$options,\n effect = _ref3.effect;\n\n if (typeof effect === 'function') {\n var cleanupFn = effect({\n state: state,\n name: name,\n instance: instance,\n options: options\n });\n\n var noopFn = function noopFn() {};\n\n effectCleanupFns.push(cleanupFn || noopFn);\n }\n });\n }\n\n function cleanupModifierEffects() {\n effectCleanupFns.forEach(function (fn) {\n return fn();\n });\n effectCleanupFns = [];\n }\n\n return instance;\n };\n}\nexport var createPopper = /*#__PURE__*/popperGenerator(); // eslint-disable-next-line import/no-unused-modules\n\nexport { detectOverflow };","export default function debounce(fn) {\n var pending;\n return function () {\n if (!pending) {\n pending = new Promise(function (resolve) {\n Promise.resolve().then(function () {\n pending = undefined;\n resolve(fn());\n });\n });\n }\n\n return pending;\n };\n}","export default function mergeByName(modifiers) {\n var merged = modifiers.reduce(function (merged, current) {\n var existing = merged[current.name];\n merged[current.name] = existing ? Object.assign({}, existing, current, {\n options: Object.assign({}, existing.options, current.options),\n data: Object.assign({}, existing.data, current.data)\n }) : current;\n return merged;\n }, {}); // IE11 does not support Object.values\n\n return Object.keys(merged).map(function (key) {\n return merged[key];\n });\n}","import { modifierPhases } from \"../enums.js\"; // source: https://stackoverflow.com/questions/49875255\n\nfunction order(modifiers) {\n var map = new Map();\n var visited = new Set();\n var result = [];\n modifiers.forEach(function (modifier) {\n map.set(modifier.name, modifier);\n }); // On visiting object, check for its dependencies and visit them recursively\n\n function sort(modifier) {\n visited.add(modifier.name);\n var requires = [].concat(modifier.requires || [], modifier.requiresIfExists || []);\n requires.forEach(function (dep) {\n if (!visited.has(dep)) {\n var depModifier = map.get(dep);\n\n if (depModifier) {\n sort(depModifier);\n }\n }\n });\n result.push(modifier);\n }\n\n modifiers.forEach(function (modifier) {\n if (!visited.has(modifier.name)) {\n // check for visited object\n sort(modifier);\n }\n });\n return result;\n}\n\nexport default function orderModifiers(modifiers) {\n // order based on dependencies\n var orderedModifiers = order(modifiers); // order based on phase\n\n return modifierPhases.reduce(function (acc, phase) {\n return acc.concat(orderedModifiers.filter(function (modifier) {\n return modifier.phase === phase;\n }));\n }, []);\n}","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow };","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nimport offset from \"./modifiers/offset.js\";\nimport flip from \"./modifiers/flip.js\";\nimport preventOverflow from \"./modifiers/preventOverflow.js\";\nimport arrow from \"./modifiers/arrow.js\";\nimport hide from \"./modifiers/hide.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles, offset, flip, preventOverflow, arrow, hide];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow }; // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper as createPopperLite } from \"./popper-lite.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport * from \"./modifiers/index.js\";","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.0.0-beta3): dropdown.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport * as Popper from '@popperjs/core'\n\nimport {\n defineJQueryPlugin,\n getElementFromSelector,\n isElement,\n isVisible,\n isRTL,\n noop,\n typeCheckConfig\n} from './util/index'\nimport Data from './dom/data'\nimport EventHandler from './dom/event-handler'\nimport Manipulator from './dom/manipulator'\nimport SelectorEngine from './dom/selector-engine'\nimport BaseComponent from './base-component'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'dropdown'\nconst DATA_KEY = 'bs.dropdown'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst ESCAPE_KEY = 'Escape'\nconst SPACE_KEY = 'Space'\nconst TAB_KEY = 'Tab'\nconst ARROW_UP_KEY = 'ArrowUp'\nconst ARROW_DOWN_KEY = 'ArrowDown'\nconst RIGHT_MOUSE_BUTTON = 2 // MouseEvent.button value for the secondary button, usually the right button\n\nconst REGEXP_KEYDOWN = new RegExp(`${ARROW_UP_KEY}|${ARROW_DOWN_KEY}|${ESCAPE_KEY}`)\n\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_CLICK = `click${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_DISABLED = 'disabled'\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_DROPUP = 'dropup'\nconst CLASS_NAME_DROPEND = 'dropend'\nconst CLASS_NAME_DROPSTART = 'dropstart'\nconst CLASS_NAME_NAVBAR = 'navbar'\n\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"dropdown\"]'\nconst SELECTOR_MENU = '.dropdown-menu'\nconst SELECTOR_NAVBAR_NAV = '.navbar-nav'\nconst SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'\n\nconst PLACEMENT_TOP = isRTL() ? 'top-end' : 'top-start'\nconst PLACEMENT_TOPEND = isRTL() ? 'top-start' : 'top-end'\nconst PLACEMENT_BOTTOM = isRTL() ? 'bottom-end' : 'bottom-start'\nconst PLACEMENT_BOTTOMEND = isRTL() ? 'bottom-start' : 'bottom-end'\nconst PLACEMENT_RIGHT = isRTL() ? 'left-start' : 'right-start'\nconst PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start'\n\nconst Default = {\n offset: [0, 2],\n boundary: 'clippingParents',\n reference: 'toggle',\n display: 'dynamic',\n popperConfig: null\n}\n\nconst DefaultType = {\n offset: '(array|string|function)',\n boundary: '(string|element)',\n reference: '(string|element|object)',\n display: 'string',\n popperConfig: '(null|object|function)'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Dropdown extends BaseComponent {\n constructor(element, config) {\n super(element)\n\n this._popper = null\n this._config = this._getConfig(config)\n this._menu = this._getMenuElement()\n this._inNavbar = this._detectNavbar()\n\n this._addEventListeners()\n }\n\n // Getters\n\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get DATA_KEY() {\n return DATA_KEY\n }\n\n // Public\n\n toggle() {\n if (this._element.disabled || this._element.classList.contains(CLASS_NAME_DISABLED)) {\n return\n }\n\n const isActive = this._element.classList.contains(CLASS_NAME_SHOW)\n\n Dropdown.clearMenus()\n\n if (isActive) {\n return\n }\n\n this.show()\n }\n\n show() {\n if (this._element.disabled || this._element.classList.contains(CLASS_NAME_DISABLED) || this._menu.classList.contains(CLASS_NAME_SHOW)) {\n return\n }\n\n const parent = Dropdown.getParentFromElement(this._element)\n const relatedTarget = {\n relatedTarget: this._element\n }\n\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, relatedTarget)\n\n if (showEvent.defaultPrevented) {\n return\n }\n\n // Totally disable Popper for Dropdowns in Navbar\n if (this._inNavbar) {\n Manipulator.setDataAttribute(this._menu, 'popper', 'none')\n } else {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s dropdowns require Popper (https://popper.js.org)')\n }\n\n let referenceElement = this._element\n\n if (this._config.reference === 'parent') {\n referenceElement = parent\n } else if (isElement(this._config.reference)) {\n referenceElement = this._config.reference\n\n // Check if it's jQuery element\n if (typeof this._config.reference.jquery !== 'undefined') {\n referenceElement = this._config.reference[0]\n }\n } else if (typeof this._config.reference === 'object') {\n referenceElement = this._config.reference\n }\n\n const popperConfig = this._getPopperConfig()\n const isDisplayStatic = popperConfig.modifiers.find(modifier => modifier.name === 'applyStyles' && modifier.enabled === false)\n\n this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig)\n\n if (isDisplayStatic) {\n Manipulator.setDataAttribute(this._menu, 'popper', 'static')\n }\n }\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement &&\n !parent.closest(SELECTOR_NAVBAR_NAV)) {\n [].concat(...document.body.children)\n .forEach(elem => EventHandler.on(elem, 'mouseover', null, noop()))\n }\n\n this._element.focus()\n this._element.setAttribute('aria-expanded', true)\n\n this._menu.classList.toggle(CLASS_NAME_SHOW)\n this._element.classList.toggle(CLASS_NAME_SHOW)\n EventHandler.trigger(this._element, EVENT_SHOWN, relatedTarget)\n }\n\n hide() {\n if (this._element.disabled || this._element.classList.contains(CLASS_NAME_DISABLED) || !this._menu.classList.contains(CLASS_NAME_SHOW)) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE, relatedTarget)\n\n if (hideEvent.defaultPrevented) {\n return\n }\n\n if (this._popper) {\n this._popper.destroy()\n }\n\n this._menu.classList.toggle(CLASS_NAME_SHOW)\n this._element.classList.toggle(CLASS_NAME_SHOW)\n Manipulator.removeDataAttribute(this._menu, 'popper')\n EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget)\n }\n\n dispose() {\n EventHandler.off(this._element, EVENT_KEY)\n this._menu = null\n\n if (this._popper) {\n this._popper.destroy()\n this._popper = null\n }\n\n super.dispose()\n }\n\n update() {\n this._inNavbar = this._detectNavbar()\n if (this._popper) {\n this._popper.update()\n }\n }\n\n // Private\n\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_CLICK, event => {\n event.preventDefault()\n this.toggle()\n })\n }\n\n _getConfig(config) {\n config = {\n ...this.constructor.Default,\n ...Manipulator.getDataAttributes(this._element),\n ...config\n }\n\n typeCheckConfig(NAME, config, this.constructor.DefaultType)\n\n if (typeof config.reference === 'object' && !isElement(config.reference) &&\n typeof config.reference.getBoundingClientRect !== 'function'\n ) {\n // Popper virtual elements require a getBoundingClientRect method\n throw new TypeError(`${NAME.toUpperCase()}: Option \"reference\" provided type \"object\" without a required \"getBoundingClientRect\" method.`)\n }\n\n return config\n }\n\n _getMenuElement() {\n return SelectorEngine.next(this._element, SELECTOR_MENU)[0]\n }\n\n _getPlacement() {\n const parentDropdown = this._element.parentNode\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) {\n return PLACEMENT_RIGHT\n }\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) {\n return PLACEMENT_LEFT\n }\n\n // We need to trim the value because custom properties can also include spaces\n const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end'\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) {\n return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP\n }\n\n return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM\n }\n\n _detectNavbar() {\n return this._element.closest(`.${CLASS_NAME_NAVBAR}`) !== null\n }\n\n _getOffset() {\n const { offset } = this._config\n\n if (typeof offset === 'string') {\n return offset.split(',').map(val => Number.parseInt(val, 10))\n }\n\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element)\n }\n\n return offset\n }\n\n _getPopperConfig() {\n const defaultBsPopperConfig = {\n placement: this._getPlacement(),\n modifiers: [{\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n },\n {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n }]\n }\n\n // Disable Popper if we have a static display\n if (this._config.display === 'static') {\n defaultBsPopperConfig.modifiers = [{\n name: 'applyStyles',\n enabled: false\n }]\n }\n\n return {\n ...defaultBsPopperConfig,\n ...(typeof this._config.popperConfig === 'function' ? this._config.popperConfig(defaultBsPopperConfig) : this._config.popperConfig)\n }\n }\n\n // Static\n\n static dropdownInterface(element, config) {\n let data = Data.get(element, DATA_KEY)\n const _config = typeof config === 'object' ? config : null\n\n if (!data) {\n data = new Dropdown(element, _config)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config]()\n }\n }\n\n static jQueryInterface(config) {\n return this.each(function () {\n Dropdown.dropdownInterface(this, config)\n })\n }\n\n static clearMenus(event) {\n if (event) {\n if (event.button === RIGHT_MOUSE_BUTTON || (event.type === 'keyup' && event.key !== TAB_KEY)) {\n return\n }\n\n if (/input|select|textarea|form/i.test(event.target.tagName)) {\n return\n }\n }\n\n const toggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE)\n\n for (let i = 0, len = toggles.length; i < len; i++) {\n const context = Data.get(toggles[i], DATA_KEY)\n const relatedTarget = {\n relatedTarget: toggles[i]\n }\n\n if (event && event.type === 'click') {\n relatedTarget.clickEvent = event\n }\n\n if (!context) {\n continue\n }\n\n const dropdownMenu = context._menu\n if (!toggles[i].classList.contains(CLASS_NAME_SHOW)) {\n continue\n }\n\n if (event) {\n // Don't close the menu if the clicked element or one of its parents is the dropdown button\n if ([context._element].some(element => event.composedPath().includes(element))) {\n continue\n }\n\n // Tab navigation through the dropdown menu shouldn't close the menu\n if (event.type === 'keyup' && event.key === TAB_KEY && dropdownMenu.contains(event.target)) {\n continue\n }\n }\n\n const hideEvent = EventHandler.trigger(toggles[i], EVENT_HIDE, relatedTarget)\n if (hideEvent.defaultPrevented) {\n continue\n }\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n [].concat(...document.body.children)\n .forEach(elem => EventHandler.off(elem, 'mouseover', null, noop()))\n }\n\n toggles[i].setAttribute('aria-expanded', 'false')\n\n if (context._popper) {\n context._popper.destroy()\n }\n\n dropdownMenu.classList.remove(CLASS_NAME_SHOW)\n toggles[i].classList.remove(CLASS_NAME_SHOW)\n Manipulator.removeDataAttribute(dropdownMenu, 'popper')\n EventHandler.trigger(toggles[i], EVENT_HIDDEN, relatedTarget)\n }\n }\n\n static getParentFromElement(element) {\n return getElementFromSelector(element) || element.parentNode\n }\n\n static dataApiKeydownHandler(event) {\n // If not input/textarea:\n // - And not a key in REGEXP_KEYDOWN => not a dropdown command\n // If input/textarea:\n // - If space key => not a dropdown command\n // - If key is other than escape\n // - If key is not up or down => not a dropdown command\n // - If trigger inside the menu => not a dropdown command\n if (/input|textarea/i.test(event.target.tagName) ?\n event.key === SPACE_KEY || (event.key !== ESCAPE_KEY &&\n ((event.key !== ARROW_DOWN_KEY && event.key !== ARROW_UP_KEY) ||\n event.target.closest(SELECTOR_MENU))) :\n !REGEXP_KEYDOWN.test(event.key)) {\n return\n }\n\n event.preventDefault()\n event.stopPropagation()\n\n if (this.disabled || this.classList.contains(CLASS_NAME_DISABLED)) {\n return\n }\n\n const parent = Dropdown.getParentFromElement(this)\n const isActive = this.classList.contains(CLASS_NAME_SHOW)\n\n if (event.key === ESCAPE_KEY) {\n const button = this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0]\n button.focus()\n Dropdown.clearMenus()\n return\n }\n\n if (!isActive && (event.key === ARROW_UP_KEY || event.key === ARROW_DOWN_KEY)) {\n const button = this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0]\n button.click()\n return\n }\n\n if (!isActive || event.key === SPACE_KEY) {\n Dropdown.clearMenus()\n return\n }\n\n const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, parent).filter(isVisible)\n\n if (!items.length) {\n return\n }\n\n let index = items.indexOf(event.target)\n\n // Up\n if (event.key === ARROW_UP_KEY && index > 0) {\n index--\n }\n\n // Down\n if (event.key === ARROW_DOWN_KEY && index < items.length - 1) {\n index++\n }\n\n // index is -1 if the first keydown is an ArrowUp\n index = index === -1 ? 0 : index\n\n items[index].focus()\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Dropdown.dataApiKeydownHandler)\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler)\nEventHandler.on(document, EVENT_CLICK_DATA_API, Dropdown.clearMenus)\nEventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus)\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n event.preventDefault()\n Dropdown.dropdownInterface(this)\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n * add .Dropdown to jQuery only if jQuery is present\n */\n\ndefineJQueryPlugin(NAME, Dropdown)\n\nexport default Dropdown\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.0.0-beta3): modal.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport {\n defineJQueryPlugin,\n emulateTransitionEnd,\n getElementFromSelector,\n getTransitionDurationFromElement,\n isVisible,\n isRTL,\n reflow,\n typeCheckConfig\n} from './util/index'\nimport Data from './dom/data'\nimport EventHandler from './dom/event-handler'\nimport Manipulator from './dom/manipulator'\nimport SelectorEngine from './dom/selector-engine'\nimport BaseComponent from './base-component'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'modal'\nconst DATA_KEY = 'bs.modal'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst ESCAPE_KEY = 'Escape'\n\nconst Default = {\n backdrop: true,\n keyboard: true,\n focus: true\n}\n\nconst DefaultType = {\n backdrop: '(boolean|string)',\n keyboard: 'boolean',\n focus: 'boolean'\n}\n\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_FOCUSIN = `focusin${EVENT_KEY}`\nconst EVENT_RESIZE = `resize${EVENT_KEY}`\nconst EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`\nconst EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`\nconst EVENT_MOUSEUP_DISMISS = `mouseup.dismiss${EVENT_KEY}`\nconst EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_SCROLLBAR_MEASURER = 'modal-scrollbar-measure'\nconst CLASS_NAME_BACKDROP = 'modal-backdrop'\nconst CLASS_NAME_OPEN = 'modal-open'\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_STATIC = 'modal-static'\n\nconst SELECTOR_DIALOG = '.modal-dialog'\nconst SELECTOR_MODAL_BODY = '.modal-body'\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"modal\"]'\nconst SELECTOR_DATA_DISMISS = '[data-bs-dismiss=\"modal\"]'\nconst SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top'\nconst SELECTOR_STICKY_CONTENT = '.sticky-top'\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Modal extends BaseComponent {\n constructor(element, config) {\n super(element)\n\n this._config = this._getConfig(config)\n this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element)\n this._backdrop = null\n this._isShown = false\n this._isBodyOverflowing = false\n this._ignoreBackdropClick = false\n this._isTransitioning = false\n this._scrollbarWidth = 0\n }\n\n // Getters\n\n static get Default() {\n return Default\n }\n\n static get DATA_KEY() {\n return DATA_KEY\n }\n\n // Public\n\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget)\n }\n\n show(relatedTarget) {\n if (this._isShown || this._isTransitioning) {\n return\n }\n\n if (this._isAnimated()) {\n this._isTransitioning = true\n }\n\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, {\n relatedTarget\n })\n\n if (this._isShown || showEvent.defaultPrevented) {\n return\n }\n\n this._isShown = true\n\n this._checkScrollbar()\n this._setScrollbar()\n\n this._adjustDialog()\n\n this._setEscapeEvent()\n this._setResizeEvent()\n\n EventHandler.on(this._element, EVENT_CLICK_DISMISS, SELECTOR_DATA_DISMISS, event => this.hide(event))\n\n EventHandler.on(this._dialog, EVENT_MOUSEDOWN_DISMISS, () => {\n EventHandler.one(this._element, EVENT_MOUSEUP_DISMISS, event => {\n if (event.target === this._element) {\n this._ignoreBackdropClick = true\n }\n })\n })\n\n this._showBackdrop(() => this._showElement(relatedTarget))\n }\n\n hide(event) {\n if (event) {\n event.preventDefault()\n }\n\n if (!this._isShown || this._isTransitioning) {\n return\n }\n\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)\n\n if (hideEvent.defaultPrevented) {\n return\n }\n\n this._isShown = false\n const isAnimated = this._isAnimated()\n\n if (isAnimated) {\n this._isTransitioning = true\n }\n\n this._setEscapeEvent()\n this._setResizeEvent()\n\n EventHandler.off(document, EVENT_FOCUSIN)\n\n this._element.classList.remove(CLASS_NAME_SHOW)\n\n EventHandler.off(this._element, EVENT_CLICK_DISMISS)\n EventHandler.off(this._dialog, EVENT_MOUSEDOWN_DISMISS)\n\n if (isAnimated) {\n const transitionDuration = getTransitionDurationFromElement(this._element)\n\n EventHandler.one(this._element, 'transitionend', event => this._hideModal(event))\n emulateTransitionEnd(this._element, transitionDuration)\n } else {\n this._hideModal()\n }\n }\n\n dispose() {\n [window, this._element, this._dialog]\n .forEach(htmlElement => EventHandler.off(htmlElement, EVENT_KEY))\n\n super.dispose()\n\n /**\n * `document` has 2 events `EVENT_FOCUSIN` and `EVENT_CLICK_DATA_API`\n * Do not move `document` in `htmlElements` array\n * It will remove `EVENT_CLICK_DATA_API` event that should remain\n */\n EventHandler.off(document, EVENT_FOCUSIN)\n\n this._config = null\n this._dialog = null\n this._backdrop = null\n this._isShown = null\n this._isBodyOverflowing = null\n this._ignoreBackdropClick = null\n this._isTransitioning = null\n this._scrollbarWidth = null\n }\n\n handleUpdate() {\n this._adjustDialog()\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _showElement(relatedTarget) {\n const isAnimated = this._isAnimated()\n const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog)\n\n if (!this._element.parentNode || this._element.parentNode.nodeType !== Node.ELEMENT_NODE) {\n // Don't move modal's DOM position\n document.body.appendChild(this._element)\n }\n\n this._element.style.display = 'block'\n this._element.removeAttribute('aria-hidden')\n this._element.setAttribute('aria-modal', true)\n this._element.setAttribute('role', 'dialog')\n this._element.scrollTop = 0\n\n if (modalBody) {\n modalBody.scrollTop = 0\n }\n\n if (isAnimated) {\n reflow(this._element)\n }\n\n this._element.classList.add(CLASS_NAME_SHOW)\n\n if (this._config.focus) {\n this._enforceFocus()\n }\n\n const transitionComplete = () => {\n if (this._config.focus) {\n this._element.focus()\n }\n\n this._isTransitioning = false\n EventHandler.trigger(this._element, EVENT_SHOWN, {\n relatedTarget\n })\n }\n\n if (isAnimated) {\n const transitionDuration = getTransitionDurationFromElement(this._dialog)\n\n EventHandler.one(this._dialog, 'transitionend', transitionComplete)\n emulateTransitionEnd(this._dialog, transitionDuration)\n } else {\n transitionComplete()\n }\n }\n\n _enforceFocus() {\n EventHandler.off(document, EVENT_FOCUSIN) // guard against infinite focus loop\n EventHandler.on(document, EVENT_FOCUSIN, event => {\n if (document !== event.target &&\n this._element !== event.target &&\n !this._element.contains(event.target)) {\n this._element.focus()\n }\n })\n }\n\n _setEscapeEvent() {\n if (this._isShown) {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {\n if (this._config.keyboard && event.key === ESCAPE_KEY) {\n event.preventDefault()\n this.hide()\n } else if (!this._config.keyboard && event.key === ESCAPE_KEY) {\n this._triggerBackdropTransition()\n }\n })\n } else {\n EventHandler.off(this._element, EVENT_KEYDOWN_DISMISS)\n }\n }\n\n _setResizeEvent() {\n if (this._isShown) {\n EventHandler.on(window, EVENT_RESIZE, () => this._adjustDialog())\n } else {\n EventHandler.off(window, EVENT_RESIZE)\n }\n }\n\n _hideModal() {\n this._element.style.display = 'none'\n this._element.setAttribute('aria-hidden', true)\n this._element.removeAttribute('aria-modal')\n this._element.removeAttribute('role')\n this._isTransitioning = false\n this._showBackdrop(() => {\n document.body.classList.remove(CLASS_NAME_OPEN)\n this._resetAdjustments()\n this._resetScrollbar()\n EventHandler.trigger(this._element, EVENT_HIDDEN)\n })\n }\n\n _removeBackdrop() {\n this._backdrop.parentNode.removeChild(this._backdrop)\n this._backdrop = null\n }\n\n _showBackdrop(callback) {\n const isAnimated = this._isAnimated()\n if (this._isShown && this._config.backdrop) {\n this._backdrop = document.createElement('div')\n this._backdrop.className = CLASS_NAME_BACKDROP\n\n if (isAnimated) {\n this._backdrop.classList.add(CLASS_NAME_FADE)\n }\n\n document.body.appendChild(this._backdrop)\n\n EventHandler.on(this._element, EVENT_CLICK_DISMISS, event => {\n if (this._ignoreBackdropClick) {\n this._ignoreBackdropClick = false\n return\n }\n\n if (event.target !== event.currentTarget) {\n return\n }\n\n if (this._config.backdrop === 'static') {\n this._triggerBackdropTransition()\n } else {\n this.hide()\n }\n })\n\n if (isAnimated) {\n reflow(this._backdrop)\n }\n\n this._backdrop.classList.add(CLASS_NAME_SHOW)\n\n if (!isAnimated) {\n callback()\n return\n }\n\n const backdropTransitionDuration = getTransitionDurationFromElement(this._backdrop)\n\n EventHandler.one(this._backdrop, 'transitionend', callback)\n emulateTransitionEnd(this._backdrop, backdropTransitionDuration)\n } else if (!this._isShown && this._backdrop) {\n this._backdrop.classList.remove(CLASS_NAME_SHOW)\n\n const callbackRemove = () => {\n this._removeBackdrop()\n callback()\n }\n\n if (isAnimated) {\n const backdropTransitionDuration = getTransitionDurationFromElement(this._backdrop)\n EventHandler.one(this._backdrop, 'transitionend', callbackRemove)\n emulateTransitionEnd(this._backdrop, backdropTransitionDuration)\n } else {\n callbackRemove()\n }\n } else {\n callback()\n }\n }\n\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_FADE)\n }\n\n _triggerBackdropTransition() {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED)\n if (hideEvent.defaultPrevented) {\n return\n }\n\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight\n\n if (!isModalOverflowing) {\n this._element.style.overflowY = 'hidden'\n }\n\n this._element.classList.add(CLASS_NAME_STATIC)\n const modalTransitionDuration = getTransitionDurationFromElement(this._dialog)\n EventHandler.off(this._element, 'transitionend')\n EventHandler.one(this._element, 'transitionend', () => {\n this._element.classList.remove(CLASS_NAME_STATIC)\n if (!isModalOverflowing) {\n EventHandler.one(this._element, 'transitionend', () => {\n this._element.style.overflowY = ''\n })\n emulateTransitionEnd(this._element, modalTransitionDuration)\n }\n })\n emulateTransitionEnd(this._element, modalTransitionDuration)\n this._element.focus()\n }\n\n // ----------------------------------------------------------------------\n // the following methods are used to handle overflowing modals\n // ----------------------------------------------------------------------\n\n _adjustDialog() {\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight\n\n if ((!this._isBodyOverflowing && isModalOverflowing && !isRTL()) || (this._isBodyOverflowing && !isModalOverflowing && isRTL())) {\n this._element.style.paddingLeft = `${this._scrollbarWidth}px`\n }\n\n if ((this._isBodyOverflowing && !isModalOverflowing && !isRTL()) || (!this._isBodyOverflowing && isModalOverflowing && isRTL())) {\n this._element.style.paddingRight = `${this._scrollbarWidth}px`\n }\n }\n\n _resetAdjustments() {\n this._element.style.paddingLeft = ''\n this._element.style.paddingRight = ''\n }\n\n _checkScrollbar() {\n const rect = document.body.getBoundingClientRect()\n this._isBodyOverflowing = Math.round(rect.left + rect.right) < window.innerWidth\n this._scrollbarWidth = this._getScrollbarWidth()\n }\n\n _setScrollbar() {\n if (this._isBodyOverflowing) {\n this._setElementAttributes(SELECTOR_FIXED_CONTENT, 'paddingRight', calculatedValue => calculatedValue + this._scrollbarWidth)\n this._setElementAttributes(SELECTOR_STICKY_CONTENT, 'marginRight', calculatedValue => calculatedValue - this._scrollbarWidth)\n this._setElementAttributes('body', 'paddingRight', calculatedValue => calculatedValue + this._scrollbarWidth)\n }\n\n document.body.classList.add(CLASS_NAME_OPEN)\n }\n\n _setElementAttributes(selector, styleProp, callback) {\n SelectorEngine.find(selector)\n .forEach(element => {\n if (element !== document.body && window.innerWidth > element.clientWidth + this._scrollbarWidth) {\n return\n }\n\n const actualValue = element.style[styleProp]\n const calculatedValue = window.getComputedStyle(element)[styleProp]\n Manipulator.setDataAttribute(element, styleProp, actualValue)\n element.style[styleProp] = callback(Number.parseFloat(calculatedValue)) + 'px'\n })\n }\n\n _resetScrollbar() {\n this._resetElementAttributes(SELECTOR_FIXED_CONTENT, 'paddingRight')\n this._resetElementAttributes(SELECTOR_STICKY_CONTENT, 'marginRight')\n this._resetElementAttributes('body', 'paddingRight')\n }\n\n _resetElementAttributes(selector, styleProp) {\n SelectorEngine.find(selector).forEach(element => {\n const value = Manipulator.getDataAttribute(element, styleProp)\n if (typeof value === 'undefined' && element === document.body) {\n element.style[styleProp] = ''\n } else {\n Manipulator.removeDataAttribute(element, styleProp)\n element.style[styleProp] = value\n }\n })\n }\n\n _getScrollbarWidth() { // thx d.walsh\n const scrollDiv = document.createElement('div')\n scrollDiv.className = CLASS_NAME_SCROLLBAR_MEASURER\n document.body.appendChild(scrollDiv)\n const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth\n document.body.removeChild(scrollDiv)\n return scrollbarWidth\n }\n\n // Static\n\n static jQueryInterface(config, relatedTarget) {\n return this.each(function () {\n let data = Data.get(this, DATA_KEY)\n const _config = {\n ...Default,\n ...Manipulator.getDataAttributes(this),\n ...(typeof config === 'object' && config ? config : {})\n }\n\n if (!data) {\n data = new Modal(this, _config)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config](relatedTarget)\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n const target = getElementFromSelector(this)\n\n if (this.tagName === 'A' || this.tagName === 'AREA') {\n event.preventDefault()\n }\n\n EventHandler.one(target, EVENT_SHOW, showEvent => {\n if (showEvent.defaultPrevented) {\n // only register focus restorer if modal will actually get shown\n return\n }\n\n EventHandler.one(target, EVENT_HIDDEN, () => {\n if (isVisible(this)) {\n this.focus()\n }\n })\n })\n\n let data = Data.get(target, DATA_KEY)\n if (!data) {\n const config = {\n ...Manipulator.getDataAttributes(target),\n ...Manipulator.getDataAttributes(this)\n }\n\n data = new Modal(target, config)\n }\n\n data.toggle(this)\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n * add .Modal to jQuery only if jQuery is present\n */\n\ndefineJQueryPlugin(NAME, Modal)\n\nexport default Modal\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.0.0-beta3): util/scrollBar.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport SelectorEngine from '../dom/selector-engine'\nimport Manipulator from '../dom/manipulator'\n\nconst SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed'\nconst SELECTOR_STICKY_CONTENT = '.sticky-top'\n\nconst getWidth = () => {\n // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes\n const documentWidth = document.documentElement.clientWidth\n return Math.abs(window.innerWidth - documentWidth)\n}\n\nconst hide = (width = getWidth()) => {\n document.body.style.overflow = 'hidden'\n _setElementAttributes(SELECTOR_FIXED_CONTENT, 'paddingRight', calculatedValue => calculatedValue + width)\n _setElementAttributes(SELECTOR_STICKY_CONTENT, 'marginRight', calculatedValue => calculatedValue - width)\n _setElementAttributes('body', 'paddingRight', calculatedValue => calculatedValue + width)\n}\n\nconst _setElementAttributes = (selector, styleProp, callback) => {\n const scrollbarWidth = getWidth()\n SelectorEngine.find(selector)\n .forEach(element => {\n if (element !== document.body && window.innerWidth > element.clientWidth + scrollbarWidth) {\n return\n }\n\n const actualValue = element.style[styleProp]\n const calculatedValue = window.getComputedStyle(element)[styleProp]\n Manipulator.setDataAttribute(element, styleProp, actualValue)\n element.style[styleProp] = callback(Number.parseFloat(calculatedValue)) + 'px'\n })\n}\n\nconst reset = () => {\n document.body.style.overflow = 'auto'\n _resetElementAttributes(SELECTOR_FIXED_CONTENT, 'paddingRight')\n _resetElementAttributes(SELECTOR_STICKY_CONTENT, 'marginRight')\n _resetElementAttributes('body', 'paddingRight')\n}\n\nconst _resetElementAttributes = (selector, styleProp) => {\n SelectorEngine.find(selector).forEach(element => {\n const value = Manipulator.getDataAttribute(element, styleProp)\n if (typeof value === 'undefined' && element === document.body) {\n element.style.removeProperty(styleProp)\n } else {\n Manipulator.removeDataAttribute(element, styleProp)\n element.style[styleProp] = value\n }\n })\n}\n\nconst isBodyOverflowing = () => {\n return getWidth() > 0\n}\n\nexport {\n getWidth,\n hide,\n isBodyOverflowing,\n reset\n}\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.0.0-beta3): offcanvas.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport {\n defineJQueryPlugin,\n getElementFromSelector,\n getSelectorFromElement,\n getTransitionDurationFromElement,\n isDisabled,\n isVisible,\n typeCheckConfig\n} from './util/index'\nimport { hide as scrollBarHide, reset as scrollBarReset } from './util/scrollbar'\nimport Data from './dom/data'\nimport EventHandler from './dom/event-handler'\nimport BaseComponent from './base-component'\nimport SelectorEngine from './dom/selector-engine'\nimport Manipulator from './dom/manipulator'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'offcanvas'\nconst DATA_KEY = 'bs.offcanvas'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`\nconst ESCAPE_KEY = 'Escape'\n\nconst Default = {\n backdrop: true,\n keyboard: true,\n scroll: false\n}\n\nconst DefaultType = {\n backdrop: 'boolean',\n keyboard: 'boolean',\n scroll: 'boolean'\n}\n\nconst CLASS_NAME_BACKDROP_BODY = 'offcanvas-backdrop'\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_TOGGLING = 'offcanvas-toggling'\nconst OPEN_SELECTOR = '.offcanvas.show'\nconst ACTIVE_SELECTOR = `${OPEN_SELECTOR}, .${CLASS_NAME_TOGGLING}`\n\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_FOCUSIN = `focusin${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`\n\nconst SELECTOR_DATA_DISMISS = '[data-bs-dismiss=\"offcanvas\"]'\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"offcanvas\"]'\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Offcanvas extends BaseComponent {\n constructor(element, config) {\n super(element)\n\n this._config = this._getConfig(config)\n this._isShown = false\n this._addEventListeners()\n }\n\n // Getters\n\n static get Default() {\n return Default\n }\n\n static get DATA_KEY() {\n return DATA_KEY\n }\n\n // Public\n\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget)\n }\n\n show(relatedTarget) {\n if (this._isShown) {\n return\n }\n\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, { relatedTarget })\n\n if (showEvent.defaultPrevented) {\n return\n }\n\n this._isShown = true\n this._element.style.visibility = 'visible'\n\n if (this._config.backdrop) {\n document.body.classList.add(CLASS_NAME_BACKDROP_BODY)\n }\n\n if (!this._config.scroll) {\n scrollBarHide()\n }\n\n this._element.classList.add(CLASS_NAME_TOGGLING)\n this._element.removeAttribute('aria-hidden')\n this._element.setAttribute('aria-modal', true)\n this._element.setAttribute('role', 'dialog')\n this._element.classList.add(CLASS_NAME_SHOW)\n\n const completeCallBack = () => {\n this._element.classList.remove(CLASS_NAME_TOGGLING)\n EventHandler.trigger(this._element, EVENT_SHOWN, { relatedTarget })\n this._enforceFocusOnElement(this._element)\n }\n\n setTimeout(completeCallBack, getTransitionDurationFromElement(this._element))\n }\n\n hide() {\n if (!this._isShown) {\n return\n }\n\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)\n\n if (hideEvent.defaultPrevented) {\n return\n }\n\n this._element.classList.add(CLASS_NAME_TOGGLING)\n EventHandler.off(document, EVENT_FOCUSIN)\n this._element.blur()\n this._isShown = false\n this._element.classList.remove(CLASS_NAME_SHOW)\n\n const completeCallback = () => {\n this._element.setAttribute('aria-hidden', true)\n this._element.removeAttribute('aria-modal')\n this._element.removeAttribute('role')\n this._element.style.visibility = 'hidden'\n\n if (this._config.backdrop) {\n document.body.classList.remove(CLASS_NAME_BACKDROP_BODY)\n }\n\n if (!this._config.scroll) {\n scrollBarReset()\n }\n\n EventHandler.trigger(this._element, EVENT_HIDDEN)\n this._element.classList.remove(CLASS_NAME_TOGGLING)\n }\n\n setTimeout(completeCallback, getTransitionDurationFromElement(this._element))\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...Manipulator.getDataAttributes(this._element),\n ...(typeof config === 'object' ? config : {})\n }\n typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _enforceFocusOnElement(element) {\n EventHandler.off(document, EVENT_FOCUSIN) // guard against infinite focus loop\n EventHandler.on(document, EVENT_FOCUSIN, event => {\n if (document !== event.target &&\n element !== event.target &&\n !element.contains(event.target)) {\n element.focus()\n }\n })\n element.focus()\n }\n\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_CLICK_DISMISS, SELECTOR_DATA_DISMISS, () => this.hide())\n\n EventHandler.on(document, 'keydown', event => {\n if (this._config.keyboard && event.key === ESCAPE_KEY) {\n this.hide()\n }\n })\n\n EventHandler.on(document, EVENT_CLICK_DATA_API, event => {\n const target = SelectorEngine.findOne(getSelectorFromElement(event.target))\n if (!this._element.contains(event.target) && target !== this._element) {\n this.hide()\n }\n })\n }\n\n // Static\n\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Data.get(this, DATA_KEY) || new Offcanvas(this, typeof config === 'object' ? config : {})\n\n if (typeof config !== 'string') {\n return\n }\n\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config](this)\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n const target = getElementFromSelector(this)\n\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault()\n }\n\n if (isDisabled(this)) {\n return\n }\n\n EventHandler.one(target, EVENT_HIDDEN, () => {\n // focus on trigger when it is closed\n if (isVisible(this)) {\n this.focus()\n }\n })\n\n // avoid conflict when clicking a toggler of an offcanvas, while another is open\n const allReadyOpen = SelectorEngine.findOne(ACTIVE_SELECTOR)\n if (allReadyOpen && allReadyOpen !== target) {\n return\n }\n\n const data = Data.get(target, DATA_KEY) || new Offcanvas(target)\n\n data.toggle(this)\n})\n\nEventHandler.on(window, EVENT_LOAD_DATA_API, () => {\n SelectorEngine.find(OPEN_SELECTOR).forEach(el => (Data.get(el, DATA_KEY) || new Offcanvas(el)).show())\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\ndefineJQueryPlugin(NAME, Offcanvas)\n\nexport default Offcanvas\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.0.0-beta3): util/sanitizer.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst uriAttrs = new Set([\n 'background',\n 'cite',\n 'href',\n 'itemtype',\n 'longdesc',\n 'poster',\n 'src',\n 'xlink:href'\n])\n\nconst ARIA_ATTRIBUTE_PATTERN = /^aria-[\\w-]*$/i\n\n/**\n * A pattern that recognizes a commonly useful subset of URLs that are safe.\n *\n * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts\n */\nconst SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^#&/:?]*(?:[#/?]|$))/i\n\n/**\n * A pattern that matches safe data URLs. Only matches image, video and audio types.\n *\n * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts\n */\nconst DATA_URL_PATTERN = /^data:(?:image\\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\\/(?:mpeg|mp4|ogg|webm)|audio\\/(?:mp3|oga|ogg|opus));base64,[\\d+/a-z]+=*$/i\n\nconst allowedAttribute = (attr, allowedAttributeList) => {\n const attrName = attr.nodeName.toLowerCase()\n\n if (allowedAttributeList.includes(attrName)) {\n if (uriAttrs.has(attrName)) {\n return Boolean(SAFE_URL_PATTERN.test(attr.nodeValue) || DATA_URL_PATTERN.test(attr.nodeValue))\n }\n\n return true\n }\n\n const regExp = allowedAttributeList.filter(attrRegex => attrRegex instanceof RegExp)\n\n // Check if a regular expression validates the attribute.\n for (let i = 0, len = regExp.length; i < len; i++) {\n if (regExp[i].test(attrName)) {\n return true\n }\n }\n\n return false\n}\n\nexport const DefaultAllowlist = {\n // Global attributes allowed on any supplied element below.\n '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],\n a: ['target', 'href', 'title', 'rel'],\n area: [],\n b: [],\n br: [],\n col: [],\n code: [],\n div: [],\n em: [],\n hr: [],\n h1: [],\n h2: [],\n h3: [],\n h4: [],\n h5: [],\n h6: [],\n i: [],\n img: ['src', 'srcset', 'alt', 'title', 'width', 'height'],\n li: [],\n ol: [],\n p: [],\n pre: [],\n s: [],\n small: [],\n span: [],\n sub: [],\n sup: [],\n strong: [],\n u: [],\n ul: []\n}\n\nexport function sanitizeHtml(unsafeHtml, allowList, sanitizeFn) {\n if (!unsafeHtml.length) {\n return unsafeHtml\n }\n\n if (sanitizeFn && typeof sanitizeFn === 'function') {\n return sanitizeFn(unsafeHtml)\n }\n\n const domParser = new window.DOMParser()\n const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html')\n const allowlistKeys = Object.keys(allowList)\n const elements = [].concat(...createdDocument.body.querySelectorAll('*'))\n\n for (let i = 0, len = elements.length; i < len; i++) {\n const el = elements[i]\n const elName = el.nodeName.toLowerCase()\n\n if (!allowlistKeys.includes(elName)) {\n el.parentNode.removeChild(el)\n\n continue\n }\n\n const attributeList = [].concat(...el.attributes)\n const allowedAttributes = [].concat(allowList['*'] || [], allowList[elName] || [])\n\n attributeList.forEach(attr => {\n if (!allowedAttribute(attr, allowedAttributes)) {\n el.removeAttribute(attr.nodeName)\n }\n })\n }\n\n return createdDocument.body.innerHTML\n}\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.0.0-beta3): tooltip.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport * as Popper from '@popperjs/core'\n\nimport {\n defineJQueryPlugin,\n emulateTransitionEnd,\n findShadowRoot,\n getTransitionDurationFromElement,\n getUID,\n isElement,\n isRTL,\n noop,\n typeCheckConfig\n} from './util/index'\nimport {\n DefaultAllowlist,\n sanitizeHtml\n} from './util/sanitizer'\nimport Data from './dom/data'\nimport EventHandler from './dom/event-handler'\nimport Manipulator from './dom/manipulator'\nimport SelectorEngine from './dom/selector-engine'\nimport BaseComponent from './base-component'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'tooltip'\nconst DATA_KEY = 'bs.tooltip'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst CLASS_PREFIX = 'bs-tooltip'\nconst BSCLS_PREFIX_REGEX = new RegExp(`(^|\\\\s)${CLASS_PREFIX}\\\\S+`, 'g')\nconst DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn'])\n\nconst DefaultType = {\n animation: 'boolean',\n template: 'string',\n title: '(string|element|function)',\n trigger: 'string',\n delay: '(number|object)',\n html: 'boolean',\n selector: '(string|boolean)',\n placement: '(string|function)',\n offset: '(array|string|function)',\n container: '(string|element|boolean)',\n fallbackPlacements: 'array',\n boundary: '(string|element)',\n customClass: '(string|function)',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n allowList: 'object',\n popperConfig: '(null|object|function)'\n}\n\nconst AttachmentMap = {\n AUTO: 'auto',\n TOP: 'top',\n RIGHT: isRTL() ? 'left' : 'right',\n BOTTOM: 'bottom',\n LEFT: isRTL() ? 'right' : 'left'\n}\n\nconst Default = {\n animation: true,\n template: '
' +\n '
' +\n '
' +\n '
',\n trigger: 'hover focus',\n title: '',\n delay: 0,\n html: false,\n selector: false,\n placement: 'top',\n offset: [0, 0],\n container: false,\n fallbackPlacements: ['top', 'right', 'bottom', 'left'],\n boundary: 'clippingParents',\n customClass: '',\n sanitize: true,\n sanitizeFn: null,\n allowList: DefaultAllowlist,\n popperConfig: null\n}\n\nconst Event = {\n HIDE: `hide${EVENT_KEY}`,\n HIDDEN: `hidden${EVENT_KEY}`,\n SHOW: `show${EVENT_KEY}`,\n SHOWN: `shown${EVENT_KEY}`,\n INSERTED: `inserted${EVENT_KEY}`,\n CLICK: `click${EVENT_KEY}`,\n FOCUSIN: `focusin${EVENT_KEY}`,\n FOCUSOUT: `focusout${EVENT_KEY}`,\n MOUSEENTER: `mouseenter${EVENT_KEY}`,\n MOUSELEAVE: `mouseleave${EVENT_KEY}`\n}\n\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_MODAL = 'modal'\nconst CLASS_NAME_SHOW = 'show'\n\nconst HOVER_STATE_SHOW = 'show'\nconst HOVER_STATE_OUT = 'out'\n\nconst SELECTOR_TOOLTIP_INNER = '.tooltip-inner'\n\nconst TRIGGER_HOVER = 'hover'\nconst TRIGGER_FOCUS = 'focus'\nconst TRIGGER_CLICK = 'click'\nconst TRIGGER_MANUAL = 'manual'\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Tooltip extends BaseComponent {\n constructor(element, config) {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s tooltips require Popper (https://popper.js.org)')\n }\n\n super(element)\n\n // private\n this._isEnabled = true\n this._timeout = 0\n this._hoverState = ''\n this._activeTrigger = {}\n this._popper = null\n\n // Protected\n this.config = this._getConfig(config)\n this.tip = null\n\n this._setListeners()\n }\n\n // Getters\n\n static get Default() {\n return Default\n }\n\n static get NAME() {\n return NAME\n }\n\n static get DATA_KEY() {\n return DATA_KEY\n }\n\n static get Event() {\n return Event\n }\n\n static get EVENT_KEY() {\n return EVENT_KEY\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n // Public\n\n enable() {\n this._isEnabled = true\n }\n\n disable() {\n this._isEnabled = false\n }\n\n toggleEnabled() {\n this._isEnabled = !this._isEnabled\n }\n\n toggle(event) {\n if (!this._isEnabled) {\n return\n }\n\n if (event) {\n const context = this._initializeOnDelegatedTarget(event)\n\n context._activeTrigger.click = !context._activeTrigger.click\n\n if (context._isWithActiveTrigger()) {\n context._enter(null, context)\n } else {\n context._leave(null, context)\n }\n } else {\n if (this.getTipElement().classList.contains(CLASS_NAME_SHOW)) {\n this._leave(null, this)\n return\n }\n\n this._enter(null, this)\n }\n }\n\n dispose() {\n clearTimeout(this._timeout)\n\n EventHandler.off(this._element, this.constructor.EVENT_KEY)\n EventHandler.off(this._element.closest(`.${CLASS_NAME_MODAL}`), 'hide.bs.modal', this._hideModalHandler)\n\n if (this.tip && this.tip.parentNode) {\n this.tip.parentNode.removeChild(this.tip)\n }\n\n this._isEnabled = null\n this._timeout = null\n this._hoverState = null\n this._activeTrigger = null\n if (this._popper) {\n this._popper.destroy()\n }\n\n this._popper = null\n this.config = null\n this.tip = null\n super.dispose()\n }\n\n show() {\n if (this._element.style.display === 'none') {\n throw new Error('Please use show on visible elements')\n }\n\n if (!(this.isWithContent() && this._isEnabled)) {\n return\n }\n\n const showEvent = EventHandler.trigger(this._element, this.constructor.Event.SHOW)\n const shadowRoot = findShadowRoot(this._element)\n const isInTheDom = shadowRoot === null ?\n this._element.ownerDocument.documentElement.contains(this._element) :\n shadowRoot.contains(this._element)\n\n if (showEvent.defaultPrevented || !isInTheDom) {\n return\n }\n\n const tip = this.getTipElement()\n const tipId = getUID(this.constructor.NAME)\n\n tip.setAttribute('id', tipId)\n this._element.setAttribute('aria-describedby', tipId)\n\n this.setContent()\n\n if (this.config.animation) {\n tip.classList.add(CLASS_NAME_FADE)\n }\n\n const placement = typeof this.config.placement === 'function' ?\n this.config.placement.call(this, tip, this._element) :\n this.config.placement\n\n const attachment = this._getAttachment(placement)\n this._addAttachmentClass(attachment)\n\n const container = this._getContainer()\n Data.set(tip, this.constructor.DATA_KEY, this)\n\n if (!this._element.ownerDocument.documentElement.contains(this.tip)) {\n container.appendChild(tip)\n EventHandler.trigger(this._element, this.constructor.Event.INSERTED)\n }\n\n if (this._popper) {\n this._popper.update()\n } else {\n this._popper = Popper.createPopper(this._element, tip, this._getPopperConfig(attachment))\n }\n\n tip.classList.add(CLASS_NAME_SHOW)\n\n const customClass = typeof this.config.customClass === 'function' ? this.config.customClass() : this.config.customClass\n if (customClass) {\n tip.classList.add(...customClass.split(' '))\n }\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement) {\n [].concat(...document.body.children).forEach(element => {\n EventHandler.on(element, 'mouseover', noop())\n })\n }\n\n const complete = () => {\n const prevHoverState = this._hoverState\n\n this._hoverState = null\n EventHandler.trigger(this._element, this.constructor.Event.SHOWN)\n\n if (prevHoverState === HOVER_STATE_OUT) {\n this._leave(null, this)\n }\n }\n\n if (this.tip.classList.contains(CLASS_NAME_FADE)) {\n const transitionDuration = getTransitionDurationFromElement(this.tip)\n EventHandler.one(this.tip, 'transitionend', complete)\n emulateTransitionEnd(this.tip, transitionDuration)\n } else {\n complete()\n }\n }\n\n hide() {\n if (!this._popper) {\n return\n }\n\n const tip = this.getTipElement()\n const complete = () => {\n if (this._isWithActiveTrigger()) {\n return\n }\n\n if (this._hoverState !== HOVER_STATE_SHOW && tip.parentNode) {\n tip.parentNode.removeChild(tip)\n }\n\n this._cleanTipClass()\n this._element.removeAttribute('aria-describedby')\n EventHandler.trigger(this._element, this.constructor.Event.HIDDEN)\n\n if (this._popper) {\n this._popper.destroy()\n this._popper = null\n }\n }\n\n const hideEvent = EventHandler.trigger(this._element, this.constructor.Event.HIDE)\n if (hideEvent.defaultPrevented) {\n return\n }\n\n tip.classList.remove(CLASS_NAME_SHOW)\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n [].concat(...document.body.children)\n .forEach(element => EventHandler.off(element, 'mouseover', noop))\n }\n\n this._activeTrigger[TRIGGER_CLICK] = false\n this._activeTrigger[TRIGGER_FOCUS] = false\n this._activeTrigger[TRIGGER_HOVER] = false\n\n if (this.tip.classList.contains(CLASS_NAME_FADE)) {\n const transitionDuration = getTransitionDurationFromElement(tip)\n\n EventHandler.one(tip, 'transitionend', complete)\n emulateTransitionEnd(tip, transitionDuration)\n } else {\n complete()\n }\n\n this._hoverState = ''\n }\n\n update() {\n if (this._popper !== null) {\n this._popper.update()\n }\n }\n\n // Protected\n\n isWithContent() {\n return Boolean(this.getTitle())\n }\n\n getTipElement() {\n if (this.tip) {\n return this.tip\n }\n\n const element = document.createElement('div')\n element.innerHTML = this.config.template\n\n this.tip = element.children[0]\n return this.tip\n }\n\n setContent() {\n const tip = this.getTipElement()\n this.setElementContent(SelectorEngine.findOne(SELECTOR_TOOLTIP_INNER, tip), this.getTitle())\n tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW)\n }\n\n setElementContent(element, content) {\n if (element === null) {\n return\n }\n\n if (typeof content === 'object' && isElement(content)) {\n if (content.jquery) {\n content = content[0]\n }\n\n // content is a DOM node or a jQuery\n if (this.config.html) {\n if (content.parentNode !== element) {\n element.innerHTML = ''\n element.appendChild(content)\n }\n } else {\n element.textContent = content.textContent\n }\n\n return\n }\n\n if (this.config.html) {\n if (this.config.sanitize) {\n content = sanitizeHtml(content, this.config.allowList, this.config.sanitizeFn)\n }\n\n element.innerHTML = content\n } else {\n element.textContent = content\n }\n }\n\n getTitle() {\n let title = this._element.getAttribute('data-bs-original-title')\n\n if (!title) {\n title = typeof this.config.title === 'function' ?\n this.config.title.call(this._element) :\n this.config.title\n }\n\n return title\n }\n\n updateAttachment(attachment) {\n if (attachment === 'right') {\n return 'end'\n }\n\n if (attachment === 'left') {\n return 'start'\n }\n\n return attachment\n }\n\n // Private\n\n _initializeOnDelegatedTarget(event, context) {\n const dataKey = this.constructor.DATA_KEY\n context = context || Data.get(event.delegateTarget, dataKey)\n\n if (!context) {\n context = new this.constructor(event.delegateTarget, this._getDelegateConfig())\n Data.set(event.delegateTarget, dataKey, context)\n }\n\n return context\n }\n\n _getOffset() {\n const { offset } = this.config\n\n if (typeof offset === 'string') {\n return offset.split(',').map(val => Number.parseInt(val, 10))\n }\n\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element)\n }\n\n return offset\n }\n\n _getPopperConfig(attachment) {\n const defaultBsPopperConfig = {\n placement: attachment,\n modifiers: [\n {\n name: 'flip',\n options: {\n altBoundary: true,\n fallbackPlacements: this.config.fallbackPlacements\n }\n },\n {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n },\n {\n name: 'preventOverflow',\n options: {\n boundary: this.config.boundary\n }\n },\n {\n name: 'arrow',\n options: {\n element: `.${this.constructor.NAME}-arrow`\n }\n },\n {\n name: 'onChange',\n enabled: true,\n phase: 'afterWrite',\n fn: data => this._handlePopperPlacementChange(data)\n }\n ],\n onFirstUpdate: data => {\n if (data.options.placement !== data.placement) {\n this._handlePopperPlacementChange(data)\n }\n }\n }\n\n return {\n ...defaultBsPopperConfig,\n ...(typeof this.config.popperConfig === 'function' ? this.config.popperConfig(defaultBsPopperConfig) : this.config.popperConfig)\n }\n }\n\n _addAttachmentClass(attachment) {\n this.getTipElement().classList.add(`${CLASS_PREFIX}-${this.updateAttachment(attachment)}`)\n }\n\n _getContainer() {\n if (this.config.container === false) {\n return document.body\n }\n\n if (isElement(this.config.container)) {\n return this.config.container\n }\n\n return SelectorEngine.findOne(this.config.container)\n }\n\n _getAttachment(placement) {\n return AttachmentMap[placement.toUpperCase()]\n }\n\n _setListeners() {\n const triggers = this.config.trigger.split(' ')\n\n triggers.forEach(trigger => {\n if (trigger === 'click') {\n EventHandler.on(this._element, this.constructor.Event.CLICK, this.config.selector, event => this.toggle(event))\n } else if (trigger !== TRIGGER_MANUAL) {\n const eventIn = trigger === TRIGGER_HOVER ?\n this.constructor.Event.MOUSEENTER :\n this.constructor.Event.FOCUSIN\n const eventOut = trigger === TRIGGER_HOVER ?\n this.constructor.Event.MOUSELEAVE :\n this.constructor.Event.FOCUSOUT\n\n EventHandler.on(this._element, eventIn, this.config.selector, event => this._enter(event))\n EventHandler.on(this._element, eventOut, this.config.selector, event => this._leave(event))\n }\n })\n\n this._hideModalHandler = () => {\n if (this._element) {\n this.hide()\n }\n }\n\n EventHandler.on(this._element.closest(`.${CLASS_NAME_MODAL}`), 'hide.bs.modal', this._hideModalHandler)\n\n if (this.config.selector) {\n this.config = {\n ...this.config,\n trigger: 'manual',\n selector: ''\n }\n } else {\n this._fixTitle()\n }\n }\n\n _fixTitle() {\n const title = this._element.getAttribute('title')\n const originalTitleType = typeof this._element.getAttribute('data-bs-original-title')\n\n if (title || originalTitleType !== 'string') {\n this._element.setAttribute('data-bs-original-title', title || '')\n if (title && !this._element.getAttribute('aria-label') && !this._element.textContent) {\n this._element.setAttribute('aria-label', title)\n }\n\n this._element.setAttribute('title', '')\n }\n }\n\n _enter(event, context) {\n context = this._initializeOnDelegatedTarget(event, context)\n\n if (event) {\n context._activeTrigger[\n event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER\n ] = true\n }\n\n if (context.getTipElement().classList.contains(CLASS_NAME_SHOW) || context._hoverState === HOVER_STATE_SHOW) {\n context._hoverState = HOVER_STATE_SHOW\n return\n }\n\n clearTimeout(context._timeout)\n\n context._hoverState = HOVER_STATE_SHOW\n\n if (!context.config.delay || !context.config.delay.show) {\n context.show()\n return\n }\n\n context._timeout = setTimeout(() => {\n if (context._hoverState === HOVER_STATE_SHOW) {\n context.show()\n }\n }, context.config.delay.show)\n }\n\n _leave(event, context) {\n context = this._initializeOnDelegatedTarget(event, context)\n\n if (event) {\n context._activeTrigger[\n event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER\n ] = context._element.contains(event.relatedTarget)\n }\n\n if (context._isWithActiveTrigger()) {\n return\n }\n\n clearTimeout(context._timeout)\n\n context._hoverState = HOVER_STATE_OUT\n\n if (!context.config.delay || !context.config.delay.hide) {\n context.hide()\n return\n }\n\n context._timeout = setTimeout(() => {\n if (context._hoverState === HOVER_STATE_OUT) {\n context.hide()\n }\n }, context.config.delay.hide)\n }\n\n _isWithActiveTrigger() {\n for (const trigger in this._activeTrigger) {\n if (this._activeTrigger[trigger]) {\n return true\n }\n }\n\n return false\n }\n\n _getConfig(config) {\n const dataAttributes = Manipulator.getDataAttributes(this._element)\n\n Object.keys(dataAttributes).forEach(dataAttr => {\n if (DISALLOWED_ATTRIBUTES.has(dataAttr)) {\n delete dataAttributes[dataAttr]\n }\n })\n\n if (config && typeof config.container === 'object' && config.container.jquery) {\n config.container = config.container[0]\n }\n\n config = {\n ...this.constructor.Default,\n ...dataAttributes,\n ...(typeof config === 'object' && config ? config : {})\n }\n\n if (typeof config.delay === 'number') {\n config.delay = {\n show: config.delay,\n hide: config.delay\n }\n }\n\n if (typeof config.title === 'number') {\n config.title = config.title.toString()\n }\n\n if (typeof config.content === 'number') {\n config.content = config.content.toString()\n }\n\n typeCheckConfig(NAME, config, this.constructor.DefaultType)\n\n if (config.sanitize) {\n config.template = sanitizeHtml(config.template, config.allowList, config.sanitizeFn)\n }\n\n return config\n }\n\n _getDelegateConfig() {\n const config = {}\n\n if (this.config) {\n for (const key in this.config) {\n if (this.constructor.Default[key] !== this.config[key]) {\n config[key] = this.config[key]\n }\n }\n }\n\n return config\n }\n\n _cleanTipClass() {\n const tip = this.getTipElement()\n const tabClass = tip.getAttribute('class').match(BSCLS_PREFIX_REGEX)\n if (tabClass !== null && tabClass.length > 0) {\n tabClass.map(token => token.trim())\n .forEach(tClass => tip.classList.remove(tClass))\n }\n }\n\n _handlePopperPlacementChange(popperData) {\n const { state } = popperData\n\n if (!state) {\n return\n }\n\n this.tip = state.elements.popper\n this._cleanTipClass()\n this._addAttachmentClass(this._getAttachment(state.placement))\n }\n\n // Static\n\n static jQueryInterface(config) {\n return this.each(function () {\n let data = Data.get(this, DATA_KEY)\n const _config = typeof config === 'object' && config\n\n if (!data && /dispose|hide/.test(config)) {\n return\n }\n\n if (!data) {\n data = new Tooltip(this, _config)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n * add .Tooltip to jQuery only if jQuery is present\n */\n\ndefineJQueryPlugin(NAME, Tooltip)\n\nexport default Tooltip\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.0.0-beta3): popover.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport { defineJQueryPlugin } from './util/index'\nimport Data from './dom/data'\nimport SelectorEngine from './dom/selector-engine'\nimport Tooltip from './tooltip'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'popover'\nconst DATA_KEY = 'bs.popover'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst CLASS_PREFIX = 'bs-popover'\nconst BSCLS_PREFIX_REGEX = new RegExp(`(^|\\\\s)${CLASS_PREFIX}\\\\S+`, 'g')\n\nconst Default = {\n ...Tooltip.Default,\n placement: 'right',\n offset: [0, 8],\n trigger: 'click',\n content: '',\n template: '
' +\n '
' +\n '

' +\n '
' +\n '
'\n}\n\nconst DefaultType = {\n ...Tooltip.DefaultType,\n content: '(string|element|function)'\n}\n\nconst Event = {\n HIDE: `hide${EVENT_KEY}`,\n HIDDEN: `hidden${EVENT_KEY}`,\n SHOW: `show${EVENT_KEY}`,\n SHOWN: `shown${EVENT_KEY}`,\n INSERTED: `inserted${EVENT_KEY}`,\n CLICK: `click${EVENT_KEY}`,\n FOCUSIN: `focusin${EVENT_KEY}`,\n FOCUSOUT: `focusout${EVENT_KEY}`,\n MOUSEENTER: `mouseenter${EVENT_KEY}`,\n MOUSELEAVE: `mouseleave${EVENT_KEY}`\n}\n\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_SHOW = 'show'\n\nconst SELECTOR_TITLE = '.popover-header'\nconst SELECTOR_CONTENT = '.popover-body'\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Popover extends Tooltip {\n // Getters\n\n static get Default() {\n return Default\n }\n\n static get NAME() {\n return NAME\n }\n\n static get DATA_KEY() {\n return DATA_KEY\n }\n\n static get Event() {\n return Event\n }\n\n static get EVENT_KEY() {\n return EVENT_KEY\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n // Overrides\n\n isWithContent() {\n return this.getTitle() || this._getContent()\n }\n\n setContent() {\n const tip = this.getTipElement()\n\n // we use append for html objects to maintain js events\n this.setElementContent(SelectorEngine.findOne(SELECTOR_TITLE, tip), this.getTitle())\n let content = this._getContent()\n if (typeof content === 'function') {\n content = content.call(this._element)\n }\n\n this.setElementContent(SelectorEngine.findOne(SELECTOR_CONTENT, tip), content)\n\n tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW)\n }\n\n // Private\n\n _addAttachmentClass(attachment) {\n this.getTipElement().classList.add(`${CLASS_PREFIX}-${this.updateAttachment(attachment)}`)\n }\n\n _getContent() {\n return this._element.getAttribute('data-bs-content') || this.config.content\n }\n\n _cleanTipClass() {\n const tip = this.getTipElement()\n const tabClass = tip.getAttribute('class').match(BSCLS_PREFIX_REGEX)\n if (tabClass !== null && tabClass.length > 0) {\n tabClass.map(token => token.trim())\n .forEach(tClass => tip.classList.remove(tClass))\n }\n }\n\n // Static\n\n static jQueryInterface(config) {\n return this.each(function () {\n let data = Data.get(this, DATA_KEY)\n const _config = typeof config === 'object' ? config : null\n\n if (!data && /dispose|hide/.test(config)) {\n return\n }\n\n if (!data) {\n data = new Popover(this, _config)\n Data.set(this, DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n * add .Popover to jQuery only if jQuery is present\n */\n\ndefineJQueryPlugin(NAME, Popover)\n\nexport default Popover\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.0.0-beta3): scrollspy.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport {\n defineJQueryPlugin,\n getSelectorFromElement,\n getUID,\n isElement,\n typeCheckConfig\n} from './util/index'\nimport Data from './dom/data'\nimport EventHandler from './dom/event-handler'\nimport Manipulator from './dom/manipulator'\nimport SelectorEngine from './dom/selector-engine'\nimport BaseComponent from './base-component'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'scrollspy'\nconst DATA_KEY = 'bs.scrollspy'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst Default = {\n offset: 10,\n method: 'auto',\n target: ''\n}\n\nconst DefaultType = {\n offset: 'number',\n method: 'string',\n target: '(string|element)'\n}\n\nconst EVENT_ACTIVATE = `activate${EVENT_KEY}`\nconst EVENT_SCROLL = `scroll${EVENT_KEY}`\nconst EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item'\nconst CLASS_NAME_ACTIVE = 'active'\n\nconst SELECTOR_DATA_SPY = '[data-bs-spy=\"scroll\"]'\nconst SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'\nconst SELECTOR_NAV_LINKS = '.nav-link'\nconst SELECTOR_NAV_ITEMS = '.nav-item'\nconst SELECTOR_LIST_ITEMS = '.list-group-item'\nconst SELECTOR_DROPDOWN = '.dropdown'\nconst SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'\n\nconst METHOD_OFFSET = 'offset'\nconst METHOD_POSITION = 'position'\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass ScrollSpy extends BaseComponent {\n constructor(element, config) {\n super(element)\n this._scrollElement = this._element.tagName === 'BODY' ? window : this._element\n this._config = this._getConfig(config)\n this._selector = `${this._config.target} ${SELECTOR_NAV_LINKS}, ${this._config.target} ${SELECTOR_LIST_ITEMS}, ${this._config.target} .${CLASS_NAME_DROPDOWN_ITEM}`\n this._offsets = []\n this._targets = []\n this._activeTarget = null\n this._scrollHeight = 0\n\n EventHandler.on(this._scrollElement, EVENT_SCROLL, () => this._process())\n\n this.refresh()\n this._process()\n }\n\n // Getters\n\n static get Default() {\n return Default\n }\n\n static get DATA_KEY() {\n return DATA_KEY\n }\n\n // Public\n\n refresh() {\n const autoMethod = this._scrollElement === this._scrollElement.window ?\n METHOD_OFFSET :\n METHOD_POSITION\n\n const offsetMethod = this._config.method === 'auto' ?\n autoMethod :\n this._config.method\n\n const offsetBase = offsetMethod === METHOD_POSITION ?\n this._getScrollTop() :\n 0\n\n this._offsets = []\n this._targets = []\n this._scrollHeight = this._getScrollHeight()\n\n const targets = SelectorEngine.find(this._selector)\n\n targets.map(element => {\n const targetSelector = getSelectorFromElement(element)\n const target = targetSelector ? SelectorEngine.findOne(targetSelector) : null\n\n if (target) {\n const targetBCR = target.getBoundingClientRect()\n if (targetBCR.width || targetBCR.height) {\n return [\n Manipulator[offsetMethod](target).top + offsetBase,\n targetSelector\n ]\n }\n }\n\n return null\n })\n .filter(item => item)\n .sort((a, b) => a[0] - b[0])\n .forEach(item => {\n this._offsets.push(item[0])\n this._targets.push(item[1])\n })\n }\n\n dispose() {\n super.dispose()\n EventHandler.off(this._scrollElement, EVENT_KEY)\n\n this._scrollElement = null\n this._config = null\n this._selector = null\n this._offsets = null\n this._targets = null\n this._activeTarget = null\n this._scrollHeight = null\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...(typeof config === 'object' && config ? config : {})\n }\n\n if (typeof config.target !== 'string' && isElement(config.target)) {\n let { id } = config.target\n if (!id) {\n id = getUID(NAME)\n config.target.id = id\n }\n\n config.target = `#${id}`\n }\n\n typeCheckConfig(NAME, config, DefaultType)\n\n return config\n }\n\n _getScrollTop() {\n return this._scrollElement === window ?\n this._scrollElement.pageYOffset :\n this._scrollElement.scrollTop\n }\n\n _getScrollHeight() {\n return this._scrollElement.scrollHeight || Math.max(\n document.body.scrollHeight,\n document.documentElement.scrollHeight\n )\n }\n\n _getOffsetHeight() {\n return this._scrollElement === window ?\n window.innerHeight :\n this._scrollElement.getBoundingClientRect().height\n }\n\n _process() {\n const scrollTop = this._getScrollTop() + this._config.offset\n const scrollHeight = this._getScrollHeight()\n const maxScroll = this._config.offset + scrollHeight - this._getOffsetHeight()\n\n if (this._scrollHeight !== scrollHeight) {\n this.refresh()\n }\n\n if (scrollTop >= maxScroll) {\n const target = this._targets[this._targets.length - 1]\n\n if (this._activeTarget !== target) {\n this._activate(target)\n }\n\n return\n }\n\n if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) {\n this._activeTarget = null\n this._clear()\n return\n }\n\n for (let i = this._offsets.length; i--;) {\n const isActiveTarget = this._activeTarget !== this._targets[i] &&\n scrollTop >= this._offsets[i] &&\n (typeof this._offsets[i + 1] === 'undefined' || scrollTop < this._offsets[i + 1])\n\n if (isActiveTarget) {\n this._activate(this._targets[i])\n }\n }\n }\n\n _activate(target) {\n this._activeTarget = target\n\n this._clear()\n\n const queries = this._selector.split(',')\n .map(selector => `${selector}[data-bs-target=\"${target}\"],${selector}[href=\"${target}\"]`)\n\n const link = SelectorEngine.findOne(queries.join(','))\n\n if (link.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {\n SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE, link.closest(SELECTOR_DROPDOWN))\n .classList.add(CLASS_NAME_ACTIVE)\n\n link.classList.add(CLASS_NAME_ACTIVE)\n } else {\n // Set triggered link as active\n link.classList.add(CLASS_NAME_ACTIVE)\n\n SelectorEngine.parents(link, SELECTOR_NAV_LIST_GROUP)\n .forEach(listGroup => {\n // Set triggered links parents as active\n // With both
\n').replace(/(^|\n)\s*/g,""),Be=()=>{const e=m();return!!e&&(e.remove(),C([document.documentElement,document.body],[p["no-backdrop"],p["toast-shown"],p["has-column"]]),!0)},E=()=>{x.currentInstance.resetValidationMessage()},Pe=()=>{const e=g(),t=A(e,p.input),n=A(e,p.file),o=e.querySelector(".".concat(p.range," input")),i=e.querySelector(".".concat(p.range," output")),r=A(e,p.select),a=e.querySelector(".".concat(p.checkbox," input")),s=A(e,p.textarea);t.oninput=E,n.onchange=E,r.onchange=E,a.onchange=E,s.oninput=E,o.oninput=()=>{E(),i.value=o.value},o.onchange=()=>{E(),i.value=o.value}},xe=e=>"string"==typeof e?document.querySelector(e):e,Ee=e=>{const t=g();t.setAttribute("role",e.toast?"alert":"dialog"),t.setAttribute("aria-live",e.toast?"polite":"assertive"),e.toast||t.setAttribute("aria-modal","true")},Te=e=>{"rtl"===window.getComputedStyle(e).direction&&w(m(),p.rtl)},Le=(e,t)=>{if(e instanceof HTMLElement)t.appendChild(e);else if("object"==typeof e){var n=e,o=t;if(n.jquery)Se(o,n);else y(o,n.toString())}else e&&y(t,e)},Se=(t,n)=>{if(t.textContent="",0 in n)for(let e=0;e in n;e++)t.appendChild(n[e].cloneNode(!0));else t.appendChild(n.cloneNode(!0))},Oe=(()=>{if(ve())return!1;var e=document.createElement("div"),t={WebkitAnimation:"webkitAnimationEnd",animation:"animationend"};for(const n in t)if(Object.prototype.hasOwnProperty.call(t,n)&&void 0!==e.style[n])return t[n];return!1})(),Me=(e,t)=>{var n,o,i,r,a,s=ne(),c=d();(t.showConfirmButton||t.showDenyButton||t.showCancelButton?k:B)(s),v(s,t,"actions"),s=s,n=c,o=t,i=h(),r=f(),a=b(),je(i,"confirm",o),je(r,"deny",o),je(a,"cancel",o),function(e,t,n,o){if(!o.buttonsStyling)return C([e,t,n],p.styled);w([e,t,n],p.styled),o.confirmButtonColor&&(e.style.backgroundColor=o.confirmButtonColor,w(e,p["default-outline"]));o.denyButtonColor&&(t.style.backgroundColor=o.denyButtonColor,w(t,p["default-outline"]));o.cancelButtonColor&&(n.style.backgroundColor=o.cancelButtonColor,w(n,p["default-outline"]))}(i,r,a,o),o.reverseButtons&&(o.toast?(s.insertBefore(a,i),s.insertBefore(r,i)):(s.insertBefore(a,n),s.insertBefore(r,n),s.insertBefore(i,n))),y(c,t.loaderHtml),v(c,t,"loader")};function je(e,t,n){he(e,n["show".concat(q(t),"Button")],"inline-block"),y(e,n["".concat(t,"ButtonText")]),e.setAttribute("aria-label",n["".concat(t,"ButtonAriaLabel")]),e.className=p[t],v(e,n,"".concat(t,"Button")),w(e,n["".concat(t,"ButtonClass")])}const He=(e,t)=>{var n,o,i=m();i&&(o=i,"string"==typeof(n=t.backdrop)?o.style.background=n:n||w([document.documentElement,document.body],p["no-backdrop"]),o=i,(n=t.position)in p?w(o,p[n]):(r('The "position" parameter is not valid, defaulting to "center"'),w(o,p.center)),n=i,(o=t.grow)&&"string"==typeof o&&(o="grow-".concat(o))in p&&w(n,p[o]),v(i,t,"container"))};var T={awaitingPromise:new WeakMap,promise:new WeakMap,innerParams:new WeakMap,domCache:new WeakMap};const Ie=["input","file","range","select","radio","checkbox","textarea"],De=(e,a)=>{const s=g();var t,e=T.innerParams.get(e);const c=!e||a.input!==e.input;Ie.forEach(e=>{const t=A(s,p[e]);{var n=e,o=a.inputAttributes;const i=de(g(),n);if(i){qe(i);for(const r in o)i.setAttribute(r,o[r])}}t.className=p[e],c&&B(t)}),a.input&&(c&&(e=>{if(!L[e.input])return l('Unexpected type of input! Expected "text", "email", "password", "number", "tel", "select", "radio", "checkbox", "textarea", "file" or "url", got "'.concat(e.input,'"'));const t=Re(e.input),n=L[e.input](t,e);k(t),setTimeout(()=>{pe(n)})})(a),e=a,t=Re(e.input),"object"==typeof e.customClass&&w(t,e.customClass.input))},qe=t=>{for(let e=0;e{e.placeholder&&!t.inputPlaceholder||(e.placeholder=t.inputPlaceholder)},Ne=(e,t,n)=>{if(n.inputLabel){e.id=p.input;const i=document.createElement("label");var o=p["input-label"];i.setAttribute("for",e.id),i.className=o,"object"==typeof n.customClass&&w(i,n.customClass.inputLabel),i.innerText=n.inputLabel,t.insertAdjacentElement("beforebegin",i)}},Re=e=>A(g(),p[e]||p.input),Fe=(e,t)=>{["string","number"].includes(typeof t)?e.value="".concat(t):U(t)||r('Unexpected type of inputValue! Expected "string", "number" or "Promise", got "'.concat(typeof t,'"'))},L={},Ue=(L.text=L.email=L.password=L.number=L.tel=L.url=(e,t)=>(Fe(e,t.inputValue),Ne(e,e,t),Ve(e,t),e.type=t.input,e),L.file=(e,t)=>(Ne(e,e,t),Ve(e,t),e),L.range=(e,t)=>{const n=e.querySelector("input");var o=e.querySelector("output");return Fe(n,t.inputValue),n.type=t.input,Fe(o,t.inputValue),Ne(n,e,t),e},L.select=(e,t)=>{if(e.textContent="",t.inputPlaceholder){const n=document.createElement("option");y(n,t.inputPlaceholder),n.value="",n.disabled=!0,n.selected=!0,e.appendChild(n)}return Ne(e,e,t),e},L.radio=e=>(e.textContent="",e),L.checkbox=(e,t)=>{const n=de(g(),"checkbox");n.value="1",n.id=p.checkbox,n.checked=Boolean(t.inputValue);e=e.querySelector("span");return y(e,t.inputPlaceholder),n},L.textarea=(n,e)=>{Fe(n,e.inputValue),Ve(n,e),Ne(n,n,e);return setTimeout(()=>{if("MutationObserver"in window){const t=parseInt(window.getComputedStyle(g()).width);new MutationObserver(()=>{var e=n.offsetWidth+(e=n,parseInt(window.getComputedStyle(e).marginLeft)+parseInt(window.getComputedStyle(e).marginRight));e>t?g().style.width="".concat(e,"px"):g().style.width=null}).observe(n,{attributes:!0,attributeFilter:["style"]})}}),n},(e,t)=>{const n=G();v(n,t,"htmlContainer"),t.html?(Le(t.html,n),k(n,"block")):t.text?(n.textContent=t.text,k(n,"block")):B(n),De(e,t)}),We=(e,t)=>{var n=oe();he(n,t.footer),t.footer&&Le(t.footer,n),v(n,t,"footer")},ze=(e,t)=>{const n=re();y(n,t.closeButtonHtml),v(n,t,"closeButton"),he(n,t.showCloseButton),n.setAttribute("aria-label",t.closeButtonAriaLabel)},Ke=(e,t)=>{var e=T.innerParams.get(e),n=$();if(e&&t.icon===e.icon)return $e(n,t),void _e(n,t);if(t.icon||t.iconHtml){if(t.icon&&-1===Object.keys(o).indexOf(t.icon))return l('Unknown icon! Expected "success", "error", "warning", "info" or "question", got "'.concat(t.icon,'"')),void B(n);k(n),$e(n,t),_e(n,t),w(n,t.showClass.icon)}else B(n)},_e=(e,t)=>{for(const n in o)t.icon!==n&&C(e,o[n]);w(e,o[t.icon]),Je(e,t),Ye(),v(e,t,"icon")},Ye=()=>{const e=g();var t=window.getComputedStyle(e).getPropertyValue("background-color");const n=e.querySelectorAll("[class^=swal2-success-circular-line], .swal2-success-fix");for(let e=0;e
\n \n
\n
\n',Xe='\n \n \n \n \n',$e=(e,t)=>{let n=e.innerHTML,o;var i;t.iconHtml?o=Ge(t.iconHtml):"success"===t.icon?(o=Ze,n=n.replace(/ style=".*?"/g,"")):o="error"===t.icon?Xe:(i={question:"?",warning:"!",info:"i"},Ge(i[t.icon])),n.trim()!==o.trim()&&y(e,o)},Je=(e,t)=>{if(t.iconColor){e.style.color=t.iconColor,e.style.borderColor=t.iconColor;for(const n of[".swal2-success-line-tip",".swal2-success-line-long",".swal2-x-mark-line-left",".swal2-x-mark-line-right"])ge(e,n,"backgroundColor",t.iconColor);ge(e,".swal2-success-ring","borderColor",t.iconColor)}},Ge=e=>'
').concat(e,"
"),Qe=(e,t)=>{const n=Q();if(!t.imageUrl)return B(n);k(n,""),n.setAttribute("src",t.imageUrl),n.setAttribute("alt",t.imageAlt),c(n,"width",t.imageWidth),c(n,"height",t.imageHeight),n.className=p.image,v(n,t,"image")},et=(e,n)=>{const o=ee();if(!n.progressSteps||0===n.progressSteps.length)return B(o);k(o),o.textContent="",n.currentProgressStep>=n.progressSteps.length&&r("Invalid currentProgressStep parameter, it should be less than progressSteps.length (currentProgressStep like JS arrays starts from 0)"),n.progressSteps.forEach((e,t)=>{var e=(e=>{const t=document.createElement("li");return w(t,p["progress-step"]),y(t,e),t})(e);o.appendChild(e),t===n.currentProgressStep&&w(e,p["active-progress-step"]),t!==n.progressSteps.length-1&&(e=(e=>{const t=document.createElement("li");if(w(t,p["progress-step-line"]),e.progressStepsDistance)c(t,"width",e.progressStepsDistance);return t})(n),o.appendChild(e))})},tt=(e,t)=>{const n=J();he(n,t.title||t.titleText,"block"),t.title&&Le(t.title,n),t.titleText&&(n.innerText=t.titleText),v(n,t,"title")},nt=(e,t)=>{var n=m();const o=g();t.toast?(c(n,"width",t.width),o.style.width="100%",o.insertBefore(d(),$())):c(o,"width",t.width),c(o,"padding",t.padding),t.color&&(o.style.color=t.color),t.background&&(o.style.background=t.background),B(te());n=o;(n.className="".concat(p.popup," ").concat(P(n)?t.showClass.popup:""),t.toast)?(w([document.documentElement,document.body],p["toast-shown"]),w(n,p.toast)):w(n,p.modal);v(n,t,"popup"),"string"==typeof t.customClass&&w(n,t.customClass);t.icon&&w(n,p["icon-".concat(t.icon)])},ot=(e,t)=>{nt(e,t),He(e,t),et(e,t),Ke(e,t),Qe(e,t),tt(e,t),ze(e,t),Ue(e,t),Me(e,t),We(e,t),"function"==typeof t.didRender&&t.didRender(g())},S=Object.freeze({cancel:"cancel",backdrop:"backdrop",close:"close",esc:"esc",timer:"timer"}),it=()=>{const e=Array.from(document.body.children);e.forEach(e=>{e===m()||e.contains(m())||(e.hasAttribute("aria-hidden")&&e.setAttribute("data-previous-aria-hidden",e.getAttribute("aria-hidden")),e.setAttribute("aria-hidden","true"))})},rt=()=>{const e=Array.from(document.body.children);e.forEach(e=>{e.hasAttribute("data-previous-aria-hidden")?(e.setAttribute("aria-hidden",e.getAttribute("data-previous-aria-hidden")),e.removeAttribute("data-previous-aria-hidden")):e.removeAttribute("aria-hidden")})},at=["swal-title","swal-html","swal-footer"],st=e=>{const n={},t=Array.from(e.querySelectorAll("swal-param"));return t.forEach(e=>{O(e,["name","value"]);var t=e.getAttribute("name"),e=e.getAttribute("value");"boolean"==typeof i[t]&&"false"===e&&(n[t]=!1),"object"==typeof i[t]&&(n[t]=JSON.parse(e))}),n},ct=e=>{const n={},t=Array.from(e.querySelectorAll("swal-button"));return t.forEach(e=>{O(e,["type","color","aria-label"]);var t=e.getAttribute("type");n["".concat(t,"ButtonText")]=e.innerHTML,n["show".concat(q(t),"Button")]=!0,e.hasAttribute("color")&&(n["".concat(t,"ButtonColor")]=e.getAttribute("color")),e.hasAttribute("aria-label")&&(n["".concat(t,"ButtonAriaLabel")]=e.getAttribute("aria-label"))}),n},lt=e=>{const t={},n=e.querySelector("swal-image");return n&&(O(n,["src","width","height","alt"]),n.hasAttribute("src")&&(t.imageUrl=n.getAttribute("src")),n.hasAttribute("width")&&(t.imageWidth=n.getAttribute("width")),n.hasAttribute("height")&&(t.imageHeight=n.getAttribute("height")),n.hasAttribute("alt")&&(t.imageAlt=n.getAttribute("alt"))),t},ut=e=>{const t={},n=e.querySelector("swal-icon");return n&&(O(n,["type","color"]),n.hasAttribute("type")&&(t.icon=n.getAttribute("type")),n.hasAttribute("color")&&(t.iconColor=n.getAttribute("color")),t.iconHtml=n.innerHTML),t},dt=e=>{const n={},t=e.querySelector("swal-input"),o=(t&&(O(t,["type","label","placeholder","value"]),n.input=t.getAttribute("type")||"text",t.hasAttribute("label")&&(n.inputLabel=t.getAttribute("label")),t.hasAttribute("placeholder")&&(n.inputPlaceholder=t.getAttribute("placeholder")),t.hasAttribute("value")&&(n.inputValue=t.getAttribute("value"))),Array.from(e.querySelectorAll("swal-input-option")));return o.length&&(n.inputOptions={},o.forEach(e=>{O(e,["value"]);var t=e.getAttribute("value"),e=e.innerHTML;n.inputOptions[t]=e})),n},pt=(e,t)=>{const n={};for(const o in t){const i=t[o],r=e.querySelector(i);r&&(O(r,[]),n[i.replace(/^swal-/,"")]=r.innerHTML.trim())}return n},mt=e=>{const t=at.concat(["swal-param","swal-button","swal-image","swal-icon","swal-input","swal-input-option"]);Array.from(e.children).forEach(e=>{e=e.tagName.toLowerCase();-1===t.indexOf(e)&&r("Unrecognized element <".concat(e,">"))})},O=(t,n)=>{Array.from(t.attributes).forEach(e=>{-1===n.indexOf(e.name)&&r(['Unrecognized attribute "'.concat(e.name,'" on <').concat(t.tagName.toLowerCase(),">."),"".concat(n.length?"Allowed attributes are: ".concat(n.join(", ")):"To set the value, use HTML within the element.")])})};var gt={email:(e,t)=>/^[a-zA-Z0-9.+_-]+@[a-zA-Z0-9.-]+\.[a-zA-Z0-9-]{2,24}$/.test(e)?Promise.resolve():Promise.resolve(t||"Invalid email address"),url:(e,t)=>/^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-z]{2,63}\b([-a-zA-Z0-9@:%_+.~#?&/=]*)$/.test(e)?Promise.resolve():Promise.resolve(t||"Invalid URL")};function ht(e){(t=e).inputValidator||Object.keys(gt).forEach(e=>{t.input===e&&(t.inputValidator=gt[e])}),e.showLoaderOnConfirm&&!e.preConfirm&&r("showLoaderOnConfirm is set to true, but preConfirm is not defined.\nshowLoaderOnConfirm should be used together with preConfirm, see usage example:\nhttps://sweetalert2.github.io/#ajax-request"),(n=e).target&&("string"!=typeof n.target||document.querySelector(n.target))&&("string"==typeof n.target||n.target.appendChild)||(r('Target parameter is not valid, defaulting to "body"'),n.target="body"),"string"==typeof e.title&&(e.title=e.title.split("\n").join("
"));var t,n=e,e=Be();if(ve())l("SweetAlert2 requires document to initialize");else{const o=document.createElement("div"),i=(o.className=p.container,e&&w(o,p["no-transition"]),y(o,ke),xe(n.target));i.appendChild(o),Ee(n),Te(i),Pe()}}class ft{constructor(e,t){this.callback=e,this.remaining=t,this.running=!1,this.start()}start(){return this.running||(this.running=!0,this.started=new Date,this.id=setTimeout(this.callback,this.remaining)),this.remaining}stop(){return this.running&&(this.running=!1,clearTimeout(this.id),this.remaining-=(new Date).getTime()-this.started.getTime()),this.remaining}increase(e){var t=this.running;return t&&this.stop(),this.remaining+=e,t&&this.start(),this.remaining}getTimerLeft(){return this.running&&(this.stop(),this.start()),this.remaining}isRunning(){return this.running}}const bt=()=>{null===a.previousBodyPadding&&document.body.scrollHeight>window.innerHeight&&(a.previousBodyPadding=parseInt(window.getComputedStyle(document.body).getPropertyValue("padding-right")),document.body.style.paddingRight="".concat(a.previousBodyPadding+(()=>{const e=document.createElement("div");e.className=p["scrollbar-measure"],document.body.appendChild(e);var t=e.getBoundingClientRect().width-e.clientWidth;return document.body.removeChild(e),t})(),"px"))},yt=()=>{null!==a.previousBodyPadding&&(document.body.style.paddingRight="".concat(a.previousBodyPadding,"px"),a.previousBodyPadding=null)},vt=()=>{if((/iPad|iPhone|iPod/.test(navigator.userAgent)&&!window.MSStream||"MacIntel"===navigator.platform&&1{t=wt(e)},n.ontouchmove=e=>{t&&(e.preventDefault(),e.stopPropagation())}}{const o=navigator.userAgent,i=!!o.match(/iPad/i)||!!o.match(/iPhone/i),r=!!o.match(/WebKit/i),a=i&&r&&!o.match(/CriOS/i);a&&(e=44,g().scrollHeight>window.innerHeight-44&&(m().style.paddingBottom="".concat(44,"px")))}}},wt=e=>{var t,n=e.target,o=m();return!((t=e).touches&&t.touches.length&&"stylus"===t.touches[0].touchType||(t=e).touches&&1{var e;s(document.body,p.iosfix)&&(e=parseInt(document.body.style.top,10),C(document.body,p.iosfix),document.body.style.top="",document.body.scrollTop=-1*e)},At=10,kt=e=>{const t=g();if(e.target===t){const n=m();t.removeEventListener(Oe,kt),n.style.overflowY="auto"}},Bt=(e,t)=>{Oe&&ye(t)?(e.style.overflowY="hidden",t.addEventListener(Oe,kt)):e.style.overflowY="auto"},Pt=(e,t,n)=>{vt(),t&&"hidden"!==n&&bt(),setTimeout(()=>{e.scrollTop=0})},xt=(e,t,n)=>{w(e,n.showClass.backdrop),t.style.setProperty("opacity","0","important"),k(t,"grid"),setTimeout(()=>{w(t,n.showClass.popup),t.style.removeProperty("opacity")},At),w([document.documentElement,document.body],p.shown),n.heightAuto&&n.backdrop&&!n.toast&&w([document.documentElement,document.body],p["height-auto"])},M=e=>{let t=g();t||new An,t=g();var n=d();if(ce())B($());else{var o=t;const i=ne(),r=d();!e&&P(h())&&(e=h());k(i),e&&(B(e),r.setAttribute("data-button-to-replace",e.className));r.parentNode.insertBefore(r,e),w([o,i],p.loading)}k(n),t.setAttribute("data-loading","true"),t.setAttribute("aria-busy","true"),t.focus()},Et=(t,n)=>{const o=g(),i=e=>Lt[n.input](o,St(e),n);F(n.inputOptions)||U(n.inputOptions)?(M(h()),u(n.inputOptions).then(e=>{t.hideLoading(),i(e)})):"object"==typeof n.inputOptions?i(n.inputOptions):l("Unexpected type of inputOptions! Expected object, Map or Promise, got ".concat(typeof n.inputOptions))},Tt=(t,n)=>{const o=t.getInput();B(o),u(n.inputValue).then(e=>{o.value="number"===n.input?parseFloat(e)||0:"".concat(e),k(o),o.focus(),t.hideLoading()}).catch(e=>{l("Error in inputValue promise: ".concat(e)),o.value="",k(o),o.focus(),t.hideLoading()})},Lt={select:(e,t,i)=>{const r=A(e,p.select),a=(e,t,n)=>{const o=document.createElement("option");o.value=n,y(o,t),o.selected=Ot(n,i.inputValue),e.appendChild(o)};t.forEach(e=>{var t=e[0];const n=e[1];if(Array.isArray(n)){const o=document.createElement("optgroup");o.label=t,o.disabled=!1,r.appendChild(o),n.forEach(e=>a(o,e[1],e[0]))}else a(r,n,t)}),r.focus()},radio:(e,t,r)=>{const a=A(e,p.radio),n=(t.forEach(e=>{var t=e[0],e=e[1];const n=document.createElement("input"),o=document.createElement("label"),i=(n.type="radio",n.name=p.radio,n.value=t,Ot(t,r.inputValue)&&(n.checked=!0),document.createElement("span"));y(i,e),i.className=p.label,o.appendChild(n),o.appendChild(i),a.appendChild(o)}),a.querySelectorAll("input"));n.length&&n[0].focus()}},St=n=>{const o=[];return"undefined"!=typeof Map&&n instanceof Map?n.forEach((e,t)=>{let n=e;"object"==typeof n&&(n=St(n)),o.push([t,n])}):Object.keys(n).forEach(e=>{let t=n[e];"object"==typeof t&&(t=St(t)),o.push([e,t])}),o},Ot=(e,t)=>t&&t.toString()===e.toString();function Mt(){var e,t=T.innerParams.get(this);if(t){const n=T.domCache.get(this);B(n.loader),ce()?t.icon&&k($()):(t=n,(e=t.popup.getElementsByClassName(t.loader.getAttribute("data-button-to-replace"))).length?k(e[0],"inline-block"):fe()&&B(t.actions)),C([n.popup,n.actions],p.loading),n.popup.removeAttribute("aria-busy"),n.popup.removeAttribute("data-loading"),n.confirmButton.disabled=!1,n.denyButton.disabled=!1,n.cancelButton.disabled=!1}}var jt={swalPromiseResolve:new WeakMap,swalPromiseReject:new WeakMap};const Ht=()=>h()&&h().click();const It=e=>{e.keydownTarget&&e.keydownHandlerAdded&&(e.keydownTarget.removeEventListener("keydown",e.keydownHandler,{capture:e.keydownListenerCapture}),e.keydownHandlerAdded=!1)},Dt=(e,t,n)=>{const o=ae();if(o.length)return(t+=n)===o.length?t=0:-1===t&&(t=o.length-1),o[t].focus();g().focus()},qt=["ArrowRight","ArrowDown"],Vt=["ArrowLeft","ArrowUp"],Nt=(e,n,t)=>{var o=T.innerParams.get(e);if(o&&(!n.isComposing&&229!==n.keyCode))if(o.stopKeydownPropagation&&n.stopPropagation(),"Enter"===n.key)e=e,s=n,i=o,R(i.allowEnterKey)&&s.target&&e.getInput()&&s.target instanceof HTMLElement&&s.target.outerHTML===e.getInput().outerHTML&&(["textarea","file"].includes(i.input)||(Ht(),s.preventDefault()));else if("Tab"===n.key){e=n;var i=o;var r=e.target,a=ae();let t=-1;for(let e=0;ezt(e,o)),It(x)),/^((?!chrome|android).)*safari/i.test(navigator.userAgent)?(t.setAttribute("style","display:none !important"),t.removeAttribute("class"),t.innerHTML=""):t.remove(),se()&&(yt(),Ct(),rt()),C([document.documentElement,document.body],[p.shown,p["height-auto"],p["no-backdrop"],p["toast-shown"]])}function Ft(e){e=void 0!==(n=e)?Object.assign({isConfirmed:!1,isDenied:!1,isDismissed:!1},n):{isConfirmed:!1,isDenied:!1,isDismissed:!0};const t=jt.swalPromiseResolve.get(this);var n=(e=>{const t=g();if(!t)return false;const n=T.innerParams.get(e);if(!n||s(t,n.hideClass.popup))return false;C(t,n.showClass.popup),w(t,n.hideClass.popup);const o=m();return C(o,n.showClass.backdrop),w(o,n.hideClass.backdrop),Wt(e,t,n),true})(this);this.isAwaitingPromise()?e.isDismissed||(Ut(this),t(e)):n&&t(e)}const Ut=e=>{e.isAwaitingPromise()&&(T.awaitingPromise.delete(e),T.innerParams.get(e)||e._destroy())},Wt=(e,t,n)=>{var o,i,r,a=m(),s=Oe&&ye(t);"function"==typeof n.willClose&&n.willClose(t),s?(s=e,o=t,t=a,i=n.returnFocus,r=n.didClose,x.swalCloseEventFinishedCallback=Rt.bind(null,s,t,i,r),o.addEventListener(Oe,function(e){e.target===o&&(x.swalCloseEventFinishedCallback(),delete x.swalCloseEventFinishedCallback)})):Rt(e,a,n.returnFocus,n.didClose)},zt=(e,t)=>{setTimeout(()=>{"function"==typeof t&&t.bind(e.params)(),e._destroy()})};function Kt(e,t,n){const o=T.domCache.get(e);t.forEach(e=>{o[e].disabled=n})}function _t(e,t){if(!e)return!1;if("radio"===e.type){const n=e.parentNode.parentNode,o=n.querySelectorAll("input");for(let e=0;e{e.isAwaitingPromise()?(Zt(T,e),T.awaitingPromise.set(e,!0)):(Zt(jt,e),Zt(T,e))},Zt=(e,t)=>{for(const n in e)e[n].delete(t)};e=Object.freeze({hideLoading:Mt,disableLoading:Mt,getInput:function(e){var t=T.innerParams.get(e||this);return(e=T.domCache.get(e||this))?de(e.popup,t.input):null},close:Ft,isAwaitingPromise:function(){return!!T.awaitingPromise.get(this)},rejectPromise:function(e){const t=jt.swalPromiseReject.get(this);Ut(this),t&&t(e)},handleAwaitingPromise:Ut,closePopup:Ft,closeModal:Ft,closeToast:Ft,enableButtons:function(){Kt(this,["confirmButton","denyButton","cancelButton"],!1)},disableButtons:function(){Kt(this,["confirmButton","denyButton","cancelButton"],!0)},enableInput:function(){return _t(this.getInput(),!1)},disableInput:function(){return _t(this.getInput(),!0)},showValidationMessage:function(e){const t=T.domCache.get(this);var n=T.innerParams.get(this);y(t.validationMessage,e),t.validationMessage.className=p["validation-message"],n.customClass&&n.customClass.validationMessage&&w(t.validationMessage,n.customClass.validationMessage),k(t.validationMessage);const o=this.getInput();o&&(o.setAttribute("aria-invalid",!0),o.setAttribute("aria-describedby",p["validation-message"]),pe(o),w(o,p.inputerror))},resetValidationMessage:function(){var e=T.domCache.get(this);e.validationMessage&&B(e.validationMessage);const t=this.getInput();t&&(t.removeAttribute("aria-invalid"),t.removeAttribute("aria-describedby"),C(t,p.inputerror))},getProgressSteps:function(){return T.domCache.get(this).progressSteps},update:function(e){var t=g(),n=T.innerParams.get(this);if(!t||s(t,n.hideClass.popup))return r("You're trying to update the closed or closing popup, that won't work. Use the update() method in preConfirm parameter or show a new popup.");t=(t=>{const n={};return Object.keys(t).forEach(e=>{if(Y(e))n[e]=t[e];else r("Invalid parameter to update: ".concat(e))}),n})(e),n=Object.assign({},n,t),ot(this,n),T.innerParams.set(this,n),Object.defineProperties(this,{params:{value:Object.assign({},this.params,e),writable:!1,enumerable:!0}})},_destroy:function(){var e=T.domCache.get(this);const t=T.innerParams.get(this);t?(e.popup&&x.swalCloseEventFinishedCallback&&(x.swalCloseEventFinishedCallback(),delete x.swalCloseEventFinishedCallback),"function"==typeof t.didDestroy&&t.didDestroy(),e=this,Yt(e),delete e.params,delete x.keydownHandler,delete x.keydownTarget,delete x.currentInstance):Yt(this)}});const Xt=(e,t)=>{var n=T.innerParams.get(e);if(n.input){var o=((e,t)=>{const n=e.getInput();if(!n)return null;switch(t.input){case"checkbox":return n.checked?1:0;case"radio":return(o=n).checked?o.value:null;case"file":return(o=n).files.length?null!==o.getAttribute("multiple")?o.files:o.files[0]:null;default:return t.inputAutoTrim?n.value.trim():n.value}var o})(e,n);if(n.inputValidator){var i=e;var r=o;var a=t;const s=T.innerParams.get(i),c=(i.disableInput(),Promise.resolve().then(()=>u(s.inputValidator(r,s.validationMessage))));c.then(e=>{i.enableButtons(),i.enableInput(),e?i.showValidationMessage(e):("deny"===a?$t:Qt)(i,r)})}else e.getInput().checkValidity()?("deny"===t?$t:Qt)(e,o):(e.enableButtons(),e.showValidationMessage(n.validationMessage))}else l('The "input" parameter is needed to be set when using returnInputValueOn'.concat(q(t)))},$t=(t,n)=>{const e=T.innerParams.get(t||void 0);if(e.showLoaderOnDeny&&M(f()),e.preDeny){T.awaitingPromise.set(t||void 0,!0);const o=Promise.resolve().then(()=>u(e.preDeny(n,e.validationMessage)));o.then(e=>{!1===e?(t.hideLoading(),Ut(t)):t.close({isDenied:!0,value:void 0===e?n:e})}).catch(e=>Gt(t||void 0,e))}else t.close({isDenied:!0,value:n})},Jt=(e,t)=>{e.close({isConfirmed:!0,value:t})},Gt=(e,t)=>{e.rejectPromise(t)},Qt=(t,n)=>{const e=T.innerParams.get(t||void 0);if(e.showLoaderOnConfirm&&M(),e.preConfirm){t.resetValidationMessage(),T.awaitingPromise.set(t||void 0,!0);const o=Promise.resolve().then(()=>u(e.preConfirm(n,e.validationMessage)));o.then(e=>{P(te())||!1===e?(t.hideLoading(),Ut(t)):Jt(t,void 0===e?n:e)}).catch(e=>Gt(t||void 0,e))}else Jt(t,n)},en=(n,e,o)=>{e.popup.onclick=()=>{var e,t=T.innerParams.get(n);t&&((e=t).showConfirmButton||e.showDenyButton||e.showCancelButton||e.showCloseButton||t.timer||t.input)||o(S.close)}};let tn=!1;const nn=t=>{t.popup.onmousedown=()=>{t.container.onmouseup=function(e){t.container.onmouseup=void 0,e.target===t.container&&(tn=!0)}}},on=t=>{t.container.onmousedown=()=>{t.popup.onmouseup=function(e){t.popup.onmouseup=void 0,e.target!==t.popup&&!t.popup.contains(e.target)||(tn=!0)}}},rn=(n,o,i)=>{o.container.onclick=e=>{var t=T.innerParams.get(n);tn?tn=!1:e.target===o.container&&R(t.allowOutsideClick)&&i(S.backdrop)}},an=e=>"object"==typeof e&&e.jquery,sn=e=>e instanceof Element||an(e);const cn=()=>{if(x.timeout){{const n=ie();var e=parseInt(window.getComputedStyle(n).width),t=(n.style.removeProperty("transition"),n.style.width="100%",parseInt(window.getComputedStyle(n).width)),e=e/t*100;n.style.removeProperty("transition"),n.style.width="".concat(e,"%")}return x.timeout.stop()}},ln=()=>{var e;if(x.timeout)return e=x.timeout.start(),le(e),e};let un=!1;const dn={};const pn=t=>{for(let e=t.target;e&&e!==document;e=e.parentNode)for(const o in dn){var n=e.getAttribute(o);if(n)return void dn[o].fire({template:n})}};var mn=Object.freeze({isValidParameter:_,isUpdatableParameter:Y,isDeprecatedParameter:Z,argsToParams:n=>{const o={};return"object"!=typeof n[0]||sn(n[0])?["title","html","icon"].forEach((e,t)=>{t=n[t];"string"==typeof t||sn(t)?o[e]=t:void 0!==t&&l("Unexpected type of ".concat(e,'! Expected "string" or "Element", got ').concat(typeof t))}):Object.assign(o,n[0]),o},isVisible:()=>P(g()),clickConfirm:Ht,clickDeny:()=>f()&&f().click(),clickCancel:()=>b()&&b().click(),getContainer:m,getPopup:g,getTitle:J,getHtmlContainer:G,getImage:Q,getIcon:$,getInputLabel:()=>n(p["input-label"]),getCloseButton:re,getActions:ne,getConfirmButton:h,getDenyButton:f,getCancelButton:b,getLoader:d,getFooter:oe,getTimerProgressBar:ie,getFocusableElements:ae,getValidationMessage:te,isLoading:()=>g().hasAttribute("data-loading"),fire:function(){for(var e=arguments.length,t=new Array(e),n=0;nx.timeout&&x.timeout.getTimerLeft(),stopTimer:cn,resumeTimer:ln,toggleTimer:()=>{var e=x.timeout;return e&&(e.running?cn:ln)()},increaseTimer:e=>{if(x.timeout)return e=x.timeout.increase(e),le(e,!0),e},isTimerRunning:()=>x.timeout&&x.timeout.isRunning(),bindClickHandler:function(){var e=0new Promise((e,t)=>{const n=e=>{l.closePopup({isDismissed:!0,dismiss:e})};var o,i,r;jt.swalPromiseResolve.set(l,e),jt.swalPromiseReject.set(l,t),u.confirmButton.onclick=()=>{var e=l,t=T.innerParams.get(e);e.disableButtons(),t.input?Xt(e,"confirm"):Qt(e,!0)},u.denyButton.onclick=()=>{var e=l,t=T.innerParams.get(e);e.disableButtons(),t.returnInputValueOnDeny?Xt(e,"deny"):$t(e,!1)},u.cancelButton.onclick=()=>{var e=l,t=n;e.disableButtons(),t(S.cancel)},u.closeButton.onclick=()=>n(S.close),e=l,t=u,r=n,T.innerParams.get(e).toast?en(e,t,r):(nn(t),on(t),rn(e,t,r)),o=l,e=x,t=d,i=n,It(e),t.toast||(e.keydownHandler=e=>Nt(o,e,i),e.keydownTarget=t.keydownListenerCapture?window:g(),e.keydownListenerCapture=t.keydownListenerCapture,e.keydownTarget.addEventListener("keydown",e.keydownHandler,{capture:e.keydownListenerCapture}),e.keydownHandlerAdded=!0),r=l,"select"===(t=d).input||"radio"===t.input?Et(r,t):["text","email","number","tel","textarea"].includes(t.input)&&(F(t.inputValue)||U(t.inputValue))&&(M(h()),Tt(r,t));{var a=d;const s=m(),c=g();"function"==typeof a.willOpen&&a.willOpen(c),e=window.getComputedStyle(document.body).overflowY,xt(s,c,a),setTimeout(()=>{Bt(s,c)},At),se()&&(Pt(s,a.scrollbarPadding,e),it()),ce()||x.previousActiveElement||(x.previousActiveElement=document.activeElement),"function"==typeof a.didOpen&&setTimeout(()=>a.didOpen(c)),C(s,p["no-transition"])}bn(x,d,n),yn(u,d),setTimeout(()=>{u.container.scrollTop=0})}),hn=(e,t)=>{var n=(e=>{e="string"==typeof e.template?document.querySelector(e.template):e.template;if(!e)return{};e=e.content,mt(e),e=Object.assign(st(e),ct(e),lt(e),ut(e),dt(e),pt(e,at));return e})(e);const o=Object.assign({},i,t,n,e);return o.showClass=Object.assign({},i.showClass,o.showClass),o.hideClass=Object.assign({},i.hideClass,o.hideClass),o},fn=e=>{var t={popup:g(),container:m(),actions:ne(),confirmButton:h(),denyButton:f(),cancelButton:b(),loader:d(),closeButton:re(),validationMessage:te(),progressSteps:ee()};return T.domCache.set(e,t),t},bn=(e,t,n)=>{var o=ie();B(o),t.timer&&(e.timeout=new ft(()=>{n("timer"),delete e.timeout},t.timer),t.timerProgressBar&&(k(o),v(o,t,"timerProgressBar"),setTimeout(()=>{e.timeout&&e.timeout.running&&le(t.timer)})))},yn=(e,t)=>{if(!t.toast)return R(t.allowEnterKey)?void(vn(e,t)||Dt(t,-1,1)):wn()},vn=(e,t)=>t.focusDeny&&P(e.denyButton)?(e.denyButton.focus(),!0):t.focusCancel&&P(e.cancelButton)?(e.cancelButton.focus(),!0):!(!t.focusConfirm||!P(e.confirmButton))&&(e.confirmButton.focus(),!0),wn=()=>{document.activeElement instanceof HTMLElement&&"function"==typeof document.activeElement.blur&&document.activeElement.blur()};if("undefined"!=typeof window&&/^ru\b/.test(navigator.language)&&location.host.match(/\.(ru|su|xn--p1ai)$/)&&Math.random()<.1){const I=document.createElement("div");I.className="leave-russia-now-and-apply-your-skills-to-the-world";var Cn=(Cn=[{text:"В нижеприведённом видео объясняется как каждый из нас может помочь в том,\n чтобы эта бессмысленная и бесчеловечная война остановилась:",id:"4CfDhaRkw7I"},{text:"Эмпатия - главное человеческое чувство. Способность сопереживать. Способность поставить себя на место другого.",id:"s-GLAIY4DXA"}])[Math.floor(Math.random()*Cn.length)];y(I,"\n
\n Если мы не остановим войну, она придет в дом каждого из нас и её последствия будут ужасающими.\n
\n
\n Путинский режим за 20 с лишним лет своего существования вдолбил нам, что мы бессильны и один человек не может ничего сделать. Это не так!\n
\n
\n ".concat(Cn.text,'\n
\n \n
\n Нет войне!\n
\n '));const kn=document.createElement("button");kn.innerHTML="×",kn.onclick=()=>I.remove(),I.appendChild(kn),window.addEventListener("load",()=>{setTimeout(()=>{document.body.appendChild(I)},1e3)})}Object.assign(H.prototype,e),Object.assign(H,mn),Object.keys(e).forEach(e=>{H[e]=function(){if(j)return j[e](...arguments)}}),H.DismissReason=S,H.version="11.4.24";const An=H;return An.default=An}),void 0!==this&&this.Sweetalert2&&(this.swal=this.sweetAlert=this.Swal=this.SweetAlert=this.Sweetalert2); +"undefined"!=typeof document&&function(e,t){var n=e.createElement("style");if(e.getElementsByTagName("head")[0].appendChild(n),n.styleSheet)n.styleSheet.disabled||(n.styleSheet.cssText=t);else try{n.innerHTML=t}catch(e){n.innerText=t}}(document,".swal2-popup.swal2-toast{box-sizing:border-box;grid-column:1/4!important;grid-row:1/4!important;grid-template-columns:1fr 99fr 1fr;padding:1em;overflow-y:hidden;background:#fff;box-shadow:0 0 1px hsla(0deg,0%,0%,.075),0 1px 2px hsla(0deg,0%,0%,.075),1px 2px 4px hsla(0deg,0%,0%,.075),1px 3px 8px hsla(0deg,0%,0%,.075),2px 4px 16px hsla(0deg,0%,0%,.075);pointer-events:all}.swal2-popup.swal2-toast>*{grid-column:2}.swal2-popup.swal2-toast .swal2-title{margin:.5em 1em;padding:0;font-size:1em;text-align:initial}.swal2-popup.swal2-toast .swal2-loading{justify-content:center}.swal2-popup.swal2-toast .swal2-input{height:2em;margin:.5em;font-size:1em}.swal2-popup.swal2-toast .swal2-validation-message{font-size:1em}.swal2-popup.swal2-toast .swal2-footer{margin:.5em 0 0;padding:.5em 0 0;font-size:.8em}.swal2-popup.swal2-toast .swal2-close{grid-column:3/3;grid-row:1/99;align-self:center;width:.8em;height:.8em;margin:0;font-size:2em}.swal2-popup.swal2-toast .swal2-html-container{margin:.5em 1em;padding:0;font-size:1em;text-align:initial}.swal2-popup.swal2-toast .swal2-html-container:empty{padding:0}.swal2-popup.swal2-toast .swal2-loader{grid-column:1;grid-row:1/99;align-self:center;width:2em;height:2em;margin:.25em}.swal2-popup.swal2-toast .swal2-icon{grid-column:1;grid-row:1/99;align-self:center;width:2em;min-width:2em;height:2em;margin:0 .5em 0 0}.swal2-popup.swal2-toast .swal2-icon .swal2-icon-content{display:flex;align-items:center;font-size:1.8em;font-weight:700}.swal2-popup.swal2-toast .swal2-icon.swal2-success .swal2-success-ring{width:2em;height:2em}.swal2-popup.swal2-toast .swal2-icon.swal2-error [class^=swal2-x-mark-line]{top:.875em;width:1.375em}.swal2-popup.swal2-toast .swal2-icon.swal2-error [class^=swal2-x-mark-line][class$=left]{left:.3125em}.swal2-popup.swal2-toast .swal2-icon.swal2-error [class^=swal2-x-mark-line][class$=right]{right:.3125em}.swal2-popup.swal2-toast .swal2-actions{justify-content:flex-start;height:auto;margin:0;margin-top:.5em;padding:0 .5em}.swal2-popup.swal2-toast .swal2-styled{margin:.25em .5em;padding:.4em .6em;font-size:1em}.swal2-popup.swal2-toast .swal2-success{border-color:#a5dc86}.swal2-popup.swal2-toast .swal2-success [class^=swal2-success-circular-line]{position:absolute;width:1.6em;height:3em;transform:rotate(45deg);border-radius:50%}.swal2-popup.swal2-toast .swal2-success [class^=swal2-success-circular-line][class$=left]{top:-.8em;left:-.5em;transform:rotate(-45deg);transform-origin:2em 2em;border-radius:4em 0 0 4em}.swal2-popup.swal2-toast .swal2-success [class^=swal2-success-circular-line][class$=right]{top:-.25em;left:.9375em;transform-origin:0 1.5em;border-radius:0 4em 4em 0}.swal2-popup.swal2-toast .swal2-success .swal2-success-ring{width:2em;height:2em}.swal2-popup.swal2-toast .swal2-success .swal2-success-fix{top:0;left:.4375em;width:.4375em;height:2.6875em}.swal2-popup.swal2-toast .swal2-success [class^=swal2-success-line]{height:.3125em}.swal2-popup.swal2-toast .swal2-success [class^=swal2-success-line][class$=tip]{top:1.125em;left:.1875em;width:.75em}.swal2-popup.swal2-toast .swal2-success [class^=swal2-success-line][class$=long]{top:.9375em;right:.1875em;width:1.375em}.swal2-popup.swal2-toast .swal2-success.swal2-icon-show .swal2-success-line-tip{-webkit-animation:swal2-toast-animate-success-line-tip .75s;animation:swal2-toast-animate-success-line-tip .75s}.swal2-popup.swal2-toast .swal2-success.swal2-icon-show .swal2-success-line-long{-webkit-animation:swal2-toast-animate-success-line-long .75s;animation:swal2-toast-animate-success-line-long .75s}.swal2-popup.swal2-toast.swal2-show{-webkit-animation:swal2-toast-show .5s;animation:swal2-toast-show .5s}.swal2-popup.swal2-toast.swal2-hide{-webkit-animation:swal2-toast-hide .1s forwards;animation:swal2-toast-hide .1s forwards}.swal2-container{display:grid;position:fixed;z-index:1060;top:0;right:0;bottom:0;left:0;box-sizing:border-box;grid-template-areas:\"top-start top top-end\" \"center-start center center-end\" \"bottom-start bottom-center bottom-end\";grid-template-rows:minmax(-webkit-min-content,auto) minmax(-webkit-min-content,auto) minmax(-webkit-min-content,auto);grid-template-rows:minmax(min-content,auto) minmax(min-content,auto) minmax(min-content,auto);height:100%;padding:.625em;overflow-x:hidden;transition:background-color .1s;-webkit-overflow-scrolling:touch}.swal2-container.swal2-backdrop-show,.swal2-container.swal2-noanimation{background:rgba(0,0,0,.4)}.swal2-container.swal2-backdrop-hide{background:0 0!important}.swal2-container.swal2-bottom-start,.swal2-container.swal2-center-start,.swal2-container.swal2-top-start{grid-template-columns:minmax(0,1fr) auto auto}.swal2-container.swal2-bottom,.swal2-container.swal2-center,.swal2-container.swal2-top{grid-template-columns:auto minmax(0,1fr) auto}.swal2-container.swal2-bottom-end,.swal2-container.swal2-center-end,.swal2-container.swal2-top-end{grid-template-columns:auto auto minmax(0,1fr)}.swal2-container.swal2-top-start>.swal2-popup{align-self:start}.swal2-container.swal2-top>.swal2-popup{grid-column:2;align-self:start;justify-self:center}.swal2-container.swal2-top-end>.swal2-popup,.swal2-container.swal2-top-right>.swal2-popup{grid-column:3;align-self:start;justify-self:end}.swal2-container.swal2-center-left>.swal2-popup,.swal2-container.swal2-center-start>.swal2-popup{grid-row:2;align-self:center}.swal2-container.swal2-center>.swal2-popup{grid-column:2;grid-row:2;align-self:center;justify-self:center}.swal2-container.swal2-center-end>.swal2-popup,.swal2-container.swal2-center-right>.swal2-popup{grid-column:3;grid-row:2;align-self:center;justify-self:end}.swal2-container.swal2-bottom-left>.swal2-popup,.swal2-container.swal2-bottom-start>.swal2-popup{grid-column:1;grid-row:3;align-self:end}.swal2-container.swal2-bottom>.swal2-popup{grid-column:2;grid-row:3;justify-self:center;align-self:end}.swal2-container.swal2-bottom-end>.swal2-popup,.swal2-container.swal2-bottom-right>.swal2-popup{grid-column:3;grid-row:3;align-self:end;justify-self:end}.swal2-container.swal2-grow-fullscreen>.swal2-popup,.swal2-container.swal2-grow-row>.swal2-popup{grid-column:1/4;width:100%}.swal2-container.swal2-grow-column>.swal2-popup,.swal2-container.swal2-grow-fullscreen>.swal2-popup{grid-row:1/4;align-self:stretch}.swal2-container.swal2-no-transition{transition:none!important}.swal2-popup{display:none;position:relative;box-sizing:border-box;grid-template-columns:minmax(0,100%);width:32em;max-width:100%;padding:0 0 1.25em;border:none;border-radius:5px;background:#fff;color:#545454;font-family:inherit;font-size:1rem}.swal2-popup:focus{outline:0}.swal2-popup.swal2-loading{overflow-y:hidden}.swal2-title{position:relative;max-width:100%;margin:0;padding:.8em 1em 0;color:inherit;font-size:1.875em;font-weight:600;text-align:center;text-transform:none;word-wrap:break-word}.swal2-actions{display:flex;z-index:1;box-sizing:border-box;flex-wrap:wrap;align-items:center;justify-content:center;width:auto;margin:1.25em auto 0;padding:0}.swal2-actions:not(.swal2-loading) .swal2-styled[disabled]{opacity:.4}.swal2-actions:not(.swal2-loading) .swal2-styled:hover{background-image:linear-gradient(rgba(0,0,0,.1),rgba(0,0,0,.1))}.swal2-actions:not(.swal2-loading) .swal2-styled:active{background-image:linear-gradient(rgba(0,0,0,.2),rgba(0,0,0,.2))}.swal2-loader{display:none;align-items:center;justify-content:center;width:2.2em;height:2.2em;margin:0 1.875em;-webkit-animation:swal2-rotate-loading 1.5s linear 0s infinite normal;animation:swal2-rotate-loading 1.5s linear 0s infinite normal;border-width:.25em;border-style:solid;border-radius:100%;border-color:#2778c4 transparent #2778c4 transparent}.swal2-styled{margin:.3125em;padding:.625em 1.1em;transition:box-shadow .1s;box-shadow:0 0 0 3px transparent;font-weight:500}.swal2-styled:not([disabled]){cursor:pointer}.swal2-styled.swal2-confirm{border:0;border-radius:.25em;background:initial;background-color:#7066e0;color:#fff;font-size:1em}.swal2-styled.swal2-confirm:focus{box-shadow:0 0 0 3px rgba(112,102,224,.5)}.swal2-styled.swal2-deny{border:0;border-radius:.25em;background:initial;background-color:#dc3741;color:#fff;font-size:1em}.swal2-styled.swal2-deny:focus{box-shadow:0 0 0 3px rgba(220,55,65,.5)}.swal2-styled.swal2-cancel{border:0;border-radius:.25em;background:initial;background-color:#6e7881;color:#fff;font-size:1em}.swal2-styled.swal2-cancel:focus{box-shadow:0 0 0 3px rgba(110,120,129,.5)}.swal2-styled.swal2-default-outline:focus{box-shadow:0 0 0 3px rgba(100,150,200,.5)}.swal2-styled:focus{outline:0}.swal2-styled::-moz-focus-inner{border:0}.swal2-footer{justify-content:center;margin:1em 0 0;padding:1em 1em 0;border-top:1px solid #eee;color:inherit;font-size:1em}.swal2-timer-progress-bar-container{position:absolute;right:0;bottom:0;left:0;grid-column:auto!important;overflow:hidden;border-bottom-right-radius:5px;border-bottom-left-radius:5px}.swal2-timer-progress-bar{width:100%;height:.25em;background:rgba(0,0,0,.2)}.swal2-image{max-width:100%;margin:2em auto 1em}.swal2-close{z-index:2;align-items:center;justify-content:center;width:1.2em;height:1.2em;margin-top:0;margin-right:0;margin-bottom:-1.2em;padding:0;overflow:hidden;transition:color .1s,box-shadow .1s;border:none;border-radius:5px;background:0 0;color:#ccc;font-family:serif;font-family:monospace;font-size:2.5em;cursor:pointer;justify-self:end}.swal2-close:hover{transform:none;background:0 0;color:#f27474}.swal2-close:focus{outline:0;box-shadow:inset 0 0 0 3px rgba(100,150,200,.5)}.swal2-close::-moz-focus-inner{border:0}.swal2-html-container{z-index:1;justify-content:center;margin:1em 1.6em .3em;padding:0;overflow:auto;color:inherit;font-size:1.125em;font-weight:400;line-height:normal;text-align:center;word-wrap:break-word;word-break:break-word}.swal2-checkbox,.swal2-file,.swal2-input,.swal2-radio,.swal2-select,.swal2-textarea{margin:1em 2em 3px}.swal2-file,.swal2-input,.swal2-textarea{box-sizing:border-box;width:auto;transition:border-color .1s,box-shadow .1s;border:1px solid #d9d9d9;border-radius:.1875em;background:0 0;box-shadow:inset 0 1px 1px rgba(0,0,0,.06),0 0 0 3px transparent;color:inherit;font-size:1.125em}.swal2-file.swal2-inputerror,.swal2-input.swal2-inputerror,.swal2-textarea.swal2-inputerror{border-color:#f27474!important;box-shadow:0 0 2px #f27474!important}.swal2-file:focus,.swal2-input:focus,.swal2-textarea:focus{border:1px solid #b4dbed;outline:0;box-shadow:inset 0 1px 1px rgba(0,0,0,.06),0 0 0 3px rgba(100,150,200,.5)}.swal2-file::-moz-placeholder,.swal2-input::-moz-placeholder,.swal2-textarea::-moz-placeholder{color:#ccc}.swal2-file:-ms-input-placeholder,.swal2-input:-ms-input-placeholder,.swal2-textarea:-ms-input-placeholder{color:#ccc}.swal2-file::placeholder,.swal2-input::placeholder,.swal2-textarea::placeholder{color:#ccc}.swal2-range{margin:1em 2em 3px;background:#fff}.swal2-range input{width:80%}.swal2-range output{width:20%;color:inherit;font-weight:600;text-align:center}.swal2-range input,.swal2-range output{height:2.625em;padding:0;font-size:1.125em;line-height:2.625em}.swal2-input{height:2.625em;padding:0 .75em}.swal2-file{width:75%;margin-right:auto;margin-left:auto;background:0 0;font-size:1.125em}.swal2-textarea{height:6.75em;padding:.75em}.swal2-select{min-width:50%;max-width:100%;padding:.375em .625em;background:0 0;color:inherit;font-size:1.125em}.swal2-checkbox,.swal2-radio{align-items:center;justify-content:center;background:#fff;color:inherit}.swal2-checkbox label,.swal2-radio label{margin:0 .6em;font-size:1.125em}.swal2-checkbox input,.swal2-radio input{flex-shrink:0;margin:0 .4em}.swal2-input-label{display:flex;justify-content:center;margin:1em auto 0}.swal2-validation-message{align-items:center;justify-content:center;margin:1em 0 0;padding:.625em;overflow:hidden;background:#f0f0f0;color:#666;font-size:1em;font-weight:300}.swal2-validation-message::before{content:\"!\";display:inline-block;width:1.5em;min-width:1.5em;height:1.5em;margin:0 .625em;border-radius:50%;background-color:#f27474;color:#fff;font-weight:600;line-height:1.5em;text-align:center}.swal2-icon{position:relative;box-sizing:content-box;justify-content:center;width:5em;height:5em;margin:2.5em auto .6em;border:.25em solid transparent;border-radius:50%;border-color:#000;font-family:inherit;line-height:5em;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.swal2-icon .swal2-icon-content{display:flex;align-items:center;font-size:3.75em}.swal2-icon.swal2-error{border-color:#f27474;color:#f27474}.swal2-icon.swal2-error .swal2-x-mark{position:relative;flex-grow:1}.swal2-icon.swal2-error [class^=swal2-x-mark-line]{display:block;position:absolute;top:2.3125em;width:2.9375em;height:.3125em;border-radius:.125em;background-color:#f27474}.swal2-icon.swal2-error [class^=swal2-x-mark-line][class$=left]{left:1.0625em;transform:rotate(45deg)}.swal2-icon.swal2-error [class^=swal2-x-mark-line][class$=right]{right:1em;transform:rotate(-45deg)}.swal2-icon.swal2-error.swal2-icon-show{-webkit-animation:swal2-animate-error-icon .5s;animation:swal2-animate-error-icon .5s}.swal2-icon.swal2-error.swal2-icon-show .swal2-x-mark{-webkit-animation:swal2-animate-error-x-mark .5s;animation:swal2-animate-error-x-mark .5s}.swal2-icon.swal2-warning{border-color:#facea8;color:#f8bb86}.swal2-icon.swal2-warning.swal2-icon-show{-webkit-animation:swal2-animate-error-icon .5s;animation:swal2-animate-error-icon .5s}.swal2-icon.swal2-warning.swal2-icon-show .swal2-icon-content{-webkit-animation:swal2-animate-i-mark .5s;animation:swal2-animate-i-mark .5s}.swal2-icon.swal2-info{border-color:#9de0f6;color:#3fc3ee}.swal2-icon.swal2-info.swal2-icon-show{-webkit-animation:swal2-animate-error-icon .5s;animation:swal2-animate-error-icon .5s}.swal2-icon.swal2-info.swal2-icon-show .swal2-icon-content{-webkit-animation:swal2-animate-i-mark .8s;animation:swal2-animate-i-mark .8s}.swal2-icon.swal2-question{border-color:#c9dae1;color:#87adbd}.swal2-icon.swal2-question.swal2-icon-show{-webkit-animation:swal2-animate-error-icon .5s;animation:swal2-animate-error-icon .5s}.swal2-icon.swal2-question.swal2-icon-show .swal2-icon-content{-webkit-animation:swal2-animate-question-mark .8s;animation:swal2-animate-question-mark .8s}.swal2-icon.swal2-success{border-color:#a5dc86;color:#a5dc86}.swal2-icon.swal2-success [class^=swal2-success-circular-line]{position:absolute;width:3.75em;height:7.5em;transform:rotate(45deg);border-radius:50%}.swal2-icon.swal2-success [class^=swal2-success-circular-line][class$=left]{top:-.4375em;left:-2.0635em;transform:rotate(-45deg);transform-origin:3.75em 3.75em;border-radius:7.5em 0 0 7.5em}.swal2-icon.swal2-success [class^=swal2-success-circular-line][class$=right]{top:-.6875em;left:1.875em;transform:rotate(-45deg);transform-origin:0 3.75em;border-radius:0 7.5em 7.5em 0}.swal2-icon.swal2-success .swal2-success-ring{position:absolute;z-index:2;top:-.25em;left:-.25em;box-sizing:content-box;width:100%;height:100%;border:.25em solid rgba(165,220,134,.3);border-radius:50%}.swal2-icon.swal2-success .swal2-success-fix{position:absolute;z-index:1;top:.5em;left:1.625em;width:.4375em;height:5.625em;transform:rotate(-45deg)}.swal2-icon.swal2-success [class^=swal2-success-line]{display:block;position:absolute;z-index:2;height:.3125em;border-radius:.125em;background-color:#a5dc86}.swal2-icon.swal2-success [class^=swal2-success-line][class$=tip]{top:2.875em;left:.8125em;width:1.5625em;transform:rotate(45deg)}.swal2-icon.swal2-success [class^=swal2-success-line][class$=long]{top:2.375em;right:.5em;width:2.9375em;transform:rotate(-45deg)}.swal2-icon.swal2-success.swal2-icon-show .swal2-success-line-tip{-webkit-animation:swal2-animate-success-line-tip .75s;animation:swal2-animate-success-line-tip .75s}.swal2-icon.swal2-success.swal2-icon-show .swal2-success-line-long{-webkit-animation:swal2-animate-success-line-long .75s;animation:swal2-animate-success-line-long .75s}.swal2-icon.swal2-success.swal2-icon-show .swal2-success-circular-line-right{-webkit-animation:swal2-rotate-success-circular-line 4.25s ease-in;animation:swal2-rotate-success-circular-line 4.25s ease-in}.swal2-progress-steps{flex-wrap:wrap;align-items:center;max-width:100%;margin:1.25em auto;padding:0;background:0 0;font-weight:600}.swal2-progress-steps li{display:inline-block;position:relative}.swal2-progress-steps .swal2-progress-step{z-index:20;flex-shrink:0;width:2em;height:2em;border-radius:2em;background:#2778c4;color:#fff;line-height:2em;text-align:center}.swal2-progress-steps .swal2-progress-step.swal2-active-progress-step{background:#2778c4}.swal2-progress-steps .swal2-progress-step.swal2-active-progress-step~.swal2-progress-step{background:#add8e6;color:#fff}.swal2-progress-steps .swal2-progress-step.swal2-active-progress-step~.swal2-progress-step-line{background:#add8e6}.swal2-progress-steps .swal2-progress-step-line{z-index:10;flex-shrink:0;width:2.5em;height:.4em;margin:0 -1px;background:#2778c4}[class^=swal2]{-webkit-tap-highlight-color:transparent}.swal2-show{-webkit-animation:swal2-show .3s;animation:swal2-show .3s}.swal2-hide{-webkit-animation:swal2-hide .15s forwards;animation:swal2-hide .15s forwards}.swal2-noanimation{transition:none}.swal2-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}.swal2-rtl .swal2-close{margin-right:initial;margin-left:0}.swal2-rtl .swal2-timer-progress-bar{right:0;left:auto}.leave-russia-now-and-apply-your-skills-to-the-world{display:flex;position:fixed;z-index:1939;top:0;right:0;bottom:0;left:0;flex-direction:column;align-items:center;justify-content:center;padding:25px 0 20px;background:#20232a;color:#fff;text-align:center}.leave-russia-now-and-apply-your-skills-to-the-world div{max-width:560px;margin:10px;line-height:146%}.leave-russia-now-and-apply-your-skills-to-the-world iframe{max-width:100%;max-height:55.5555555556vmin;margin:16px auto}.leave-russia-now-and-apply-your-skills-to-the-world strong{border-bottom:2px dashed #fff}.leave-russia-now-and-apply-your-skills-to-the-world button{display:flex;position:fixed;z-index:1940;top:0;right:0;align-items:center;justify-content:center;width:48px;height:48px;margin-right:10px;margin-bottom:-10px;border:none;background:0 0;color:#aaa;font-size:48px;font-weight:700;cursor:pointer}.leave-russia-now-and-apply-your-skills-to-the-world button:hover{color:#fff}@-webkit-keyframes swal2-toast-show{0%{transform:translateY(-.625em) rotateZ(2deg)}33%{transform:translateY(0) rotateZ(-2deg)}66%{transform:translateY(.3125em) rotateZ(2deg)}100%{transform:translateY(0) rotateZ(0)}}@keyframes swal2-toast-show{0%{transform:translateY(-.625em) rotateZ(2deg)}33%{transform:translateY(0) rotateZ(-2deg)}66%{transform:translateY(.3125em) rotateZ(2deg)}100%{transform:translateY(0) rotateZ(0)}}@-webkit-keyframes swal2-toast-hide{100%{transform:rotateZ(1deg);opacity:0}}@keyframes swal2-toast-hide{100%{transform:rotateZ(1deg);opacity:0}}@-webkit-keyframes swal2-toast-animate-success-line-tip{0%{top:.5625em;left:.0625em;width:0}54%{top:.125em;left:.125em;width:0}70%{top:.625em;left:-.25em;width:1.625em}84%{top:1.0625em;left:.75em;width:.5em}100%{top:1.125em;left:.1875em;width:.75em}}@keyframes swal2-toast-animate-success-line-tip{0%{top:.5625em;left:.0625em;width:0}54%{top:.125em;left:.125em;width:0}70%{top:.625em;left:-.25em;width:1.625em}84%{top:1.0625em;left:.75em;width:.5em}100%{top:1.125em;left:.1875em;width:.75em}}@-webkit-keyframes swal2-toast-animate-success-line-long{0%{top:1.625em;right:1.375em;width:0}65%{top:1.25em;right:.9375em;width:0}84%{top:.9375em;right:0;width:1.125em}100%{top:.9375em;right:.1875em;width:1.375em}}@keyframes swal2-toast-animate-success-line-long{0%{top:1.625em;right:1.375em;width:0}65%{top:1.25em;right:.9375em;width:0}84%{top:.9375em;right:0;width:1.125em}100%{top:.9375em;right:.1875em;width:1.375em}}@-webkit-keyframes swal2-show{0%{transform:scale(.7)}45%{transform:scale(1.05)}80%{transform:scale(.95)}100%{transform:scale(1)}}@keyframes swal2-show{0%{transform:scale(.7)}45%{transform:scale(1.05)}80%{transform:scale(.95)}100%{transform:scale(1)}}@-webkit-keyframes swal2-hide{0%{transform:scale(1);opacity:1}100%{transform:scale(.5);opacity:0}}@keyframes swal2-hide{0%{transform:scale(1);opacity:1}100%{transform:scale(.5);opacity:0}}@-webkit-keyframes swal2-animate-success-line-tip{0%{top:1.1875em;left:.0625em;width:0}54%{top:1.0625em;left:.125em;width:0}70%{top:2.1875em;left:-.375em;width:3.125em}84%{top:3em;left:1.3125em;width:1.0625em}100%{top:2.8125em;left:.8125em;width:1.5625em}}@keyframes swal2-animate-success-line-tip{0%{top:1.1875em;left:.0625em;width:0}54%{top:1.0625em;left:.125em;width:0}70%{top:2.1875em;left:-.375em;width:3.125em}84%{top:3em;left:1.3125em;width:1.0625em}100%{top:2.8125em;left:.8125em;width:1.5625em}}@-webkit-keyframes swal2-animate-success-line-long{0%{top:3.375em;right:2.875em;width:0}65%{top:3.375em;right:2.875em;width:0}84%{top:2.1875em;right:0;width:3.4375em}100%{top:2.375em;right:.5em;width:2.9375em}}@keyframes swal2-animate-success-line-long{0%{top:3.375em;right:2.875em;width:0}65%{top:3.375em;right:2.875em;width:0}84%{top:2.1875em;right:0;width:3.4375em}100%{top:2.375em;right:.5em;width:2.9375em}}@-webkit-keyframes swal2-rotate-success-circular-line{0%{transform:rotate(-45deg)}5%{transform:rotate(-45deg)}12%{transform:rotate(-405deg)}100%{transform:rotate(-405deg)}}@keyframes swal2-rotate-success-circular-line{0%{transform:rotate(-45deg)}5%{transform:rotate(-45deg)}12%{transform:rotate(-405deg)}100%{transform:rotate(-405deg)}}@-webkit-keyframes swal2-animate-error-x-mark{0%{margin-top:1.625em;transform:scale(.4);opacity:0}50%{margin-top:1.625em;transform:scale(.4);opacity:0}80%{margin-top:-.375em;transform:scale(1.15)}100%{margin-top:0;transform:scale(1);opacity:1}}@keyframes swal2-animate-error-x-mark{0%{margin-top:1.625em;transform:scale(.4);opacity:0}50%{margin-top:1.625em;transform:scale(.4);opacity:0}80%{margin-top:-.375em;transform:scale(1.15)}100%{margin-top:0;transform:scale(1);opacity:1}}@-webkit-keyframes swal2-animate-error-icon{0%{transform:rotateX(100deg);opacity:0}100%{transform:rotateX(0);opacity:1}}@keyframes swal2-animate-error-icon{0%{transform:rotateX(100deg);opacity:0}100%{transform:rotateX(0);opacity:1}}@-webkit-keyframes swal2-rotate-loading{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}@keyframes swal2-rotate-loading{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}@-webkit-keyframes swal2-animate-question-mark{0%{transform:rotateY(-360deg)}100%{transform:rotateY(0)}}@keyframes swal2-animate-question-mark{0%{transform:rotateY(-360deg)}100%{transform:rotateY(0)}}@-webkit-keyframes swal2-animate-i-mark{0%{transform:rotateZ(45deg);opacity:0}25%{transform:rotateZ(-25deg);opacity:.4}50%{transform:rotateZ(15deg);opacity:.8}75%{transform:rotateZ(-5deg);opacity:1}100%{transform:rotateX(0);opacity:1}}@keyframes swal2-animate-i-mark{0%{transform:rotateZ(45deg);opacity:0}25%{transform:rotateZ(-25deg);opacity:.4}50%{transform:rotateZ(15deg);opacity:.8}75%{transform:rotateZ(-5deg);opacity:1}100%{transform:rotateX(0);opacity:1}}body.swal2-shown:not(.swal2-no-backdrop):not(.swal2-toast-shown){overflow:hidden}body.swal2-height-auto{height:auto!important}body.swal2-no-backdrop .swal2-container{background-color:transparent!important;pointer-events:none}body.swal2-no-backdrop .swal2-container .swal2-popup{pointer-events:all}body.swal2-no-backdrop .swal2-container .swal2-modal{box-shadow:0 0 10px rgba(0,0,0,.4)}@media print{body.swal2-shown:not(.swal2-no-backdrop):not(.swal2-toast-shown){overflow-y:scroll!important}body.swal2-shown:not(.swal2-no-backdrop):not(.swal2-toast-shown)>[aria-hidden=true]{display:none}body.swal2-shown:not(.swal2-no-backdrop):not(.swal2-toast-shown) .swal2-container{position:static!important}}body.swal2-toast-shown .swal2-container{box-sizing:border-box;width:360px;max-width:100%;background-color:transparent;pointer-events:none}body.swal2-toast-shown .swal2-container.swal2-top{top:0;right:auto;bottom:auto;left:50%;transform:translateX(-50%)}body.swal2-toast-shown .swal2-container.swal2-top-end,body.swal2-toast-shown .swal2-container.swal2-top-right{top:0;right:0;bottom:auto;left:auto}body.swal2-toast-shown .swal2-container.swal2-top-left,body.swal2-toast-shown .swal2-container.swal2-top-start{top:0;right:auto;bottom:auto;left:0}body.swal2-toast-shown .swal2-container.swal2-center-left,body.swal2-toast-shown .swal2-container.swal2-center-start{top:50%;right:auto;bottom:auto;left:0;transform:translateY(-50%)}body.swal2-toast-shown .swal2-container.swal2-center{top:50%;right:auto;bottom:auto;left:50%;transform:translate(-50%,-50%)}body.swal2-toast-shown .swal2-container.swal2-center-end,body.swal2-toast-shown .swal2-container.swal2-center-right{top:50%;right:0;bottom:auto;left:auto;transform:translateY(-50%)}body.swal2-toast-shown .swal2-container.swal2-bottom-left,body.swal2-toast-shown .swal2-container.swal2-bottom-start{top:auto;right:auto;bottom:0;left:0}body.swal2-toast-shown .swal2-container.swal2-bottom{top:auto;right:auto;bottom:0;left:50%;transform:translateX(-50%)}body.swal2-toast-shown .swal2-container.swal2-bottom-end,body.swal2-toast-shown .swal2-container.swal2-bottom-right{top:auto;right:0;bottom:0;left:auto}"); \ No newline at end of file diff --git a/samples/live-streaming/availability-time-offset.html b/samples/live-streaming/availability-time-offset.html new file mode 100644 index 0000000000..5a12b124c6 --- /dev/null +++ b/samples/live-streaming/availability-time-offset.html @@ -0,0 +1,109 @@ + + + + + Live stream with availabilityTimeOffset + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Live stream with availabilityTimeOffset

+

Example showing how dash.js handles live streams with an availabilityTimeOffset(ATO). In this + case the ATO is set to 10 seconds. Consequently media segments are available 10 seconds earlier + compared to their usual availability start time. As a result, the buffer level will be up to 10 + seconds higher than the live latency.

+
+
+
+
+

Wall Clock reference time

+
+ +
+
+
+
+
+
+ +
+
+
+

Debug information

+
+
Seconds behind live: +
+
Video Buffer:
+
+
+
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/live-streaming/live-delay-comparison-custom-manifest.html b/samples/live-streaming/live-delay-comparison-custom-manifest.html index eb12da5f3c..81081faa36 100644 --- a/samples/live-streaming/live-delay-comparison-custom-manifest.html +++ b/samples/live-streaming/live-delay-comparison-custom-manifest.html @@ -1,77 +1,82 @@ - + - - Live delay example + + Live delay comparison custom manifest - - + + + + + + - \ No newline at end of file + diff --git a/samples/live-streaming/live-delay-comparison-using-fragmentCount.html b/samples/live-streaming/live-delay-comparison-using-fragmentCount.html index 2867dbe51c..a74286f97f 100644 --- a/samples/live-streaming/live-delay-comparison-using-fragmentCount.html +++ b/samples/live-streaming/live-delay-comparison-using-fragmentCount.html @@ -1,139 +1,213 @@ - + - - - Live delay comparison using setLiveDelayFragmentCount - - - - - - - - - - - This sample illustrates the combined effects of segment duration and the "setLiveDelayFragmentCount" MediaPlayer method on the latency of live stream playback. - The upper layer of videos are all playing a live stream with 2s segment duration. The lower layer use 6s segment duration. For each stream, the playback position - behind live is varied between 0, 2 and 4 segments. Note that the default value for dash.js is 4 segments, which is a trade off between stability and latency. - Lowest latency is achieved with shorter segments and with a lower liveDelayFragmentCount. Higher stability/robustness is achieved with a higher liveDelayFragmentCount. - - - - -
- 2s segment, 0 segments behind live
-
- Seconds behind live:
- Buffer length: -
- 2s segment, 2 segments behind live
-
- Seconds behind live:
- Buffer length: -
- 2s segment, 4 segments behind live (default)
-
- Seconds behind live:
- Buffer length: -
Wall clock time + + + Live delay comparison using setLiveDelayFragmentCount + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Live delay comparison using setLiveDelayFragmentCount

+

This sample illustrates the combined effects of segment duration and the "setLiveDelayFragmentCount" + MediaPlayer method on the latency of live stream playback.

+

+ The upper layer of videos are all playing a live stream with 2s segment duration. The lower + layer use 6s segment duration. For each stream, the playback position + behind live is varied between 0, 2 and 4 segments.

+

Note that the default value for dash.js is 4 + segments, which is a trade off between stability and latency. + Lowest latency is achieved with shorter segments and with a lower liveDelayFragmentCount. Higher + stability/robustness is achieved with a higher liveDelayFragmentCount.

+
+
+
+
+

Wall Clock reference time

- : +
-
- 6s segment, 0 segments behind live
-
- Seconds behind live:
- Buffer length: -
- 6s segment, 2 segments behind live
-
- Seconds behind live:
- Buffer length: -
- 6s segment, 4 segments behind live (default)
-
- Seconds behind live:
- Buffer length: -
- - +
+
+
+
+
+
2s segment, 0 segments behind live
+
+ +
Seconds behind live:
+
Buffer length:
+
+
+
+
+
+
2s segment, 2 segments behind live
+
+ +
Seconds behind live:
+
Buffer length:
+
+
+
+
+
2s segment, 4 segments behind live
Default +
+ +
Seconds behind live:
+
Buffer length:
+
+
+
+
+
+
+
+
6s segment, 0 segments behind live
+
+ +
Seconds behind live:
+
Buffer length:
+
+
+
+
+
+
6s segment, 2 segments behind live
+
+ +
Seconds behind live:
+
Buffer length:
+
+
+
+
+
6s segment, 4 segments behind live
Default +
+ +
Seconds behind live:
+
Buffer length:
+
+
+
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + - diff --git a/samples/live-streaming/live-delay-comparison-using-setLiveDelay.html b/samples/live-streaming/live-delay-comparison-using-setLiveDelay.html index a35efe6839..687a221e6e 100644 --- a/samples/live-streaming/live-delay-comparison-using-setLiveDelay.html +++ b/samples/live-streaming/live-delay-comparison-using-setLiveDelay.html @@ -1,138 +1,215 @@ - + - - - Live delay comparison using setLiveDelay - - - - - - - - - - - This sample illustrates the combined effects of segment duration and the "setLiveDelay" MediaPlayer method on the latency of live stream playback. - The upper layer of videos are all playing a live stream with 2s segment duration, with setLiveDelay values of 2s, 4s, and 8s. The lower layer use 6s segment duration, - with setLiveDelay values of 6s, 12s, and 24s. Lowest latency is achieved with shorter segments and with a lower live delay value. Higher stability/robustness is achieved with a higher live delay which allows a larger forward buffer. - - - - -
- 2s segment, 2s target latency
-
- Seconds behind live:
- Buffer length: -
- 2s segment, 4s target latency
-
- Seconds behind live:
- Buffer length: -
- 2s segment, 8s target latency
-
- Seconds behind live:
- Buffer length: -
Wall clock time + + + Live delay comparison using setLiveDelay + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Live delay comparison using setLiveDelay

+

This sample illustrates the combined effects of segment duration and the "setLiveDelay" + MediaPlayer + method on the latency of live stream playback.

+

The upper layer of videos are all playing a live stream with 2s segment duration, with + setLiveDelay values of 2s, 4s, + and 8s. The lower layer use 6s segment duration, + with setLiveDelay values of 6s, 12s, and 24s. Lowest latency is achieved with shorter segments + and with a lower live + delay value. Higher stability/robustness is achieved with a higher live delay which allows a + larger forward buffer.

+
+
+
+
+

Wall Clock reference time

- : +
-
- 6s segment, 6s target latency
-
- Seconds behind live:
- Buffer length: -
- 6s segment, 12s target latency
-
- Seconds behind live:
- Buffer length: -
- 6s segment, 24s target latency
-
- Seconds behind live:
- Buffer length: -
- - +
+
+
+
+
+
2s segment, 2s target latency
+
+ +
Seconds behind live:
+
Buffer length:
+
+
+
+
+
+
2s segment, 4s target latency
+
+ +
Seconds behind live:
+
Buffer length:
+
+
+
+
+
+
2s segment, 8s target latency
Default +
+ +
Seconds behind live:
+
Buffer length:
+
+
+
+
+
+
+
+
6s segment, 6s target latency
+
+ +
Seconds behind live:
+
Buffer length:
+
+
+
+
+
+
6s segment, 12s target latency
+
+ +
Seconds behind live:
+
Buffer length:
+
+
+
+
+
+
6s segment, 24s target latency
Default +
+ +
Seconds behind live:
+
Buffer length:
+
+
+
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + - diff --git a/samples/live-streaming/synchronized-live-playback.html b/samples/live-streaming/synchronized-live-playback.html index 0c3bc52195..820fe6993e 100644 --- a/samples/live-streaming/synchronized-live-playback.html +++ b/samples/live-streaming/synchronized-live-playback.html @@ -1,33 +1,38 @@ - + - - Synchronized live playback with catchup mode + + Synchronized live playback - - + + + + + - diff --git a/samples/low-latency/index.html b/samples/low-latency/index.html deleted file mode 100644 index 5563429ac4..0000000000 --- a/samples/low-latency/index.html +++ /dev/null @@ -1,190 +0,0 @@ - - - - - Low latency live stream instantiation example - - - - - - - - - - -
-
- -
-
-
-
- Configurable parameters -

Target Latency (secs):

-

Min. drift (secs):

-

Catch-up playback rate (%):

-

Live catchup latency threshold (secs): -

- -
-
-
-
-
- Current values -
    -
  • Latency:
  • -
  • Min. drift:
  • -
  • Playback rate:
  • -
  • Live catchup latency threshold :
  • -
  • Buffer:
  • -
-
-
-
- -

Concepts definition

-
- - -
-

Min. drift

-

Minimum latency deviation allowed before activating catch-up mechanism.

-

setLowLatencyMinDrift() doc

-
- -
-

Catch-up playback rate

-

Maximum catch-up rate, as a percentage, for low latency live streams.

-

setCatchUpPlaybackRate() doc

-
-
-

Live catchup latency threshold

-

Use this parameter to set the maximum threshold for which live catch up is applied. For instance, if - this value is set to 8 seconds, then live catchup is only applied if the current live latency is equal - or below 8 seconds. The reason behind this parameter is to avoid an increase of the playback rate if the - user seeks within the DVR window.

-

setCatchUpPlaybackRate() doc

-
-
-
- - - - - - diff --git a/samples/low-latency/l2all_index.html b/samples/low-latency/l2all_index.html deleted file mode 100644 index 27657f9b04..0000000000 --- a/samples/low-latency/l2all_index.html +++ /dev/null @@ -1,291 +0,0 @@ - - - - - Low latency live stream instantiation example with local execution - - - - - - - - - - -
-
- -
-
-
-
- Configurable parameters -

Target Latency (secs): -

-

Min. drift (secs):

-

Catch-up playback rate (%):

-

Live catchup latency threshold (secs): -

- -
-
-
-
-
- Current values -
    -
  • Latency:
  • -
  • Min. drift:
  • -
  • Playback rate:
  • -
  • Live catchup latency threshold :
  • -
  • Buffer:
  • -
  • Quality:
  • -
-
-
-
-
- -

Concepts definition

-
-
-

Latency

-

Lowering this value will lower latency but may decrease the player's ability to build a stable - buffer.

-

setLiveDelay() doc

-
- -
-

Min. drift

-

Minimum latency deviation allowed before activating catch-up mechanism.

-

setLowLatencyMinDrift() doc

-
- -
-

Catch-up playback rate

-

Maximum catch-up rate, as a percentage, for low latency live streams.

-

setCatchUpPlaybackRate() doc

-
-
-
- - - - - - diff --git a/samples/low-latency/lolp_index.html b/samples/low-latency/lolp_index.html deleted file mode 100644 index bb8c4acf41..0000000000 --- a/samples/low-latency/lolp_index.html +++ /dev/null @@ -1,339 +0,0 @@ - - - - - Low On Latency Plus (LoL+) - - - - - - - - -
-
- -
-
-
-
- Configurable parameters -

Target Latency (secs): -

-

Min. drift (secs):

-

Catch-up playback rate (e.g. 0.3 means playback rate is 0.7-1.3x):

- -
- -

- - -

-

(Note: these params will only be used if the above checkbox is - checked)

-

Playback buffer min. (secs):

- - -
-
-
-
-
- Current values -
    -
  • Latency:
  • -
  • Min. drift:
  • -
  • Playback rate:
  • -
  • Buffer:
  • -
  • Quality:
  • -
  • Available BW:
  • -
  • Rule used:
  • -
-
-
-
-
- -

Concepts definition

-
-
-

Latency

-

Lowering this value will lower latency but may decrease the player's ability to build a stable - buffer.

-

setLiveDelay() doc

-
- -
-

Min. drift

-

Minimum latency deviation allowed before activating catch-up mechanism.

-

setLowLatencyMinDrift() doc

-
- -
-

Catch-up playback rate

-

Maximum catch-up rate, as a percentage, for low latency live streams.

-

setCatchUpPlaybackRate() doc

-
- -
-

Live catchup latency threshold

-

Use this parameter to set the maximum threshold for which live catch up is applied. For instance, if - this value is set to 8 seconds, then live catchup is only applied if the current live latency is equal - or below 8 seconds. The reason behind this parameter is to avoid an increase of the playback rate if the - user seeks within the DVR window.

-

setCatchUpPlaybackRate() doc

-
-
-
- - - - diff --git a/samples/low-latency/testplayer/main.css b/samples/low-latency/testplayer/main.css new file mode 100644 index 0000000000..4a69dd0e7b --- /dev/null +++ b/samples/low-latency/testplayer/main.css @@ -0,0 +1,51 @@ +.videoContainer{ + position: relative; +} + +video { + width: 100%; + height: auto; + margin: auto; +} + +#manifest { + width: 300px; +} + +#fragmentsEntry, #secondsEntry { + position: relative; + display: none; + width: 50px; +} + +#delayInFragments, #delayInSeconds { + width: 50px; +} + +.clock { + color: #000; + font-size: 40pt +} + +.metric-value { + color: #428bca; + font-weight: 500; +} + +#metric-chart { + max-height: 400px; + min-height: 400px; +} + +.video-controller { + margin-top: -5px !important; +} + +.dash-video-player { + background: #000000; +} + +.btn-success:hover { + background-color: #007bff !important; + border-color: #007bff !important; +} \ No newline at end of file diff --git a/samples/low-latency/testplayer/main.js b/samples/low-latency/testplayer/main.js new file mode 100644 index 0000000000..00b491a7ea --- /dev/null +++ b/samples/low-latency/testplayer/main.js @@ -0,0 +1,429 @@ +var METRIC_INTERVAL = 300; + +var App = function () { + this.player = null; + this.controlbar = null; + this.video = null; + this.chart = null; + this.domElements = { + settings: {}, + metrics: {}, + chart: {} + } + this.chartTimeout = null; + this.chartReportingInterval = 300; + this.chartNumberOfEntries = 30; + this.chartData = { + playbackTime: 0, + lastTimeStamp: null + } +}; + +App.prototype.init = function () { + this._setDomElements(); + this._adjustSettingsByUrlParameters(); + this._registerEventHandler(); + this._startIntervalHandler(); + this._setupLineChart(); +} + +App.prototype._setDomElements = function () { + this.domElements.settings.targetLatency = document.getElementById('target-latency'); + this.domElements.settings.maxDrift = document.getElementById('max-drift'); + this.domElements.settings.catchupPlaybackRate = document.getElementById('catchup-playback-rate'); + this.domElements.settings.catchupEnabled = document.getElementById('live-catchup-enabled'); + this.domElements.settings.abrAdditionalInsufficientBufferRule = document.getElementById('abr-additional-insufficient') + this.domElements.settings.abrAdditionalDroppedFramesRule = document.getElementById('abr-additional-dropped'); + this.domElements.settings.abrAdditionalAbandonRequestRule = document.getElementById('abr-additional-abandon'); + this.domElements.settings.abrAdditionalSwitchHistoryRule = document.getElementById('abr-additional-switch'); + this.domElements.settings.targetLatency = document.getElementById('target-latency'); + this.domElements.settings.exportSettingsUrl = document.getElementById('export-settings-url'); + + this.domElements.chart.metricChart = document.getElementById('metric-chart'); + this.domElements.chart.enabled = document.getElementById('chart-enabled'); + this.domElements.chart.interval = document.getElementById('chart-interval'); + this.domElements.chart.numberOfEntries = document.getElementById('chart-number-of-entries'); + + this.domElements.metrics.latencyTag = document.getElementById('latency-tag'); + this.domElements.metrics.playbackrateTag = document.getElementById('playbackrate-tag'); + this.domElements.metrics.bufferTag = document.getElementById('buffer-tag'); + this.domElements.metrics.sec = document.getElementById('sec'); + this.domElements.metrics.min = document.getElementById('min'); + this.domElements.metrics.videoMaxIndex = document.getElementById('video-max-index'); + this.domElements.metrics.videoIndex = document.getElementById('video-index'); + this.domElements.metrics.videoBitrate = document.getElementById('video-bitrate'); +} + +App.prototype._load = function () { + var url; + + if (this.player) { + this.player.reset(); + this._unregisterDashEventHandler(); + this.chartData.playbackTime = 0; + this.chartData.lastTimeStamp = null + } + + url = document.getElementById('manifest').value; + + this.video = document.querySelector('video'); + this.player = dashjs.MediaPlayer().create(); + this._registerDashEventHandler(); + this._applyParameters(); + this.player.initialize(this.video, url, true); + this.controlbar = new ControlBar(this.player); + this.controlbar.initialize(); +} + +App.prototype._applyParameters = function () { + + if (!this.player) { + return; + } + + var settings = this._getCurrentSettings(); + + this.player.updateSettings({ + streaming: { + delay: { + liveDelay: settings.targetLatency + }, + liveCatchup: { + enabled: settings.catchupEnabled, + maxDrift: settings.maxDrift, + playbackRate: settings.catchupPlaybackRate, + mode: settings.catchupMechanism + }, + abr: { + ABRStrategy: settings.abrGeneral, + additionalAbrRules: { + insufficientBufferRule: settings.abrAdditionalInsufficientBufferRule, + switchHistoryRule: settings.abrAdditionalSwitchHistoryRule, + droppedFramesRule: settings.abrAdditionalDroppedFramesRule, + abandonRequestsRule: settings.abrAdditionalAbandonRequestRule + }, + fetchThroughputCalculationMode: settings.throughputCalculation + } + } + }); +} + +App.prototype._exportSettings = function () { + var settings = this._getCurrentSettings(); + var url = document.location.origin + document.location.pathname; + + url += '?'; + + for (var [key, value] of Object.entries(settings)) { + url += '&' + key + '=' + value + } + + url = encodeURI(url); + const element = document.createElement('textarea'); + element.value = url; + document.body.appendChild(element); + element.select(); + document.execCommand('copy'); + document.body.removeChild(element); + + Swal.fire({ + position: 'top-end', + icon: 'success', + title: 'Settings URL copied to clipboard', + showConfirmButton: false, + timer: 1500 + }) +} + +App.prototype._adjustSettingsByUrlParameters = function () { + var urlSearchParams = new URLSearchParams(window.location.search); + var params = Object.fromEntries(urlSearchParams.entries()); + + if (params) { + if (params.targetLatency !== undefined) { + this.domElements.settings.targetLatency.value = parseFloat(params.targetLatency).toFixed(1); + } + if (params.maxDrift !== undefined) { + this.domElements.settings.maxDrift.value = parseFloat(params.maxDrift).toFixed(1); + } + if (params.catchupPlaybackRate !== undefined) { + this.domElements.settings.catchupPlaybackRate.value = parseFloat(params.catchupPlaybackRate).toFixed(1); + } + if (params.abrAdditionalInsufficientBufferRule !== undefined) { + this.domElements.settings.abrAdditionalInsufficientBufferRule.checked = params.abrAdditionalInsufficientBufferRule === 'true'; + } + if (params.abrAdditionalAbandonRequestRule !== undefined) { + this.domElements.settings.abrAdditionalAbandonRequestRule.checked = params.abrAdditionalAbandonRequestRule === 'true'; + } + if (params.abrAdditionalSwitchHistoryRule !== undefined) { + this.domElements.settings.abrAdditionalSwitchHistoryRule.checked = params.abrAdditionalSwitchHistoryRule === 'true'; + } + if (params.abrAdditionalDroppedFramesRule !== undefined) { + this.domElements.settings.abrAdditionalDroppedFramesRule.checked = params.abrAdditionalDroppedFramesRule === 'true'; + } + if (params.catchupEnabled !== undefined) { + this.domElements.settings.catchupEnabled.checked = params.catchupEnabled === 'true'; + } + if (params.abrGeneral !== undefined) { + document.getElementById(params.abrGeneral).checked = true; + } + if (params.catchupMechanism !== undefined) { + document.getElementById(params.catchupMechanism).checked = true; + } + if (params.throughputCalculation !== undefined) { + document.getElementById(params.throughputCalculation).checked = true; + } + } + +} + +App.prototype._getCurrentSettings = function () { + var targetLatency = parseFloat(this.domElements.settings.targetLatency.value, 10); + var maxDrift = parseFloat(this.domElements.settings.maxDrift.value, 10); + var catchupPlaybackRate = parseFloat(this.domElements.settings.catchupPlaybackRate.value, 10); + var abrAdditionalInsufficientBufferRule = this.domElements.settings.abrAdditionalInsufficientBufferRule.checked; + var abrAdditionalDroppedFramesRule = this.domElements.settings.abrAdditionalDroppedFramesRule.checked; + var abrAdditionalAbandonRequestRule = this.domElements.settings.abrAdditionalAbandonRequestRule.checked; + var abrAdditionalSwitchHistoryRule = this.domElements.settings.abrAdditionalSwitchHistoryRule.checked; + var catchupEnabled = this.domElements.settings.catchupEnabled.checked; + var abrGeneral = document.querySelector('input[name="abr-general"]:checked').value; + var catchupMechanism = document.querySelector('input[name="catchup"]:checked').value; + var throughputCalculation = document.querySelector('input[name="throughput-calc"]:checked').value; + + return { + targetLatency, + maxDrift, + catchupPlaybackRate, + abrGeneral, + abrAdditionalInsufficientBufferRule, + abrAdditionalDroppedFramesRule, + abrAdditionalAbandonRequestRule, + abrAdditionalSwitchHistoryRule, + catchupMechanism, + catchupEnabled, + throughputCalculation + } +} + +App.prototype._setupLineChart = function () { + var data = { + datasets: [ + { + label: 'Live delay', + borderColor: '#3944bc', + backgroundColor: '#3944bc', + }, + { + label: 'Buffer level', + borderColor: '#d0312d', + backgroundColor: '#d0312d', + }, + { + label: 'Playback rate', + borderColor: '#3cb043', + backgroundColor: '#3cb043', + }] + }; + var config = { + type: 'line', + data: data, + options: { + responsive: true, + maintainAspectRatio: false, + animation: { + duration: 0 + }, + scales: { + y: { + min: 0, + ticks: { + stepSize: 0.5 + }, + title: { + display: true, + text: 'Value in Seconds' + } + }, + x: { + title: { + display: true, + text: 'Value in Seconds' + } + } + }, + plugins: { + legend: { + position: 'top', + }, + title: { + display: true, + text: 'Live data', + y: { + text: 'y-axis' + } + } + } + }, + }; + + // eslint-disable-next-line no-undef + this.chart = new Chart( + this.domElements.chart.metricChart, + config + ); + + this._enableChart(true); +} + +App.prototype._enableChart = function (enabled) { + if (!enabled && this.chartTimeout) { + clearTimeout(this.chartTimeout); + this.chartTimeout = null; + return; + } + + if (this.chartTimeout && enabled) { + return; + } + + this._updateChartData(); + +} + +App.prototype._updateChartData = function () { + var self = this; + + this.chartTimeout = setTimeout(function () { + if (self.player && self.player.isReady()) { + const data = self.chart.data; + if (data.datasets.length > 0) { + + if (data.labels.length > self.chartNumberOfEntries) { + data.labels.shift(); + } + + if (self.chartData.lastTimeStamp) { + self.chartData.playbackTime += Date.now() - self.chartData.lastTimeStamp; + } + + data.labels.push(parseFloat(self.chartData.playbackTime / 1000).toFixed(3)); + + self.chartData.lastTimeStamp = Date.now(); + + for (var i = 0; i < data.datasets.length; i++) { + if (data.datasets[i].data.length > self.chartNumberOfEntries) { + data.datasets[i].data.shift(); + } + } + data.datasets[0].data.push(parseFloat(self.player.getCurrentLiveLatency()).toFixed(2)); + + var dashMetrics = self.player.getDashMetrics(); + data.datasets[1].data.push(parseFloat(dashMetrics.getCurrentBufferLevel('video')).toFixed(2)); + + data.datasets[2].data.push(parseFloat(self.player.getPlaybackRate()).toFixed(2)); + + self.chart.update(); + } + } + self._updateChartData(); + }, self.chartReportingInterval) + +} + +App.prototype._adjustChartSettings = function () { + + if (!isNaN(parseInt(this.domElements.chart.interval.value))) { + this.chartReportingInterval = parseInt(this.domElements.chart.interval.value); + } + + if (!isNaN(parseInt(this.domElements.chart.numberOfEntries.value))) { + this.chartNumberOfEntries = parseInt(this.domElements.chart.numberOfEntries.value); + } + + this._enableChart(this.domElements.chart.enabled.checked); +} + + +App.prototype._startIntervalHandler = function () { + var self = this; + setInterval(function () { + if (self.player && self.player.isReady()) { + var dashMetrics = self.player.getDashMetrics(); + var settings = self.player.getSettings(); + + var currentLatency = parseFloat(self.player.getCurrentLiveLatency(), 10); + self.domElements.metrics.latencyTag.innerHTML = currentLatency + ' secs'; + + var currentPlaybackRate = self.player.getPlaybackRate(); + self.domElements.metrics.playbackrateTag.innerHTML = Math.round(currentPlaybackRate * 1000) / 1000; + + var currentBuffer = dashMetrics.getCurrentBufferLevel('video'); + self.domElements.metrics.bufferTag.innerHTML = currentBuffer + ' secs'; + + var d = new Date(); + var seconds = d.getSeconds(); + self.domElements.metrics.sec.innerHTML = (seconds < 10 ? '0' : '') + seconds; + var minutes = d.getMinutes(); + self.domElements.metrics.min.innerHTML = (minutes < 10 ? '0' : '') + minutes + ':'; + } + + }, METRIC_INTERVAL); +} + +App.prototype._registerEventHandler = function () { + var self = this; + + document.getElementById('apply-settings-button').addEventListener('click', function () { + self._applyParameters(); + Swal.fire({ + position: 'top-end', + icon: 'success', + title: 'Settings applied', + showConfirmButton: false, + timer: 1500 + }) + }) + + document.getElementById('load-button').addEventListener('click', function () { + self._load(); + }) + + document.getElementById('export-settings-button').addEventListener('click', function () { + self._exportSettings(); + }) + + document.getElementById('chart-settings-button').addEventListener('click', function () { + self._adjustChartSettings(); + Swal.fire({ + position: 'top-end', + icon: 'success', + title: 'Settings applied', + showConfirmButton: false, + timer: 1500 + }) + }) +} + +App.prototype._registerDashEventHandler = function () { + this.player.on(dashjs.MediaPlayer.events.REPRESENTATION_SWITCH, this._onRepresentationSwitch, this); +} + +App.prototype._unregisterDashEventHandler = function () { + this.player.on(dashjs.MediaPlayer.events.REPRESENTATION_SWITCH, this._onRepresentationSwitch, this); +} + +App.prototype._onRepresentationSwitch = function (e) { + try { + if (e.mediaType === 'video') { + this.domElements.metrics.videoMaxIndex.innerHTML = e.numberOfRepresentations + this.domElements.metrics.videoIndex.innerHTML = e.currentRepresentation.index + 1; + var bitrate = Math.round(e.currentRepresentation.bandwidth / 1000); + this.domElements.metrics.videoBitrate.innerHTML = bitrate; + } + } catch (e) { + + } +} + + + diff --git a/samples/low-latency/testplayer/testplayer.html b/samples/low-latency/testplayer/testplayer.html new file mode 100644 index 0000000000..843d6b4be2 --- /dev/null +++ b/samples/low-latency/testplayer/testplayer.html @@ -0,0 +1,326 @@ + + + + + Low latency streaming - Testplayer + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Live low-latency playback

+ Example showing how to use dash.js to play low latency streams. The low-latency related parameters + can be adjusted in the settings section. For more information checkout the Wiki. + + +
+
+
+
+
+
+

Settings

+
+
+
+
General
+
+ Target latency (sec) + +
+
+ Max drift (sec) + +
+
+ Catch-up playback rate + +
+
+
+
ABR - General
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
ABR - Additional
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
Catchup mechanism
+
+ + +
+
+ + +
+
+ + +
+
Throughput calculation
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ + +
+
+
+
+ +
+
+
+
+ Manifest URL + + +
+
+
+
+
+
+ +
+
+ +
+ 00:00:00 +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+ 00:00:00 +
+
+
+
+
+
+
+
+
+
+
+
Wall Clock reference time
+
+ +
+
+
+
+
Seconds behind live:
+
Video Buffer:
+
Video Index Downloading: /
+
Video Bitrate Downloading kbits/s:
+
Playback rate: +
+
+
+
+
+
+
+
+ +
+
+
+
Chart settings
+
+ + +
+
+ Interval (ms) + +
+
+ Number of data points + +
+ +
+
+
+ © DASH-IF +
+
+
+ + + + diff --git a/samples/module-builds/mediaplayer-only.html b/samples/module-builds/mediaplayer-only.html new file mode 100644 index 0000000000..4ae6496cb0 --- /dev/null +++ b/samples/module-builds/mediaplayer-only.html @@ -0,0 +1,74 @@ + + + + + Mediaplayer only example + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Mediaplayer only

+

A sample showing how to use the dash.mediaplayer bundle. This bundle does not contain any code + for playback of DRM protected content.

+

Note that you need to run "npm build" first when trying + this demo locally.

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/modules/webpack/src/entry.js b/samples/modules/webpack/src/entry.js index 602aeecfec..3b2830a7d5 100644 --- a/samples/modules/webpack/src/entry.js +++ b/samples/modules/webpack/src/entry.js @@ -3,3 +3,4 @@ import { MediaPlayer } from 'dashjs'; let url = "https://dash.akamaized.net/envivio/Envivio-dash2/manifest.mpd"; let player = MediaPlayer().create(); player.initialize(document.querySelector('#myMainVideoPlayer'), url, true); + diff --git a/samples/multi-audio/multi-audio-default-track.html b/samples/multi-audio/multi-audio-default-track.html new file mode 100644 index 0000000000..251b2332f0 --- /dev/null +++ b/samples/multi-audio/multi-audio-default-track.html @@ -0,0 +1,110 @@ + + + + + Multi Audio - Initial Track + + + + + + + + + + + + + +
+
+
+ +
+
+
+ +
+
+
+
+
+

Multi Audio - Initial Track

+

This example illustrates how to select a default audio language for playback.

+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/multi-audio/multi-audio-drm-codec-change.html b/samples/multi-audio/multi-audio-drm-codec-change.html new file mode 100644 index 0000000000..1d721a9950 --- /dev/null +++ b/samples/multi-audio/multi-audio-drm-codec-change.html @@ -0,0 +1,116 @@ + + + + + Multiple audio tracks sample + + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Multiple audio tracks with different codecs

+

This example shows how content with multiple audio tracks with different codecs can be played + back by the dash.js + player. dash.js allows a switch of the audio track during playback.

+
+
+
+
+
+ +
+
+ +
+ 00:00:00 +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+ 00:00:00 +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + diff --git a/samples/multi-audio/multi-audio.html b/samples/multi-audio/multi-audio.html index 0e2afa9e09..768460b692 100644 --- a/samples/multi-audio/multi-audio.html +++ b/samples/multi-audio/multi-audio.html @@ -1,18 +1,32 @@ - + - - Control bar Sample - + + Multiple audio tracks sample + + + + - - + + + + + - - - -
- -
- -
-
- -
- 00:00:00 -
- -
-
- -
- -
- -
-
- -
-
- +
+
+
+ +
+
+
+
+

Multiple audio tracks

+

This example shows how content with multiple audio tracks can be played back by the dash.js + player. dash.js allows a switch of the audio track during playback.

+
- 00:00:00 -
-
-
-
+
+
+
+ +
+
+ +
+ 00:00:00 +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+ 00:00:00 +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ © DASH-IF +
-
+
+ - diff --git a/samples/multiperiod/index.html b/samples/multiperiod/index.html deleted file mode 100644 index 4efbe29de2..0000000000 --- a/samples/multiperiod/index.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - Multiperiod stream example - - - - - - - - - - -
- -
- - - - - - diff --git a/samples/multiperiod/live.html b/samples/multiperiod/live.html new file mode 100644 index 0000000000..f8f2a7e35c --- /dev/null +++ b/samples/multiperiod/live.html @@ -0,0 +1,70 @@ + + + + + Multiperiod Live example + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Multiperiod live example

+

Example showing how dash.js handles live streams with multiple periods. A new period starts every minute.

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/multiperiod/vod.html b/samples/multiperiod/vod.html new file mode 100644 index 0000000000..e9eb526429 --- /dev/null +++ b/samples/multiperiod/vod.html @@ -0,0 +1,70 @@ + + + + + Multiperiod VoD example + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Multiperiod VoD example

+

Example showing how dash.js handles streams with two periods.

+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + + diff --git a/samples/offline/app/main.js b/samples/offline/app/main.js index fca8dbae79..fa171449ad 100644 --- a/samples/offline/app/main.js +++ b/samples/offline/app/main.js @@ -273,8 +273,12 @@ app.controller('DashController', function ($scope, $timeout, $q, sources, contri 'logLevel': dashjs.Debug.LOG_LEVEL_INFO }, 'streaming': { - 'fastSwitchEnabled': $scope.fastSwitchSelected, - 'jumpGaps': true, + 'buffer': { + 'fastSwitchEnabled': $scope.fastSwitchSelected + }, + 'gaps:': { + 'jumpGaps': true, + }, 'abr': { 'autoSwitchBitrate': { 'video': $scope.videoAutoSwitchSelected @@ -303,8 +307,6 @@ app.controller('DashController', function ($scope, $timeout, $q, sources, contri case dashjs.MediaPlayer.errors.MANIFEST_LOADER_PARSING_FAILURE_ERROR_CODE: case dashjs.MediaPlayer.errors.MANIFEST_LOADER_LOADING_FAILURE_ERROR_CODE: case dashjs.MediaPlayer.errors.XLINK_LOADER_LOADING_FAILURE_ERROR_CODE: - case dashjs.MediaPlayer.errors.SEGMENTS_UPDATE_FAILED_ERROR_CODE: - case dashjs.MediaPlayer.errors.SEGMENTS_UNAVAILABLE_ERROR_CODE: case dashjs.MediaPlayer.errors.SEGMENT_BASE_LOADER_ERROR_CODE: case dashjs.MediaPlayer.errors.TIME_SYNC_FAILED_ERROR_CODE: case dashjs.MediaPlayer.errors.FRAGMENT_LOADER_LOADING_FAILURE_ERROR_CODE: @@ -349,14 +351,14 @@ app.controller('DashController', function ($scope, $timeout, $q, sources, contri // get buffer default value var currentConfig = $scope.player.getSettings(); - $scope.defaultLiveDelay = currentConfig.streaming.liveDelay; - $scope.defaultStableBufferDelay = currentConfig.streaming.stableBufferTime; - $scope.defaultBufferTimeAtTopQuality = currentConfig.streaming.bufferTimeAtTopQuality; - $scope.defaultBufferTimeAtTopQualityLongForm = currentConfig.streaming.bufferTimeAtTopQualityLongForm; + $scope.defaultLiveDelay = currentConfig.streaming.delay.liveDelay; + $scope.defaultStableBufferDelay = currentConfig.streaming.buffer.stableBufferTime; + $scope.defaultBufferTimeAtTopQuality = currentConfig.streaming.buffer.bufferTimeAtTopQuality; + $scope.defaultBufferTimeAtTopQualityLongForm = currentConfig.streaming.buffer.bufferTimeAtTopQualityLongForm; $scope.lowLatencyModeSelected = currentConfig.streaming.lowLatencyEnabled; - var initVideoTrackSwitchMode = $scope.player.getTrackSwitchModeFor('video'); - var initAudioTrackSwitchMode = $scope.player.getTrackSwitchModeFor('audio'); + var initVideoTrackSwitchMode = currentConfig.streaming.trackSwitchMode.video; + var initAudioTrackSwitchMode = currentConfig.streaming.trackSwitchMode.audio; //get default track switch mode if (initVideoTrackSwitchMode === 'alwaysReplace') { @@ -427,7 +429,7 @@ app.controller('DashController', function ($scope, $timeout, $q, sources, contri if (e.data) { var session = e.data; if (session.getSessionType() === 'persistent-license') { - $scope.persistentSessionId[$scope.selectedItem.url] = session.getSessionID(); + $scope.persistentSessionId[$scope.selectedItem.url] = session.getSessionId(); } } }, $scope); @@ -496,7 +498,9 @@ app.controller('DashController', function ($scope, $timeout, $q, sources, contri $scope.toggleFastSwitch = function () { $scope.player.updateSettings({ 'streaming': { - 'fastSwitchEnabled': $scope.fastSwitchSelected + 'buffer': { + 'fastSwitchEnabled': $scope.fastSwitchSelected + }, } }); }; @@ -516,7 +520,9 @@ app.controller('DashController', function ($scope, $timeout, $q, sources, contri $scope.toggleScheduleWhilePaused = function () { $scope.player.updateSettings({ 'streaming': { - 'scheduleWhilePaused': $scope.scheduleWhilePausedSelected + 'scheduling': { + 'scheduleWhilePaused': $scope.scheduleWhilePausedSelected + } } }); }; @@ -537,7 +543,9 @@ app.controller('DashController', function ($scope, $timeout, $q, sources, contri $scope.toggleJumpGaps = function () { $scope.player.updateSettings({ 'streaming': { - 'jumpGaps': $scope.jumpGapsSelected + 'gaps': { + 'jumpGaps': $scope.jumpGapsSelected + } } }); }; @@ -589,12 +597,16 @@ app.controller('DashController', function ($scope, $timeout, $q, sources, contri } var config = { - 'streaming': { - 'liveDelay': $scope.defaultLiveDelay, - 'stableBufferTime': $scope.defaultStableBufferDelay, - 'bufferTimeAtTopQuality': $scope.defaultBufferTimeAtTopQuality, - 'bufferTimeAtTopQualityLongForm': $scope.defaultBufferTimeAtTopQualityLongForm, - 'lowLatencyEnabled': $scope.lowLatencyModeSelected + streaming: { + delay: { + liveDelay: $scope.defaultLiveDelay + }, + buffer: { + stableBufferTime: $scope.defaultStableBufferDelay, + bufferTimeAtTopQuality: $scope.defaultBufferTimeAtTopQuality, + bufferTimeAtTopQualityLongForm: $scope.defaultBufferTimeAtTopQualityLongForm, + }, + lowLatencyEnabled: $scope.lowLatencyModeSelected } }; @@ -602,19 +614,19 @@ app.controller('DashController', function ($scope, $timeout, $q, sources, contri var selectedConfig = $scope.selectedItem.bufferConfig; if (selectedConfig.liveDelay) { - config.streaming.liveDelay = selectedConfig.liveDelay; + config.streaming.delay.liveDelay = selectedConfig.liveDelay; } if (selectedConfig.stableBufferTime) { - config.streaming.stableBufferTime = selectedConfig.stableBufferTime; + config.streaming.buffer.stableBufferTime = selectedConfig.stableBufferTime; } if (selectedConfig.bufferTimeAtTopQuality) { - config.streaming.bufferTimeAtTopQuality = selectedConfig.bufferTimeAtTopQuality; + config.streaming.buffer.bufferTimeAtTopQuality = selectedConfig.bufferTimeAtTopQuality; } if (selectedConfig.bufferTimeAtTopQualityLongForm) { - config.streaming.bufferTimeAtTopQualityLongForm = selectedConfig.bufferTimeAtTopQualityLongForm; + config.streaming.buffer.bufferTimeAtTopQualityLongForm = selectedConfig.bufferTimeAtTopQualityLongForm; } if (selectedConfig.lowLatencyMode !== undefined) { @@ -649,7 +661,7 @@ app.controller('DashController', function ($scope, $timeout, $q, sources, contri if ($scope.initialSettings.text) { $scope.player.setTextDefaultLanguage($scope.initialSettings.text); } - $scope.player.setTextDefaultEnabled($scope.initialSettings.textEnabled); + $scope.player.updateSettings({ streaming: { text: { defaultEnabled: $scope.initialSettings.textEnabled } } }); $scope.player.enableForcedTextStreaming($scope.initialSettings.forceTextStreaming); $scope.controlbar.enable(); }; @@ -990,7 +1002,8 @@ app.controller('DashController', function ($scope, $timeout, $q, sources, contri if (vars && vars.hasOwnProperty('stream')) { try { item = JSON.parse(atob(vars.stream)); - } catch (e) { } + } catch (e) { + } } diff --git a/samples/offline/app/rules/DownloadRatioRule.js b/samples/offline/app/rules/DownloadRatioRule.js index 40a60301d1..d3456f10ac 100644 --- a/samples/offline/app/rules/DownloadRatioRule.js +++ b/samples/offline/app/rules/DownloadRatioRule.js @@ -52,7 +52,9 @@ function DownloadRatioRuleClass() { } function getBytesLength(request) { - return request.trace.reduce(function (a, b) { return a + b.b[0] }, 0); + return request.trace.reduce(function (a, b) { + return a + b.b[0] + }, 0); } function getMaxIndex(rulesContext) { @@ -65,7 +67,7 @@ function DownloadRatioRuleClass() { let metrics = metricsModel.getReadOnlyMetricsFor(mediaType); let streamController = StreamController(context).getInstance(); let abrController = rulesContext.getAbrController(); - let current = abrController.getQualityFor(mediaType, streamController.getActiveStreamInfo()); + let current = abrController.getQualityFor(mediaType, streamController.getActiveStreamInfo().id); let requests = dashMetrics.getHttpRequests(metrics), lastRequest = null, @@ -108,7 +110,7 @@ function DownloadRatioRuleClass() { return SwitchRequest(context).create(); } - if(lastRequest.type !== 'MediaSegment' ) { + if (lastRequest.type !== 'MediaSegment') { logger.debug("[CustomRules][" + mediaType + "][DownloadRatioRule] Last request is not a media segment, bailing."); return SwitchRequest(context).create(); } @@ -171,7 +173,7 @@ function DownloadRatioRuleClass() { p = SwitchRequest.PRIORITY.WEAK; logger.debug("[CustomRules] SwitchRequest: q=" + q + "/" + (count - 1) + " (" + bandwidths[q] + ")"/* + ", p=" + p*/); - return SwitchRequest(context).create(q, {name : DownloadRatioRuleClass.__dashjs_factory_name}, p); + return SwitchRequest(context).create(q, { name: DownloadRatioRuleClass.__dashjs_factory_name }, p); } else { for (i = count - 1; i > current; i -= 1) { if (calculatedBandwidth > (bandwidths[i] * switchUpRatioSafetyFactor)) { @@ -184,7 +186,7 @@ function DownloadRatioRuleClass() { p = SwitchRequest.PRIORITY.STRONG; logger.debug("[CustomRules] SwitchRequest: q=" + q + "/" + (count - 1) + " (" + bandwidths[q] + ")"/* + ", p=" + p*/); - return SwitchRequest(context).create(q, {name : DownloadRatioRuleClass.__dashjs_factory_name}, p); + return SwitchRequest(context).create(q, { name: DownloadRatioRuleClass.__dashjs_factory_name }, p); } } diff --git a/samples/offline/dashjs_config.json b/samples/offline/dashjs_config.json index 7e5f612666..eaa6937a62 100644 --- a/samples/offline/dashjs_config.json +++ b/samples/offline/dashjs_config.json @@ -20,15 +20,12 @@ "wallclockTimeUpdateInterval": 50, "lowLatencyEnabled": false, "keepProtectionMediaKeys": false, - "useManifestDateHeaderTimeSource": true, "segmentOverlapToleranceTime": 0.2, "useSuggestedPresentationDelay": false, "manifestUpdateRetryInterval": 100, "liveCatchup": { - "minDrift": 0.02, "maxDrift": 0, "playbackRate": 0.5, - "latencyThreshold": null, "enabled": false }, "lastBitrateCachingInfo": { "enabled": true, "ttl": 360000}, diff --git a/samples/samples.json b/samples/samples.json index 4f21963df9..73bf6f4084 100644 --- a/samples/samples.json +++ b/samples/samples.json @@ -5,54 +5,106 @@ "samples": [ { "title": "Auto load single video src", - "description": "The simplest means of using a dash.js player in a web page. The mpd src is specified within the @src attribute of the video element. The \"auto-load\" refers to the fact that this page calls the Dash.createAll() method onLoad in order to automatically convert all video elements of class 'dashjs-player' in to a functioning DASH player.", + "description": "The simplest means of using a dash.js player in a web page. The mpd src is specified within the @src attribute of the video element.", "href": "getting-started/auto-load-single-video-src.html", - "width": "25rem" + "image": "lib/img/bbb-1.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] }, { "title": "Auto load single video", - "description": "The mpd source is specified within the child Source element of the video element. Note that the Source@type attribute must be set to \"application/dash+xml\" in order for it to be automatically used.", - "href": "getting-started/auto-load-single-video.html" - }, - { - "title": "Auto load single video with reference", - "description": "While the Dash.CreateAll() method is handy for automated instantiation within a page, the Dash.create() method takes three optional parameters to give you more control. This example illustrates calling Dash.create() in four different ways. The first simply specifies a target video element with a child source element. The second specifies a target video element with a src attribute. The third specifies the video element and a dynamically generated source object. The fourth specifies the video element, a source object and a custom DashContext object.", - "href": "getting-started/auto-load-single-video-with-reference.html", - "width": "30rem" + "description": "The mpd source is specified within the child Source element of the video element.", + "href": "getting-started/auto-load-single-video.html", + "image": "lib/img/bbb-2.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] }, { "title": "Auto load multi video", - "description": "This example shows how to auto-embed multiple instances of dash.js players in a page. To make it more difficult, one of the available video elements specifies a non-DASH source.", + "description": "This example shows how to auto-embed multiple instances of dash.js players in a page.", "href": "getting-started/auto-load-multi-video.html", - "width": "25rem" + "image": "lib/img/sintel-2.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] }, { "title": "Manual load single video", - "description": "A sample showing how to load a single video", - "href": "getting-started/manual-load-single-video.html" + "description": "A sample showing how to load a single video.", + "href": "getting-started/manual-load-single-video.html", + "image": "lib/img/bbb-3.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] }, { "title": "Manual load with custom settings", - "description": "A sample showing how to load a video using custom settings", + "description": "A sample showing how to load a video using custom settings.", "href": "getting-started/manual-load-with-custom-settings.html", - "width": "30rem" + "width": "30rem", + "image": "lib/img/bbb-1.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] }, { "title": "Using the Control Bar", "description": "This example shows how to add and configure the Akamai control bar with dash.js player.", - "href": "control/controlbar.html", - "width": "25rem" + "href": "getting-started/controlbar.html", + "width": "25rem", + "image": "lib/img/bbb-4.jpg", + "labels": [ + "VoD", + "Video", + "Audio", + "Controlbar" + ] }, { "title": "Listening to events", "description": "Example showing how to listen to events raised by dash.js.", - "href": "getting-started/listening-to-events.html" + "href": "getting-started/listening-to-events.html", + "image": "lib/img/bbc-1.jpg", + "labels": [ + "VoD", + "Video", + "Audio", + "Events" + ] }, { "title": "Log levels", "description": "This examples shows how to configure dash.js logging levels.", - "href": "control/logging.html", - "width": "30rem" + "href": "getting-started/logging.html", + "image": "lib/img/bbb-2.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] + }, + { + "title": "Load with url parameters", + "description": "A demo page that uses url query parameters to configure the playback.", + "href": "getting-started/load-with-url-params.html?autoplay=true&url=https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd", + "image": "lib/img/bbb-1.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] } ] }, @@ -62,22 +114,57 @@ { "title": "Live delay comparison custom manifest", "description": "Example showing how to use the two MediaPlayer APIS which control live delay: setLiveDelay and setLiveDelayFragmentCount.", - "href": "live-streaming/live-delay-comparison-custom-manifest.html" + "href": "live-streaming/live-delay-comparison-custom-manifest.html", + "image": "lib/img/livesim-1.jpg", + "labels": [ + "Live", + "Video", + "Audio" + ] }, { "title": "Live delay comparison using fragment count", "description": "Example showing the combined effects of segment duration and the setLiveDelayFragmentCount MediaPlayer method on the latency of live stream playback", - "href": "live-streaming/live-delay-comparison-using-fragmentCount.html" + "href": "live-streaming/live-delay-comparison-using-fragmentCount.html", + "image": "lib/img/livesim-1.jpg", + "labels": [ + "Live", + "Video", + "Audio" + ] }, { "title": "Live delay comparison using setLiveDelay", "description": "Example showing the combined effects of segment duration and the setLiveDelay MediaPlayer method on the latency of live stream playback.", - "href": "live-streaming/live-delay-comparison-using-setLiveDelay.html" + "href": "live-streaming/live-delay-comparison-using-setLiveDelay.html", + "image": "lib/img/livesim-1.jpg", + "labels": [ + "Live", + "Video", + "Audio" + ] }, { "title": "Synchronized live playback with the catchup mode", "description": "Example showing a synchronized live playback of two videos using the live playback catchup mode.", - "href": "live-streaming/synchronized-live-playback.html" + "href": "live-streaming/synchronized-live-playback.html", + "image": "lib/img/livesim-1.jpg", + "labels": [ + "Live", + "Video", + "Audio" + ] + }, + { + "title": "Live stream with availabilityTimeOffset", + "description": "Example showing how dash.js handles live streams with an availabilityTimeOffset(ATO)", + "href": "live-streaming/availability-time-offset.html", + "image": "lib/img/livesim-1.jpg", + "labels": [ + "Live", + "Video", + "Audio" + ] } ] }, @@ -85,19 +172,126 @@ "section": "Live Low Latency", "samples": [ { - "title": "Low latency", - "description": "Example showing how to use dash.js to play low latency streams.", - "href": "low-latency/index.html" + "title": "Low latency testplayer", + "description": "Example showing how to use dash.js to play low latency streams. All low latency related settings such as the different ABR algorithms(LoL+,L2A) are selectable.", + "href": "low-latency/testplayer/testplayer.html", + "image": "lib/img/akamai-ll-4.jpg", + "labels": [ + "Live", + "Low Latency", + "Settings", + "Video", + "Audio" + ] + } + ] + }, + { + "section": "ABR", + "samples": [ + { + "title": "Changing the default ABR algorithm", + "description": "This example shows how configure the ABR algorithms in dash.js", + "href": "abr/abr.html", + "image": "lib/img/bbb-2.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] + }, + { + "title": "Custom ABR Rules", + "description": "Example showing how to create and define custom ABR rules in dash.js.", + "href": "abr/custom-abr-rules.html", + "image": "lib/img/bbb-3.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] + }, + { + "title": "Disable ABR", + "description": "Example showing how to disable the ABR switching in dash.js.", + "href": "abr/disable-abr.html", + "image": "lib/img/bbb-1.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] + }, + { + "title": "Initial bitrate", + "description": "Example showing how to set the initial bitrate in dash.js.", + "href": "abr/initial-bitrate.html", + "image": "lib/img/bbb-3.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] + }, + { + "title": "Max/min bitrate", + "description": "Example showing how to set the maximum and minimum bitrate in dash.js.", + "href": "abr/max-min-bitrate.html", + "image": "lib/img/bbb-1.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] + }, + { + "title": "Fast bitrate switch", + "description": "Example showing how to aggressively replace segments in the buffer when switching up in quality.", + "href": "abr/fastswitch.html", + "image": "lib/img/bbb-2.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] + } + ] + }, + { + "section": "Buffer", + "samples": [ + { + "title": "Buffer target", + "description": "Example showing how to define the buffer targets in dash.js", + "href": "buffer/buffer-target.html", + "image": "lib/img/bbb-1.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] }, { - "title": "Low latency with L2ALL", - "description": "Example showing how to use dash.js to play low latency streams using the L2ALL ABR algorithm.", - "href": "low-latency/l2all_index.html" + "title": "Buffer cleanup", + "description": "Example showing how to define the parameters for buffer cleanup/pruning in dash.js", + "href": "buffer/buffer-cleanup.html", + "image": "lib/img/bbb-2.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] }, { - "title": "Low latency with LoL+", - "description": "Example showing how to use dash.js to play low latency streams using the LoL+ ABR algorithm.", - "href": "low-latency/lolp_index.html" + "title": "Initial buffer target", + "description": "Example showing how to define the initial buffer target at playback start in dash.js.", + "href": "buffer/initial-buffer.html", + "image": "lib/img/bbb-3.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] } ] }, @@ -107,53 +301,205 @@ { "title": "Widevine", "description": "This example shows how to use dash.js to play streams with Widevine DRM protection.", - "href": "drm/widevine.html" + "href": "drm/widevine.html", + "image": "lib/img/tos-1.jpg", + "labels": [ + "VoD", + "DRM", + "Widevine", + "Video", + "Audio" + ] }, { "title": "PlayReady", "description": "This example shows how to use dash.js to play streams with PlayReady DRM protection (Windows 10 Microsoft Chromium Edge only).", - "href": "drm/playready.html" + "href": "drm/playready.html", + "image": "lib/img/tos-2.jpg", + "labels": [ + "VoD", + "DRM", + "Playready", + "Video", + "Audio" + ] }, { "title": "ClearKey", - "description": "This example shows how to use dash.js to play streams with ClearKey DRM protection.", - "href": "drm/clearkey.html" + "description": "This example shows how to use dash.js to play streams with ClearKey protection.", + "href": "drm/clearkey.html", + "image": "lib/img/tos-3.jpg", + "labels": [ + "VoD", + "Clearkey", + "Video", + "Audio" + ] }, { "title": "License wrapping", "description": "This example shows how to use dash.js to filter and wrap license requests and responses", - "href": "drm/license-wrapping.html" + "href": "drm/license-wrapping.html", + "image": "lib/img/tos-1.jpg", + "labels": [ + "VoD", + "DRM", + "Widevine", + "Playready", + "Video", + "Audio" + ] + }, + { + "title": "Keysystem priority", + "description": "This example shows how to specify a DRM system priority in case the underlying platform supports multiple DRM systems.", + "href": "drm/system-priority.html", + "image": "lib/img/tos-2.jpg", + "labels": [ + "VoD", + "DRM", + "Widevine", + "Playready", + "Video", + "Audio" + ] + }, + { + "title": "Keysystem string priority", + "description": "This example shows how to specify the system string priority for the call to requestMediaKeySystemAccess. For example, Playready might be supported with the system strings \"com.microsoft.playready.recommendation\" and \"com.microsoft.playready\". ", + "href": "drm/system-string-priority.html", + "image": "lib/img/tos-1.jpg", + "labels": [ + "VoD", + "DRM", + "Widevine", + "Playready", + "Video", + "Audio" + ] + }, + { + "title": "License server via MPD", + "description": "This example shows how to specify the license server url as part of the MPD using 'dashif:laurl'", + "href": "drm/dashif-laurl.html", + "image": "lib/img/tos-3.jpg", + "labels": [ + "VoD", + "DRM", + "Widevine", + "Playready", + "Video", + "Audio" + ] + }, + { + "title": "DRM - Keep MediaKeySession", + "description": "This example shows how the ProtectionController and the created MediaKeys and MediaKeySessions will be preserved during the MediaPlayer lifetime leading to less license requests.", + "href": "drm/keepProtectionKeys.html", + "image": "lib/img/tos-2.jpg", + "labels": [ + "VoD", + "DRM", + "Widevine", + "Playready", + "Video", + "Audio" + ] + }, + { + "title": "DRM Robustness level example", + "description": "This example shows how to define a robustness level to be used by dash.js when calling requestMediaKeySystemAccess", + "href": "drm/robustness-level.html", + "image": "lib/img/tos-1.jpg", + "labels": [ + "VoD", + "DRM", + "Widevine", + "Video", + "Audio" + ] } - ] }, { "section": "Multi Period", "samples": [ { - "title": "Play streams with two periods", + "title": "VoD Multiperiod", "description": "Example showing how dash.js handles streams with two periods.", - "href": "multiperiod/index.html" + "href": "multiperiod/vod.html", + "image": "lib/img/bbb-1.jpg", + "labels": [ + "VoD", + "Multiperiod", + "Video", + "Audio" + ] + }, + { + "title": "Live Multiperiod", + "description": "Example showing how dash.js handles live streams with multiple periods.", + "href": "multiperiod/live.html", + "image": "lib/img/livesim-1.jpg", + "labels": [ + "Live", + "Multiperiod", + "Video", + "Audio" + ] } ] }, { - "section": "Subtitles & Captions", + "section": "Subtitles and Captions", "samples": [ { "title": "Caption VTT", "description": "This example shows how content with VTT captions can be played back by the dash.js player. First captions appear at the 15s mark.", - "href": "captioning/caption_vtt.html" + "href": "captioning/caption_vtt.html", + "image": "lib/img/sintel-1.jpg", + "labels": [ + "VoD", + "External caption", + "Video", + "Audio" + ] + }, + { + "title": "CEA 608/708", + "description": "This example shows how content with embedded CEA 608/708 captions can be played back by the dash.js player.", + "href": "captioning/cea608.html", + "image": "lib/img/sintel-2.jpg", + "labels": [ + "VoD", + "CEA608", + "Video", + "Audio" + ] }, { "title": "Multi Track Captions", "description": "Example showing content with multiple timed text tracks.", - "href": "captioning/multi-track-captions.html" + "href": "captioning/multi-track-captions.html", + "image": "lib/img/sintel-3.jpg", + "labels": [ + "VoD", + "Fragmented text", + "Video", + "Audio" + ] }, { "title": "TTML EBU timed text tracks", "description": "Example showing content with TTML EBU timed text tracks.", - "href": "captioning/ttml-ebutt-sample.html" + "href": "captioning/ttml-ebutt-sample.html", + "image": "lib/img/elephant-1.jpg", + "labels": [ + "VoD", + "Fragmented text", + "Video", + "Audio" + ] } ] }, @@ -163,7 +509,42 @@ { "title": "Multiple audio tracks", "description": "This example shows how content with multiple audio tracks can be played back by the dash.js player. dash.js allows a switch of the audio track during playback.", - "href": "multi-audio/multi-audio.html" + "href": "multi-audio/multi-audio.html", + "image": "lib/img/tos-2.jpg", + "labels": [ + "VoD", + "Multi Audio", + "Fragmented Text", + "Video", + "Audio" + ] + }, + { + "title": "Multiple audio tracks with different codecs", + "description": "This example shows how content with multiple audio tracks with different codecs can be played back by the dash.js player. dash.js allows a switch of the audio track during playback.", + "href": "multi-audio/multi-audio-drm-codec-change.html", + "image": "lib/img/google-1.jpg", + "labels": [ + "VoD", + "DRM", + "Multi Audio", + "Fragmented Text", + "Video", + "Audio" + ] + }, + { + "title": "Multi Audio - Initial Track", + "description": "This example illustrates how to select a default audio language for playback.", + "href": "multi-audio/multi-audio-default-track.html", + "image": "lib/img/tos-3.jpg", + "labels": [ + "VoD", + "Multi Audio", + "Fragmented Text", + "Video", + "Audio" + ] } ] }, @@ -173,17 +554,14 @@ { "title": "Thumbnails", "description": "Example showing how to use streams with thumbnails representations.", - "href": "thumbnails/thumbnails.html" - } - ] - }, - { - "section": "Preload", - "samples": [ - { - "title": "Preload video", - "description": "This example shows how to use preload feature of dash.js, which allows to initialize streaming and starts downloading the content before the player is attached to a video element.", - "href": "getting-started/pre-load-video.html" + "href": "thumbnails/thumbnails.html", + "image": "lib/img/bbb-2.jpg", + "labels": [ + "VoD", + "Thumbnails", + "Video", + "Audio" + ] } ] }, @@ -191,9 +569,13 @@ "section": "Audio only", "samples": [ { - "title": "Only audio stream", + "title": "Audio only stream", "description": "This example shows how to play audio-only streams in dash.js.", - "href": "audio-only/index.html" + "href": "audio-only/index.html", + "labels": [ + "VoD", + "Audio" + ] } ] }, @@ -203,42 +585,126 @@ { "title": "Monitoring the stream", "description": "This example shows how to monitor metrics of the streams played by dash.js.", - "href": "advanced/monitoring.html" - }, - { - "title": "Dash.js settings", - "description": "This example shows how to deal with dash.js settings.", - "href": "advanced/settings.html" + "href": "advanced/monitoring.html", + "image": "lib/img/bbb-1.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] }, { "title": "Listening to SCTE-EMSG Events", "description": "Example showing how to listen to SCTE EMSG events raised by dash.js.", - "href": "advanced/listening-to-SCTE-EMSG-events.html" + "href": "advanced/listening-to-SCTE-EMSG-events.html", + "image": "lib/img/livesim-1.jpg", + "labels": [ + "Live", + "Events", + "Video", + "Audio" + ] }, { "title": "Autoplay Browser policy", "description": "This sample shows how to deal with autoplay browsers policy. It uses an event listener to detect when auto playback is interrupted by the browser and how to recover from this situation muting audio.", - "href": "advanced/auto-play-browser-policy.html" - }, - { - "title": "Custom ABR Rules", - "description": "Example showing how to create and define custom ABR rules in dash.js.", - "href": "advanced/abr/index.html" + "href": "advanced/auto-play-browser-policy.html", + "image": "lib/img/bbb-2.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] }, { "title": "Extending Dash.js", "description": "This sample shows how to use dash.js extend mechanism to add custom HTTP headers and modify URL's of the requests done by the player.", - "href": "advanced/extend.html" + "href": "advanced/extend.html", + "image": "lib/img/bbb-4.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] }, { "title": "CMCD Reporting", "description": "This sample shows how to use dash.js in order to enhance requests to the CDN with Common Media Client Data (CMCD - CTA 5005).", - "href": "advanced/cmcd.html" + "href": "advanced/cmcd.html", + "image": "lib/img/bbb-1.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] }, { "title": "Custom Capabilities Filters", "description": "This sample shows how to filter representations.", - "href": "advanced/custom-capabilities-filters.html" + "href": "advanced/custom-capabilities-filters.html", + "image": "lib/img/bbb-2.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] + }, + { + "title": "Custom initial track selection example", + "description": "This sample shows how to define your own initial track selection function.", + "href": "advanced/custom-initial-track-selection.html", + "image": "lib/img/google-1.jpg", + "labels": [ + "VoD", + "Video", + "Audio", + "DRM", + "Widevine" + ] + }, + { + "title": "Load with a parsed manifest", + "description": "This sample shows how to load the manifest as a parsed object instead of providing a url to the manifest", + "href": "advanced/load_with_manifest.html", + "image": "lib/img/bbb-2.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] + }, + { + "title": "MPD anchors", + "description": "This sample shows how to use MPD anchors to start a presentation at a given time.", + "href": "advanced/mpd-anchors.html", + "image": "lib/img/bbb-3.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] + }, + { + "title": "Manual load with start time", + "description": "A sample showing how to initialize playback at a specific start time.", + "href": "advanced/load-with-starttime.html", + "image": "lib/img/bbb-4.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] + }, + { + "title": "Content Steering", + "description": "A sample illustrating content steering to dynamically switch between CDNs. Note that you need to provide a manifest with a valid content steering server.", + "href": "advanced/content-steering.html", + "image": "lib/img/bbb-2.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] } ] }, @@ -248,7 +714,14 @@ { "title": "Offline mode", "description": "Example showing how store and read back streams without network connection to Dash", - "href": "offline/index.html" + "href": "offline/index.html", + "labels": [ + "VoD", + "Live", + "Offline", + "Video", + "Audio" + ] } ] }, @@ -258,7 +731,29 @@ { "title": "Microsoft Smooth Streaming", "description": "Example showing how to use dash.js to play Microsoft Smooth Streaming streams.", - "href": "smooth-streaming/mss.html" + "href": "smooth-streaming/mss.html", + "image": "lib/img/mss-1.jpg", + "labels": [ + "MSS", + "Video", + "Audio" + ] + } + ] + }, + { + "section": "Module builds", + "samples": [ + { + "title": "Mediaplayer only", + "description": "A sample showing how to use the dash.mediaplayer bundle. This bundle does not contain any code for playback of DRM protected content.", + "href": "module-builds/mediaplayer-only.html", + "image": "lib/img/bbb-1.jpg", + "labels": [ + "Module", + "Video", + "Audio" + ] } ] } diff --git a/samples/smooth-streaming/mss.html b/samples/smooth-streaming/mss.html index 334914547b..5855ff62e2 100644 --- a/samples/smooth-streaming/mss.html +++ b/samples/smooth-streaming/mss.html @@ -1,56 +1,24 @@ - + - - + Smooth Streaming example, single videoElement - - - + + + + + - - - -
-
- - -
-
- - -
-
- -
-
-
- -
- - + + - \ No newline at end of file +
+
+
+ +
+
+
+
+

Smooth Streaming example

+

Example showing how to use dash.js to play Microsoft Smooth Streaming streams.

+
+
+
+
+
+
+ Manifest URL + +
+
+ License Server Url: + +
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + + + diff --git a/samples/thumbnails/thumbnails.html b/samples/thumbnails/thumbnails.html index 4bc49fef49..17fd78824d 100644 --- a/samples/thumbnails/thumbnails.html +++ b/samples/thumbnails/thumbnails.html @@ -1,23 +1,32 @@ - + - - Thumbnails Dash Demo - - - + + Thumbnails sample + + + + + + - - - - + + + - - - -
-
- -
-
- -
- 00:00:00 -
- -
-
- -
- -
- -
-
- -
-
- -
- 00:00:00 -
-
-
-
+
+
+
+ +
+
+
+
+

Thumbnails

+

Example showing how to use streams with thumbnail representations. The thumbnail representation can be dynamically selected in the bitrate selection menu.

+
+
+
+
+
+ +
+
+ +
+ 00:00:00 +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+ 00:00:00 +
+
+
+
+
+
+
+
+
+
-
-
-
-
- - +
+
+
+
+
+
+ © DASH-IF +
+
+
+ + + diff --git a/src/core/Settings.js b/src/core/Settings.js index cc83ae1e86..7946ece6fb 100644 --- a/src/core/Settings.js +++ b/src/core/Settings.js @@ -33,6 +33,8 @@ import Utils from './Utils.js'; import Debug from '../core/Debug'; import Constants from '../streaming/constants/Constants'; import {HTTPRequest} from '../streaming/vo/metrics/HTTPRequest'; +import EventBus from './EventBus'; +import Events from './events/Events'; /** @module Settings * @description Define the configuration parameters of Dash.js MediaPlayer. @@ -44,126 +46,282 @@ import {HTTPRequest} from '../streaming/vo/metrics/HTTPRequest'; * @typedef {Object} PlayerSettings * @property {module:Settings~DebugSettings} [debug] * Debug related settings. + * @property {module:Settings~ErrorSettings} [errors] + * Error related settings * @property {module:Settings~StreamingSettings} [streaming] * Streaming related settings. * @example * * // Full settings object * settings = { - * debug: { - * logLevel: Debug.LOG_LEVEL_WARNING, - * dispatchEvent: false - * }, - * streaming: { - * metricsMaxListDepth: 1000, - * abandonLoadTimeout: 10000, - * liveDelayFragmentCount: NaN, - * liveDelay: null, - * scheduleWhilePaused: true, - * fastSwitchEnabled: false, - * flushBufferAtTrackSwitch: false, - * calcSegmentAvailabilityRangeFromTimeline: false, - * reuseExistingSourceBuffers: true, - * bufferPruningInterval: 10, - * bufferToKeep: 20, - * jumpGaps: true, - * jumpLargeGaps: true, - * smallGapLimit: 1.5, - * stableBufferTime: 12, - * bufferTimeAtTopQuality: 30, - * bufferTimeAtTopQualityLongForm: 60, - * longFormContentDurationThreshold: 600, - * wallclockTimeUpdateInterval: 50, - * lowLatencyEnabled: false, - * keepProtectionMediaKeys: false, - * useManifestDateHeaderTimeSource: true, - * useSuggestedPresentationDelay: true, - * useAppendWindow: true, - * manifestUpdateRetryInterval: 100, - * stallThreshold: 0.5, - * filterUnsupportedEssentialProperties: true, - * eventControllerRefreshDelay: 100, - * utcSynchronization: { - * backgroundAttempts: 2, - * timeBetweenSyncAttempts: 30, - * maximumTimeBetweenSyncAttempts: 600, - * minimumTimeBetweenSyncAttempts: 2, - * timeBetweenSyncAttemptsAdjustmentFactor: 2, - * maximumAllowedDrift: 100, - * enableBackgroundSyncAfterSegmentDownloadError: true, - * defaultTimingSource: { - * scheme: 'urn:mpeg:dash:utc:http-xsdate:2014', - * value: 'http://time.akamai.com/?iso&ms' - * } + * debug: { + * logLevel: Debug.LOG_LEVEL_WARNING, + * dispatchEvent: false + * }, + * streaming: { + * abandonLoadTimeout: 10000, + * wallclockTimeUpdateInterval: 100, + * manifestUpdateRetryInterval: 100, + * cacheInitSegments: true, + * applyServiceDescription: true, + * applyProducerReferenceTime: true, + * applyContentSteering: true, + * eventControllerRefreshDelay: 100, + * enableManifestDurationMismatchFix: true, + * capabilities: { + * filterUnsupportedEssentialProperties: true, + * useMediaCapabilitiesApi: false, + * replaceCodecs: [] + * }, + * timeShiftBuffer: { + * calcFromSegmentTimeline: false, + * fallbackToSegmentTimeline: true + * }, + * metrics: { + * maxListDepth: 100 + * }, + * delay: { + * liveDelayFragmentCount: NaN, + * liveDelay: NaN, + * useSuggestedPresentationDelay: true + * }, + * protection: { + * keepProtectionMediaKeys: false, + * ignoreEmeEncryptedEvent: false, + * detectPlayreadyMessageFormat: true, + * }, + * buffer: { + * enableSeekDecorrelationFix: true, + * fastSwitchEnabled: true, + * flushBufferAtTrackSwitch: false, + * reuseExistingSourceBuffers: true, + * bufferPruningInterval: 10, + * bufferToKeep: 20, + * bufferTimeAtTopQuality: 30, + * bufferTimeAtTopQualityLongForm: 60, + * initialBufferLevel: NaN, + * stableBufferTime: 12, + * longFormContentDurationThreshold: 600, + * stallThreshold: 0.5, + * useAppendWindow: true, + * setStallState: true, + * avoidCurrentTimeRangePruning: false + * enableLiveSeekableRangeFix: true + * }, + * gaps: { + * jumpGaps: true, + * jumpLargeGaps: true, + * smallGapLimit: 1.5, + * threshold: 0.3, + * enableSeekFix: true, + * enableStallFix: false, + * stallSeek: 0.1 + * }, + * utcSynchronization: { + * enabled: true, + * useManifestDateHeaderTimeSource: true, + * backgroundAttempts: 2, + * timeBetweenSyncAttempts: 30, + * maximumTimeBetweenSyncAttempts: 600, + * minimumTimeBetweenSyncAttempts: 2, + * timeBetweenSyncAttemptsAdjustmentFactor: 2, + * maximumAllowedDrift: 100, + * enableBackgroundSyncAfterSegmentDownloadError: true, + * defaultTimingSource: { + * scheme: 'urn:mpeg:dash:utc:http-xsdate:2014', + * value: 'http://time.akamai.com/?iso&ms' + * } + * }, + * scheduling: { + * defaultTimeout: 300, + * lowLatencyTimeout: 100, + * scheduleWhilePaused: true + * }, + * text: { + * defaultEnabled: true + * }, + * liveCatchup: { + * maxDrift: NaN, + * playbackRate: NaN, + * playbackBufferMin: 0.5, + * enabled: false, + * mode: Constants.LIVE_CATCHUP_MODE_DEFAULT + * }, + * lastBitrateCachingInfo: { enabled: true, ttl: 360000 }, + * lastMediaSettingsCachingInfo: { enabled: true, ttl: 360000 }, + * cacheLoadThresholds: { video: 50, audio: 5 }, + * trackSwitchMode: { + * audio: Constants.TRACK_SWITCH_MODE_ALWAYS_REPLACE, + * video: Constants.TRACK_SWITCH_MODE_NEVER_REPLACE + * }, + * selectionModeForInitialTrack: Constants.TRACK_SELECTION_MODE_HIGHEST_SELECTION_PRIORITY, + * fragmentRequestTimeout: 0, + * retryIntervals: { + * [HTTPRequest.MPD_TYPE]: 500, + * [HTTPRequest.XLINK_EXPANSION_TYPE]: 500, + * [HTTPRequest.MEDIA_SEGMENT_TYPE]: 1000, + * [HTTPRequest.INIT_SEGMENT_TYPE]: 1000, + * [HTTPRequest.BITSTREAM_SWITCHING_SEGMENT_TYPE]: 1000, + * [HTTPRequest.INDEX_SEGMENT_TYPE]: 1000, + * [HTTPRequest.MSS_FRAGMENT_INFO_SEGMENT_TYPE]: 1000, + * [HTTPRequest.LICENSE]: 1000, + * [HTTPRequest.OTHER_TYPE]: 1000, + * lowLatencyReductionFactor: 10 + * }, + * retryAttempts: { + * [HTTPRequest.MPD_TYPE]: 3, + * [HTTPRequest.XLINK_EXPANSION_TYPE]: 1, + * [HTTPRequest.MEDIA_SEGMENT_TYPE]: 3, + * [HTTPRequest.INIT_SEGMENT_TYPE]: 3, + * [HTTPRequest.BITSTREAM_SWITCHING_SEGMENT_TYPE]: 3, + * [HTTPRequest.INDEX_SEGMENT_TYPE]: 3, + * [HTTPRequest.MSS_FRAGMENT_INFO_SEGMENT_TYPE]: 3, + * [HTTPRequest.LICENSE]: 3, + * [HTTPRequest.OTHER_TYPE]: 3, + * lowLatencyMultiplyFactor: 5 + * }, + * abr: { + * movingAverageMethod: Constants.MOVING_AVERAGE_SLIDING_WINDOW, + * ABRStrategy: Constants.ABR_STRATEGY_DYNAMIC, + * additionalAbrRules: { + * insufficientBufferRule: false, + * switchHistoryRule: true, + * droppedFramesRule: true, + * abandonRequestsRule: false + * }, + * bandwidthSafetyFactor: 0.9, + * useDefaultABRRules: true, + * useDeadTimeLatency: true, + * limitBitrateByPortal: false, + * usePixelRatioInLimitBitrateByPortal: false, + * maxBitrate: { audio: -1, video: -1 }, + * minBitrate: { audio: -1, video: -1 }, + * maxRepresentationRatio: { audio: 1, video: 1 }, + * initialBitrate: { audio: -1, video: -1 }, + * initialRepresentationRatio: { audio: -1, video: -1 }, + * autoSwitchBitrate: { audio: true, video: true }, + * fetchThroughputCalculationMode: Constants.ABR_FETCH_THROUGHPUT_CALCULATION_DOWNLOADED_DATA + * }, + * cmcd: { + * enabled: false, + * sid: null, + * cid: null, + * rtp: null, + * rtpSafetyFactor: 5, + * mode: Constants.CMCD_MODE_QUERY, + * enabledKeys: ['br', 'd', 'ot', 'tb' , 'bl', 'dl', 'mtp', 'nor', 'nrr', 'su' , 'bs', 'rtp' , 'cid', 'pr', 'sf', 'sid', 'st', 'v'] + * } * }, - * liveCatchup: { - * minDrift: 0.02, - * maxDrift: 0, - * playbackRate: 0.5, - * latencyThreshold: NaN, - * playbackBufferMin: NaN, - * enabled: false, - * mode: Constants.LIVE_CATCHUP_MODE_DEFAULT - * }, - * lastBitrateCachingInfo: { enabled: true, ttl: 360000 }, - * lastMediaSettingsCachingInfo: { enabled: true, ttl: 360000 }, - * cacheLoadThresholds: { video: 50, audio: 5 }, - * trackSwitchMode: { - * audio: Constants.TRACK_SWITCH_MODE_ALWAYS_REPLACE, - * video: Constants.TRACK_SWITCH_MODE_NEVER_REPLACE - * }, - * selectionModeForInitialTrack: Constants.TRACK_SELECTION_MODE_HIGHEST_BITRATE, - * fragmentRequestTimeout: 0, - * retryIntervals: { - * MPD: 500, - * XLinkExpansion: 500, - * InitializationSegment: 1000, - * IndexSegment: 1000, - * MediaSegment: 1000, - * BitstreamSwitchingSegment: 1000, - * FragmentInfoSegment: 1000, - * other: 1000, - * lowLatencyReductionFactor: 10 - * }, - * retryAttempts: { - * MPD: 3, - * XLinkExpansion: 1, - * InitializationSegment: 3, - * IndexSegment: 3, - * MediaSegment: 3, - * BitstreamSwitchingSegment: 3, - * FragmentInfoSegment: 3, - * other: 3, - * lowLatencyMultiplyFactor: 5 - * }, - * abr: { - * movingAverageMethod: Constants.MOVING_AVERAGE_SLIDING_WINDOW, - * ABRStrategy: Constants.ABR_STRATEGY_DYNAMIC, - * bandwidthSafetyFactor: 0.9, - * useDefaultABRRules: true, - * useDeadTimeLatency: true, - * limitBitrateByPortal: false, - * usePixelRatioInLimitBitrateByPortal: false, - * maxBitrate: { audio: -1, video: -1 }, - * minBitrate: { audio: -1, video: -1 }, - * maxRepresentationRatio: { audio: 1, video: 1 }, - * initialBitrate: { audio: -1, video: -1 }, - * initialRepresentationRatio: { audio: -1, video: -1 }, - * autoSwitchBitrate: { audio: true, video: true }, - * fetchThroughputCalculationMode: Constants.ABR_FETCH_THROUGHPUT_CALCULATION_DOWNLOADED_DATA - * }, - * cmcd: { - * enabled: false, - * sid: null, - * cid: null, - * rtp: null, - * rtpSafetyFactor: 5, - * mode: Constants.CMCD_MODE_QUERY + * errors: { + * recoverAttempts: { + * mediaErrorDecode: 5 + * } * } - * } * } */ +/** + * @typedef {Object} TimeShiftBuffer + * @property {boolean} [calcFromSegmentTimeline=false] + * Enable calculation of the DVR window for SegmentTimeline manifests based on the entries in \. + * * @property {boolean} [fallbackToSegmentTimeline=true] + * In case the MPD uses \. + */ + +/** + * @typedef {Object} LiveDelay + * @property {number} [liveDelayFragmentCount=NaN] + * Changing this value will lower or increase live stream latency. + * + * The detected segment duration will be multiplied by this value to define a time in seconds to delay a live stream from the live edge. + * + * Lowering this value will lower latency but may decrease the player's ability to build a stable buffer. + * @property {number} [liveDelay] + * Equivalent in seconds of setLiveDelayFragmentCount. + * + * Lowering this value will lower latency but may decrease the player's ability to build a stable buffer. + * + * This value should be less than the manifest duration by a couple of segment durations to avoid playback issues. + * + * If set, this parameter will take precedence over setLiveDelayFragmentCount and manifest info. + * @property {boolean} [useSuggestedPresentationDelay=true] + * Set to true if you would like to overwrite the default live delay and honor the SuggestedPresentationDelay attribute in by the manifest. + */ + +/** + * @typedef {Object} Buffer + * @property {boolean} [enableSeekDecorrelationFix=false] + * Enables a workaround for playback start on some devices, e.g. WebOS 4.9. + * It is necessary because some browsers do not support setting currentTime on video element to a value that is outside of current buffer. + * + * If you experience unexpected seeking triggered by BufferController, you can try setting this value to false. + + * @property {boolean} [fastSwitchEnabled=true] + * When enabled, after an ABR up-switch in quality, instead of requesting and appending the next fragment at the end of the current buffer range it is requested and appended closer to the current time. + * + * When enabled, The maximum time to render a higher quality is current time + (1.5 * fragment duration). + * + * Note, When ABR down-switch is detected, we appended the lower quality at the end of the buffer range to preserve the + * higher quality media for as long as possible. + * + * If enabled, it should be noted there are a few cases when the client will not replace inside buffer range but rather just append at the end. + * 1. When the buffer level is less than one fragment duration. + * 2. The client is in an Abandonment State due to recent fragment abandonment event. + * + * Known issues: + * 1. In IE11 with auto switching off, if a user switches to a quality they can not download in time the fragment may be appended in the same range as the playhead or even in the past, in IE11 it may cause a stutter or stall in playback. + * @property {boolean} [flushBufferAtTrackSwitch=false] + * When enabled, after a track switch and in case buffer is being replaced, the video element is flushed (seek at current playback time) once a segment of the new track is appended in buffer in order to force video decoder to play new track. + * + * This can be required on some devices like GoogleCast devices to make track switching functional. + * + * Otherwise track switching will be effective only once after previous buffered track is fully consumed. + * @property {boolean} [reuseExistingSourceBuffers=true] + * Enable reuse of existing MediaSource Sourcebuffers during period transition. + * @property {number} [bufferPruningInterval=10] + * The interval of pruning buffer in seconds. + * @property {number} [bufferToKeep=20] + * This value influences the buffer pruning logic. + * + * Allows you to modify the buffer that is kept in source buffer in seconds. + * 0|-----------bufferToPrune-----------|-----bufferToKeep-----|currentTime| + * @property {number} [bufferTimeAtTopQuality=30] + * The time that the internal buffer target will be set to once playing the top quality. + * + * If there are multiple bitrates in your adaptation, and the media is playing at the highest bitrate, then we try to build a larger buffer at the top quality to increase stability and to maintain media quality. + * @property {number} [bufferTimeAtTopQualityLongForm=60] + * The time that the internal buffer target will be set to once playing the top quality for long form content. + * @property {number} [longFormContentDurationThreshold=600] + * The threshold which defines if the media is considered long form content. + * + * This will directly affect the buffer targets when playing back at the top quality. + * @property {number} [initialBufferLevel=NaN] + * Initial buffer level before playback starts + * @property {number} [stableBufferTime=12] + * The time that the internal buffer target will be set to post startup/seeks (NOT top quality). + * + * When the time is set higher than the default you will have to wait longer to see automatic bitrate switches but will have a larger buffer which will increase stability. + * @property {number} [stallThreshold=0.3] + * Stall threshold used in BufferController.js to determine whether a track should still be changed and which buffer range to prune. + * @property {boolean} [useAppendWindow=true] + * Specifies if the appendWindow attributes of the MSE SourceBuffers should be set according to content duration from manifest. + * @property {boolean} [setStallState=true] + * Specifies if we fire manual waiting events once the stall threshold is reached + * @property {boolean} [avoidCurrentTimeRangePruning=false] + * Avoids pruning of the buffered range that contains the current playback time. + * + * That buffered range is likely to have been enqueued for playback. Pruning it causes a flush and reenqueue in WPE and WebKitGTK based browsers. This stresses the video decoder and can cause stuttering on embedded platforms. + * @property {boolean} [enableLiveSeekableRangeFix=true] + * Sets `mediaSource.duration` when live seekable range changes if `mediaSource.setLiveSeekableRange` is unavailable. + */ + +/** + * @typedef {Object} module:Settings~AudioVideoSettings + * @property {number|boolean|string} [audio] + * Configuration for audio media type of tracks. + * @property {number|boolean|string} [video] + * Configuration for video media type of tracks. + */ /** * @typedef {Object} DebugSettings @@ -198,6 +356,188 @@ import {HTTPRequest} from '../streaming/vo/metrics/HTTPRequest'; * Note this will be dispatched regardless of log level. */ +/** + * @typedef {Object} module:Settings~ErrorSettings + * @property {object} [recoverAttempts={mediaErrorDecode: 5}] + * Defines the maximum number of recover attempts for specific media errors. + * + * For mediaErrorDecode the player will reset the MSE and skip the blacklisted segment that caused the decode error. The resulting gap will be handled by the GapController. + */ + +/** + * @typedef {Object} CachingInfoSettings + * @property {boolean} [enable] + * Enable or disable the caching feature. + * @property {number} [ttl] + * Time to live. + * + * A value defined in milliseconds representing how log to cache the settings for. + */ + +/** + * @typedef {Object} Gaps + * @property {boolean} [jumpGaps=true] + * Sets whether player should jump small gaps (discontinuities) in the buffer. + * @property {boolean} [jumpLargeGaps=true] + * Sets whether player should jump large gaps (discontinuities) in the buffer. + * @property {number} [smallGapLimit=1.5] + * Time in seconds for a gap to be considered small. + * @property {number} [threshold=0.3] + * Threshold at which the gap handling is executed. If currentRangeEnd - currentTime < threshold the gap jump will be triggered. + * For live stream the jump might be delayed to keep a consistent live edge. + * Note that the amount of buffer at which platforms automatically stall might differ. + * @property {boolean} [enableSeekFix=true] + * Enables the adjustment of the seek target once no valid segment request could be generated for a specific seek time. This can happen if the user seeks to a position for which there is a gap in the timeline. + * @property {boolean} [enableStallFix=false] + * If playback stalled in a buffered range this fix will perform a seek by the value defined in stallSeek to trigger playback again. + * @property {number} [stallSeek=0.1] + * Value to be used in case enableStallFix is set to true + */ + +/** + * @typedef {Object} UtcSynchronizationSettings + * @property {boolean} [enabled=true] + * Enables or disables the UTC clock synchronization + * @property {boolean} [useManifestDateHeaderTimeSource=true] + * Allows you to enable the use of the Date Header, if exposed with CORS, as a timing source for live edge detection. + * + * The use of the date header will happen only after the other timing source that take precedence fail or are omitted as described. + * @property {number} [backgroundAttempts=2] + * Number of synchronization attempts to perform in the background after an initial synchronization request has been done. This is used to verify that the derived client-server offset is correct. + * + * The background requests are async and done in parallel to the start of the playback. + * + * This value is also used to perform a resync after 404 errors on segments. + * @property {number} [timeBetweenSyncAttempts=30] + * The time in seconds between two consecutive sync attempts. + * + * Note: This value is used as an initial starting value. The internal value of the TimeSyncController is adjusted during playback based on the drift between two consecutive synchronization attempts. + * + * Note: A sync is only performed after an MPD update. In case the @minimumUpdatePeriod is larger than this value the sync will be delayed until the next MPD update. + * @property {number} [maximumTimeBetweenSyncAttempts=600] + * The maximum time in seconds between two consecutive sync attempts. + * + * @property {number} [minimumTimeBetweenSyncAttempts=2] + * The minimum time in seconds between two consecutive sync attempts. + * + * @property {number} [timeBetweenSyncAttemptsAdjustmentFactor=2] + * The factor used to multiply or divide the timeBetweenSyncAttempts parameter after a sync. The maximumAllowedDrift defines whether this value is used as a factor or a dividend. + * + * @property {number} [maximumAllowedDrift=100] + * The maximum allowed drift specified in milliseconds between two consecutive synchronization attempts. + * + * @property {boolean} [enableBackgroundSyncAfterSegmentDownloadError=true] + * Enables or disables the background sync after the player ran into a segment download error. + * + * @property {object} [defaultTimingSource={scheme:'urn:mpeg:dash:utc:http-xsdate:2014',value: 'http://time.akamai.com/?iso&ms'}] + * The default timing source to be used. The timing sources in the MPD take precedence over this one. + */ + +/** + * @typedef {Object} Scheduling + * @property {number} [defaultTimeout=300] + * Default timeout between two consecutive segment scheduling attempts + * @property {number} [lowLatencyTimeout] + * Default timeout between two consecutive low-latency segment scheduling attempts + * @property {boolean} [scheduleWhilePaused=true] + * Set to true if you would like dash.js to keep downloading fragments in the background when the video element is paused. + */ + +/** + * @typedef {Object} Text + * @property {number} [defaultEnabled=true] + * Enable/disable subtitle rendering by default. + */ + +/** + * @typedef {Object} LiveCatchupSettings + * @property {number} [maxDrift=NaN] + * Use this method to set the maximum latency deviation allowed before dash.js to do a seeking to live position. + * + * In low latency mode, when the difference between the measured latency and the target one, as an absolute number, is higher than the one sets with this method, then dash.js does a seek to live edge position minus the target live delay. + * + * LowLatencyMaxDriftBeforeSeeking should be provided in seconds. + * + * If 0, then seeking operations won't be used for fixing latency deviations. + * + * Note: Catch-up mechanism is only applied when playing low latency live streams. + * @property {number} [playbackRate=NaN] + * Use this parameter to set the maximum catch up rate, as a percentage, for low latency live streams. + * + * In low latency mode, when measured latency is higher/lower than the target one, dash.js increases/decreases playback rate respectively up to (+/-) the percentage defined with this method until target is reached. + * + * Valid values for catch up rate are in range 0-0.5 (0-50%). + * + * Set it to NaN to turn off live catch up feature. + * + * Note: Catch-up mechanism is only applied when playing low latency live streams. + * @property {number} [playbackBufferMin=NaN] + * Use this parameter to specify the minimum buffer which is used for LoL+ based playback rate reduction. + * + * + * @property {boolean} [enabled=false] + * Use this parameter to enable the catchup mode for non low-latency streams. + * + * @property {string} [mode="liveCatchupModeDefault"] + * Use this parameter to switch between different catchup modes. + * + * Options: "liveCatchupModeDefault" or "liveCatchupModeLOLP". + * + * Note: Catch-up mechanism is automatically applied when playing low latency live streams. + */ + +/** + * @typedef {Object} RequestTypeSettings + * @property {number} [MPD] + * Manifest type of requests. + * @property {number} [XLinkExpansion] + * XLink expansion type of requests. + * @property {number} [InitializationSegment] + * Request to retrieve an initialization segment. + * @property {number} [IndexSegment] + * Request to retrieve an index segment (SegmentBase). + * @property {number} [MediaSegment] + * Request to retrieve a media segment (video/audio/image/text chunk). + * @property {number} [BitstreamSwitchingSegment] + * Bitrate stream switching type of request. + * @property {number} [FragmentInfoSegment] + * Request to retrieve a FragmentInfo segment (specific to Smooth Streaming live streams). + * @property {number} [other] + * Other type of request. + * @property {number} [lowLatencyReductionFactor] + * For low latency mode, values of type of request are divided by lowLatencyReductionFactor. + * + * Note: It's not type of request. + * @property {number} [lowLatencyMultiplyFactor] + * For low latency mode, values of type of request are multiplied by lowLatencyMultiplyFactor. + * + * Note: It's not type of request. + */ + +/** + * @typedef {Object} Protection + * @property {boolean} [keepProtectionMediaKeys=false] + * Set the value for the ProtectionController and MediaKeys life cycle. + * + * If true, the ProtectionController and then created MediaKeys and MediaKeySessions will be preserved during the MediaPlayer lifetime. + * @property {boolean} ignoreEmeEncryptedEvent + * If set to true the player will ignore "encrypted" and "needkey" events thrown by the EME. + * @property {boolean} detectPlayreadyMessageFormat + * If set to true the player will use the raw unwrapped message from the Playready CDM + * @property {boolean} downgradePlayReadyPSSH + * If set to true the player will downgrade v1 PSSH boxes to v0. + */ + +/** + * @typedef {Object} Capabilities + * @property {boolean} [filterUnsupportedEssentialProperties=true] + * Enable to filter all the AdaptationSets and Representations which contain an unsupported \ element. + * @property {boolean} [useMediaCapabilitiesApi=false] + * Enable to use the MediaCapabilities API to check whether codecs are supported. If disabled MSE.isTypeSupported will be used instead. + * @property {Array.<[string, string]>} [replaceCodecs=[]] + * List of codecs to be replaced. + */ + /** * @typedef {Object} AbrSettings * @property {string} [movingAverageMethod="slidingWindow"] @@ -222,6 +562,9 @@ import {HTTPRequest} from '../streaming/vo/metrics/HTTPRequest'; * This allows a fast reaction to a bandwidth drop and prevents oscillations on bandwidth spikes. * @property {string} [ABRStrategy="abrDynamic"] * Returns the current ABR strategy being used: "abrDynamic", "abrBola" or "abrThroughput". + * @property {object} [trackSwitchMode={video: "neverReplace", audio: "alwaysReplace"}] + * @property {object} [additionalAbrRules={insufficientBufferRule: false,switchHistoryRule: true,droppedFramesRule: true,abandonRequestsRule: false}] + * Enable/Disable additional ABR rules in case ABRStrategy is set to "abrDynamic", "abrBola" or "abrThroughput". * @property {number} [bandwidthSafetyFactor=0.9] * Standard ABR throughput rules multiply the throughput by this value. * @@ -239,13 +582,13 @@ import {HTTPRequest} from '../streaming/vo/metrics/HTTPRequest'; * * Useful on, for example, retina displays. * @property {module:Settings~AudioVideoSettings} [maxBitrate={audio: -1, video: -1}] - * The maximum bitrate that the ABR algorithms will choose. + * The maximum bitrate that the ABR algorithms will choose. This value is specified in kbps. * - * Use NaN for no limit. + * Use -1 for no limit. * @property {module:Settings~AudioVideoSettings} [minBitrate={audio: -1, video: -1}] - * The minimum bitrate that the ABR algorithms will choose. + * The minimum bitrate that the ABR algorithms will choose. This value is specified in kbps. * - * Use NaN for no limit. + * Use -1 for no limit. * @property {module:Settings~AudioVideoSettings} [maxRepresentationRatio={audio: 1, video: 1}] * When switching multi-bitrate content (auto or manual mode) this property specifies the maximum representation allowed, as a proportion of the size of the representation set. * @@ -257,7 +600,9 @@ import {HTTPRequest} from '../streaming/vo/metrics/HTTPRequest'; * * This feature is typically used to reserve higher representations for playback only when connected over a fast connection. * @property {module:Settings~AudioVideoSettings} [initialBitrate={audio: -1, video: -1}] - * Explicitly set the starting bitrate for audio or video. + * Explicitly set the starting bitrate for audio or video. This value is specified in kbps. + * + * Use -1 to let the player decide. * @property {module:Settings~AudioVideoSettings} [initialRepresentationRatio={audio: -1, video: -1}] * Explicitly set the initial representation ratio. * @@ -272,105 +617,73 @@ import {HTTPRequest} from '../streaming/vo/metrics/HTTPRequest'; */ /** - * @typedef {Object} StreamingSettings - * @property {number} [metricsMaxListDepth=1000] - * Maximum list depth of metrics. - * @property {number} [abandonLoadTimeout=10000] - * A timeout value in seconds, which during the ABRController will block switch-up events. - * - * This will only take effect after an abandoned fragment event occurs. - * @property {number} [liveDelayFragmentCount=NaN] - * Changing this value will lower or increase live stream latency. - * - * The detected segment duration will be multiplied by this value to define a time in seconds to delay a live stream from the live edge. - * - * Lowering this value will lower latency but may decrease the player's ability to build a stable buffer. - * @property {number} [liveDelay] - * Equivalent in seconds of setLiveDelayFragmentCount. - * - * Lowering this value will lower latency but may decrease the player's ability to build a stable buffer. - * - * This value should be less than the manifest duration by a couple of segment durations to avoid playback issues. - * - * If set, this parameter will take precedence over setLiveDelayFragmentCount and manifest info. - * @property {boolean} [scheduleWhilePaused=true] - * Set to true if you would like dash.js to keep downloading fragments in the background when the video element is paused. - * @property {boolean} [fastSwitchEnabled=false] - * When enabled, after an ABR up-switch in quality, instead of requesting and appending the next fragment at the end of the current buffer range it is requested and appended closer to the current time. - * - * When enabled, The maximum time to render a higher quality is current time + (1.5 * fragment duration). - * - * Note, When ABR down-switch is detected, we appended the lower quality at the end of the buffer range to preserve the - * higher quality media for as long as possible. - * - * If enabled, it should be noted there are a few cases when the client will not replace inside buffer range but rather just append at the end. - * 1. When the buffer level is less than one fragment duration. - * 2. The client is in an Abandonment State due to recent fragment abandonment event. + * @typedef {Object} module:Settings~CmcdSettings + * @property {boolean} [enable=false] + * Enable or disable the CMCD reporting. + * @property {string} [sid] + * GUID identifying the current playback session. * - * Known issues: - * 1. In IE11 with auto switching off, if a user switches to a quality they can not download in time the fragment may be appended in the same range as the playhead or even in the past, in IE11 it may cause a stutter or stall in playback. - * @property {boolean} [flushBufferAtTrackSwitch=false] - * When enabled, after a track switch and in case buffer is being replaced (see MediaPlayer.setTrackSwitchModeFor(Constants.TRACK_SWITCH_MODE_ALWAYS_REPLACE)), the video element is flushed (seek at current playback time) once a segment of the new track is appended in buffer in order to force video decoder to play new track. + * Should be in UUID format. * - * This can be required on some devices like GoogleCast devices to make track switching functional. + * If not specified a UUID will be automatically generated. + * @property {string} [cid] + * A unique string to identify the current content. * - * Otherwise track switching will be effective only once after previous buffered track is fully consumed. - * @property {boolean} [calcSegmentAvailabilityRangeFromTimeline=false] - * Enable calculation of the DVR window for SegmentTimeline manifests based on the entries in \. - * @property {boolean} [reuseExistingSourceBuffers=true] - * Enable reuse of existing MediaSource Sourcebuffers during period transition. - * @property {number} [bufferPruningInterval=10] - * The interval of pruning buffer in seconds. - * @property {number} [bufferToKeep=20] - * This value influences the buffer pruning logic. + * If not specified it will be a hash of the MPD url. + * @property {number} [rtp] + * The requested maximum throughput that the client considers sufficient for delivery of the asset. * - * Allows you to modify the buffer that is kept in source buffer in seconds. - * 0|-----------bufferToPrune-----------|-----bufferToKeep-----|currentTime| - * @property {boolean} [jumpGaps=true] - * Sets whether player should jump small gaps (discontinuities) in the buffer. - * @property {boolean} [jumpLargeGaps=true] - * Sets whether player should jump large gaps (discontinuities) in the buffer. - * @property {number} [smallGapLimit=1.8] - * Time in seconds for a gap to be considered small. - * @property {number} [stableBufferTime=12] - * The time that the internal buffer target will be set to post startup/seeks (NOT top quality). + * If not specified this value will be dynamically calculated in the CMCDModel based on the current buffer level. + * @property {number} [rtpSafetyFactor] + * This value is used as a factor for the rtp value calculation: rtp = minBandwidth * rtpSafetyFactor * - * When the time is set higher than the default you will have to wait longer to see automatic bitrate switches but will have a larger buffer which will increase stability. - * @property {number} [bufferTimeAtTopQuality=30] - * The time that the internal buffer target will be set to once playing the top quality. + * If not specified this value defaults to 5. Note that this value is only used when no static rtp value is defined. + * @property {number} [mode] + * The method to use to attach cmcd metrics to the requests. 'query' to use query parameters, 'header' to use http headers. * - * If there are multiple bitrates in your adaptation, and the media is playing at the highest bitrate, then we try to build a larger buffer at the top quality to increase stability and to maintain media quality. - * @property {number} [bufferTimeAtTopQualityLongForm=60] - * The time that the internal buffer target will be set to once playing the top quality for long form content. - * @property {number} [longFormContentDurationThreshold=600] - * The threshold which defines if the media is considered long form content. + * If not specified this value defaults to 'query'. + * @property {Array.} [enabledKeys] + * This value is used to specify the desired CMCD parameters. Parameters not included in this list are not reported. + */ + +/** + * @typedef {Object} Metrics + * @property {number} [metricsMaxListDepth=100] + * Maximum number of metrics that are persisted per type. + */ + +/** + * @typedef {Object} StreamingSettings + * @property {number} [abandonLoadTimeout=10000] + * A timeout value in seconds, which during the ABRController will block switch-up events. * - * This will directly affect the buffer targets when playing back at the top quality. + * This will only take effect after an abandoned fragment event occurs. * @property {number} [wallclockTimeUpdateInterval=50] * How frequently the wallclockTimeUpdated internal event is triggered (in milliseconds). - * @property {boolean} [lowLatencyEnabled=false] - * Enable or disable low latency mode. - * @property {boolean} [keepProtectionMediaKeys=false] - * Set the value for the ProtectionController and MediaKeys life cycle. - * - * If true, the ProtectionController and then created MediaKeys and MediaKeySessions will be preserved during the MediaPlayer lifetime. - * @property {boolean} [useManifestDateHeaderTimeSource=true] - * Allows you to enable the use of the Date Header, if exposed with CORS, as a timing source for live edge detection. - * - * The use of the date header will happen only after the other timing source that take precedence fail or are omitted as described. - * @property {boolean} [useSuggestedPresentationDelay=true] - * Set to true if you would like to override the default live delay and honor the SuggestedPresentationDelay attribute in by the manifest. - * @property {boolean} [useAppendWindow=true] - * Specifies if the appendWindow attributes of the MSE SourceBuffers should be set according to content duration from manifest. * @property {number} [manifestUpdateRetryInterval=100] * For live streams, set the interval-frequency in milliseconds at which dash.js will check if the current manifest is still processed before downloading the next manifest once the minimumUpdatePeriod time has. - * @property {number} [stallThreshold=0.5] - * Stall threshold used in BufferController.js to determine whether a track should still be changed and which buffer range to prune. - * @property {boolean} [filterUnsupportedEssentialProperties=true] - * Enable to filter all the AdaptationSets and Representations which contain an unsupported \ element. + * @property {boolean} [cacheInitSegments=true] + * Enables the caching of init segments to avoid requesting the init segments before each representation switch. + * @property {boolean} [applyServiceDescription=true] + * Set to true if dash.js should use the parameters defined in ServiceDescription elements + * @property {boolean} [applyProducerReferenceTime=true] + * Set to true if dash.js should use the parameters defined in ProducerReferenceTime elements in combination with ServiceDescription elements. + * @property {boolean} [applyContentSteering=true] + * Set to true if dash.js should apply content steering during playback. * @property {number} [eventControllerRefreshDelay=100] + * For multi-period streams, overwrite the manifest mediaPresentationDuration attribute with the sum of period durations if the manifest mediaPresentationDuration is greater than the sum of period durations + * @property {boolean} [enableManifestDurationMismatchFix=true] * Defines the delay in milliseconds between two consecutive checks for events to be fired. + * @property {module:Settings~Metrics} metrics Metric settings + * @property {module:Settings~LiveDelay} delay Live Delay settings + * @property {module:Settings~TimeShiftBuffer} timeShiftBuffer TimeShiftBuffer settings + * @property {module:Settings~Protection} protection DRM related settings + * @property {module:Settings~Capabilities} capabilities Capability related settings + * @property {module:Settings~Buffer} buffer Buffer related settings + * @property {module:Settings~Gaps} gaps Gap related settings * @property {module:Settings~UtcSynchronizationSettings} utcSynchronization Settings related to UTC clock synchronization + * @property {module:Settings~Scheduling} scheduling Settings related to segment scheduling + * @property {module:Settings~Text} text Settings related to Subtitles and captions * @property {module:Settings~LiveCatchupSettings} liveCatchup Settings related to live catchup. * @property {module:Settings~CachingInfoSettings} [lastBitrateCachingInfo={enabled: true, ttl: 360000}] * Set to false if you would like to disable the last known bit rate from being stored during playback and used to set the initial bit rate for subsequent playback within the expiration window. @@ -378,12 +691,6 @@ import {HTTPRequest} from '../streaming/vo/metrics/HTTPRequest'; * The default expiration is one hour, defined in milliseconds. * * If expired, the default initial bit rate (closest to 1000 kbps) will be used for that session and a new bit rate will be stored during that session. - * @property {module:Settings~CachingInfoSettings} [lastMediaSettingsCachingInfo={enabled: true, ttl: 360000}] - * Set to false if you would like to disable the last known lang for audio (or camera angle for video) from being stored during playback and used to set the initial settings for subsequent playback within the expiration window. - * - * The default expiration is one hour, defined in milliseconds. - * - * If expired, the default settings will be used for that session and a new settings will be stored during that session. * @property {module:Settings~AudioVideoSettings} [cacheLoadThresholds={video: 50, audio: 5}] * For a given media type, the threshold which defines if the response to a fragment request is coming from browser cache or not. * @property {module:Settings~AudioVideoSettings} [trackSwitchMode={video: "neverReplace", audio: "alwaysReplace"}] @@ -402,8 +709,11 @@ import {HTTPRequest} from '../streaming/vo/metrics/HTTPRequest'; * * Possible values * + * - Constants.TRACK_SELECTION_MODE_HIGHEST_SELECTION_PRIORITY + * This mode makes the player select the track with the highest selectionPriority as defined in the manifest. If not selectionPriority is given we fallback to TRACK_SELECTION_MODE_HIGHEST_BITRATE. This mode is a default mode. + * * - Constants.TRACK_SELECTION_MODE_HIGHEST_BITRATE - * This mode makes the player select the track with a highest bitrate. This mode is a default mode. + * This mode makes the player select the track with a highest bitrate. * * - Constants.TRACK_SELECTION_MODE_FIRST_TRACK * This mode makes the player select the first track found in the manifest. @@ -433,170 +743,6 @@ import {HTTPRequest} from '../streaming/vo/metrics/HTTPRequest'; * Settings related to Common Media Client Data reporting. */ -/** - * @typedef {Object} CachingInfoSettings - * @property {boolean} [enable] - * Enable or disable the caching feature. - * @property {number} [ttl] - * Time to live. - * - * A value defined in milliseconds representing how log to cache the settings for. - */ - -/** - * @typedef {Object} module:Settings~AudioVideoSettings - * @property {number|boolean|string} [audio] - * Configuration for audio media type of tracks. - * @property {number|boolean|string} [video] - * Configuration for video media type of tracks. - */ - -/** - * @typedef {Object} RequestTypeSettings - * @property {number} [MPD] - * Manifest type of requests. - * @property {number} [XLinkExpansion] - * XLink expansion type of requests. - * @property {number} [InitializationSegment] - * Request to retrieve an initialization segment. - * @property {number} [IndexSegment] - * Request to retrieve an index segment (SegmentBase). - * @property {number} [MediaSegment] - * Request to retrieve a media segment (video/audio/image/text chunk). - * @property {number} [BitstreamSwitchingSegment] - * Bitrate stream switching type of request. - * @property {number} [FragmentInfoSegment] - * Request to retrieve a FragmentInfo segment (specific to Smooth Streaming live streams). - * @property {number} [other] - * Other type of request. - * @property {number} [lowLatencyReductionFactor] - * For low latency mode, values of type of request are divided by lowLatencyReductionFactor. - * - * Note: It's not type of request. - * @property {number} [lowLatencyMultiplyFactor] - * For low latency mode, values of type of request are multiplied by lowLatencyMultiplyFactor. - * - * Note: It's not type of request. - */ - -/** - * @typedef {Object} module:Settings~CmcdSettings - * @property {boolean} [enable=false] - * Enable or disable the CMCD reporting. - * @property {string} [sid] - * GUID identifying the current playback session. - * - * Should be in UUID format. - * - * If not specified a UUID will be automatically generated. - * @property {string} [cid] - * A unique string to identify the current content. - * - * If not specified it will be a hash of the MPD url. - * @property {number} [rtp] - * The requested maximum throughput that the client considers sufficient for delivery of the asset. - * - * If not specified this value will be dynamically calculated in the CMCDModel based on the current buffer level. - * @property {number} [rtpSafetyFactor] - * This value is used as a factor for the rtp value calculation: rtp = minBandwidth * rtpSafetyFactor - * - * If not specified this value defaults to 5. Note that this value is only used when no static rtp value is defined. - * @property {number} [mode] - * The method to use to attach cmcd metrics to the requests. 'query' to use query parameters, 'header' to use http headers. - * - * If not specified this value defaults to 'query'. - */ - -/** - * @typedef {Object} module:Settings~UtcSynchronizationSettings - * @property {number} [backgroundAttempts=2] - * Number of synchronization attempts to perform in the background after an initial synchronization request has been done. This is used to verify that the derived client-server offset is correct. - * - * The background requests are async and done in parallel to the start of the playback. - * - * This value is also used to perform a resync after 404 errors on segments. - * @property {number} [timeBetweenSyncAttempts=30] - * The time in seconds between two consecutive sync attempts. - * - * Note: This value is used as an initial starting value. The internal value of the TimeSyncController is adjusted during playback based on the drift between two consecutive synchronization attempts. - * - * Note: A sync is only performed after an MPD update. In case the @minimumUpdatePeriod is larger than this value the sync will be delayed until the next MPD update. - * @property {number} [maximumTimeBetweenSyncAttempts=600] - * The maximum time in seconds between two consecutive sync attempts. - * - * @property {number} [minimumTimeBetweenSyncAttempts=2] - * The minimum time in seconds between two consecutive sync attempts. - * - * @property {number} [timeBetweenSyncAttemptsAdjustmentFactor=2] - * The factor used to multiply or divide the timeBetweenSyncAttempts parameter after a sync. The maximumAllowedDrift defines whether this value is used as a factor or a dividend. - * - * @property {number} [maximumAllowedDrift=100] - * The maximum allowed drift specified in milliseconds between two consecutive synchronization attempts. - * - * @property {boolean} [enableBackgroundSyncAfterSegmentDownloadError=true] - * Enables or disables the background sync after the player ran into a segment download error. - * - * @property {object} [defaultTimingSource={scheme:'urn:mpeg:dash:utc:http-xsdate:2014',value: 'http://time.akamai.com/?iso&ms'}] - * The default timing source to be used. The timing sources in the MPD take precedence over this one. - */ - -/** - * @typedef {Object} module:Settings~LiveCatchupSettings - * @property {number} [minDrift=0.02] - * Use this method to set the minimum latency deviation allowed before activating catch-up mechanism. - * - * In low latency mode, when the difference between the measured latency and the target one, as an absolute number, is higher than the one sets with this method, then dash.js increases/decreases playback rate until target latency is reached. - * - * LowLatencyMinDrift should be provided in seconds, and it uses values between 0.0 and 0.5. - * - * Note: Catch-up mechanism is only applied when playing low latency live streams. - * @property {number} [maxDrift=0] - * Use this method to set the maximum latency deviation allowed before dash.js to do a seeking to live position. - * - * In low latency mode, when the difference between the measured latency and the target one, as an absolute number, is higher than the one sets with this method, then dash.js does a seek to live edge position minus the target live delay. - * - * LowLatencyMaxDriftBeforeSeeking should be provided in seconds. - * - * If 0, then seeking operations won't be used for fixing latency deviations. - * - * Note: Catch-up mechanism is only applied when playing low latency live streams. - * @property {number} [playbackRate=0.5] - * Use this parameter to set the maximum catch up rate, as a percentage, for low latency live streams. - * - * In low latency mode, when measured latency is higher/lower than the target one, dash.js increases/decreases playback rate respectively up to (+/-) the percentage defined with this method until target is reached. - * - * Valid values for catch up rate are in range 0-0.5 (0-50%). - * - * Set it to 0 to turn off live catch up feature. - * - * Note: Catch-up mechanism is only applied when playing low latency live streams. - * @property {number} [latencyThreshold=NaN] - * Use this parameter to set the maximum threshold for which live catch up is applied. - * - * For instance, if this value is set to 8 seconds, then live catchup is only applied if the current live latency is equal or below 8 seconds. - * - * The reason behind this parameter is to avoid an increase of the playback rate if the user seeks within the DVR window. - * - * If no value is specified this will be twice the maximum live delay. - * - * The maximum live delay is either specified in the manifest as part of a ServiceDescriptor or calculated the following: - * maximumLiveDelay = targetDelay + liveCatchupMinDrift. - * - * @property {number} [playbackBufferMin=NaN] - * Use this parameter to specify the minimum buffer which is used for LoL+ based playback rate reduction. - * - * - * @property {boolean} [enabled=false] - * Use this parameter to enable the catchup mode for non low-latency streams. - * - * @property {string} [mode="liveCatchupModeDefault"] - * Use this parameter to switch between different catchup modes. - * - * Options: "liveCatchupModeDefault" or "liveCatchupModeLOLP". - * - * Note: Catch-up mechanism is automatically applied when playing low latency live streams. - */ - /** * @class @@ -604,6 +750,14 @@ import {HTTPRequest} from '../streaming/vo/metrics/HTTPRequest'; */ function Settings() { let instance; + const context = this.context; + const eventBus = EventBus(context).getInstance(); + const DISPATCH_KEY_MAP = { + 'streaming.delay.liveDelay': Events.SETTING_UPDATED_LIVE_DELAY, + 'streaming.delay.liveDelayFragmentCount': Events.SETTING_UPDATED_LIVE_DELAY_FRAGMENT_COUNT, + 'streaming.liveCatchup.enabled': Events.SETTING_UPDATED_CATCHUP_ENABLED + }; + /** * @const {PlayerSettings} defaultSettings @@ -615,35 +769,68 @@ function Settings() { dispatchEvent: false }, streaming: { - metricsMaxListDepth: 1000, abandonLoadTimeout: 10000, - liveDelayFragmentCount: NaN, - liveDelay: null, - scheduleWhilePaused: true, - fastSwitchEnabled: false, - flushBufferAtTrackSwitch: false, - calcSegmentAvailabilityRangeFromTimeline: false, - reuseExistingSourceBuffers: true, - bufferPruningInterval: 10, - bufferToKeep: 20, - jumpGaps: true, - jumpLargeGaps: true, - smallGapLimit: 1.5, - stableBufferTime: 12, - bufferTimeAtTopQuality: 30, - bufferTimeAtTopQualityLongForm: 60, - longFormContentDurationThreshold: 600, - wallclockTimeUpdateInterval: 50, - lowLatencyEnabled: false, - keepProtectionMediaKeys: false, - useManifestDateHeaderTimeSource: true, - useSuggestedPresentationDelay: true, - useAppendWindow: true, + wallclockTimeUpdateInterval: 100, manifestUpdateRetryInterval: 100, - stallThreshold: 0.5, - filterUnsupportedEssentialProperties: true, + cacheInitSegments: false, + applyServiceDescription: true, + applyProducerReferenceTime: true, + applyContentSteering: true, eventControllerRefreshDelay: 100, + enableManifestDurationMismatchFix: true, + capabilities: { + filterUnsupportedEssentialProperties: true, + useMediaCapabilitiesApi: false, + replaceCodecs: [] + }, + timeShiftBuffer: { + calcFromSegmentTimeline: false, + fallbackToSegmentTimeline: true + }, + metrics: { + maxListDepth: 100 + }, + delay: { + liveDelayFragmentCount: NaN, + liveDelay: NaN, + useSuggestedPresentationDelay: true + }, + protection: { + keepProtectionMediaKeys: false, + ignoreEmeEncryptedEvent: false, + detectPlayreadyMessageFormat: true, + downgradePlayReadyPSSH: false + }, + buffer: { + enableSeekDecorrelationFix: false, + fastSwitchEnabled: true, + flushBufferAtTrackSwitch: false, + reuseExistingSourceBuffers: true, + bufferPruningInterval: 10, + bufferToKeep: 20, + bufferTimeAtTopQuality: 30, + bufferTimeAtTopQualityLongForm: 60, + initialBufferLevel: NaN, + stableBufferTime: 12, + longFormContentDurationThreshold: 600, + stallThreshold: 0.3, + useAppendWindow: true, + setStallState: true, + avoidCurrentTimeRangePruning: false, + enableLiveSeekableRangeFix: true + }, + gaps: { + jumpGaps: true, + jumpLargeGaps: true, + smallGapLimit: 1.5, + threshold: 0.3, + enableSeekFix: true, + enableStallFix: false, + stallSeek: 0.1 + }, utcSynchronization: { + enabled: true, + useManifestDateHeaderTimeSource: true, backgroundAttempts: 2, timeBetweenSyncAttempts: 30, maximumTimeBetweenSyncAttempts: 600, @@ -653,27 +840,42 @@ function Settings() { enableBackgroundSyncAfterSegmentDownloadError: true, defaultTimingSource: { scheme: 'urn:mpeg:dash:utc:http-xsdate:2014', - value: 'http://time.akamai.com/?iso&ms' + value: 'https://time.akamai.com/?iso&ms' } }, + scheduling: { + defaultTimeout: 500, + lowLatencyTimeout: 0, + scheduleWhilePaused: true + }, + text: { + defaultEnabled: true + }, liveCatchup: { - minDrift: 0.02, - maxDrift: 0, - playbackRate: 0.5, - latencyThreshold: 60, + maxDrift: NaN, + playbackRate: NaN, playbackBufferMin: 0.5, - enabled: false, + enabled: null, mode: Constants.LIVE_CATCHUP_MODE_DEFAULT }, - lastBitrateCachingInfo: { enabled: true, ttl: 360000 }, - lastMediaSettingsCachingInfo: { enabled: true, ttl: 360000 }, - cacheLoadThresholds: { video: 50, audio: 5 }, + lastBitrateCachingInfo: { + enabled: true, + ttl: 360000 + }, + lastMediaSettingsCachingInfo: { + enabled: true, + ttl: 360000 + }, + cacheLoadThresholds: { + video: 50, + audio: 5 + }, trackSwitchMode: { audio: Constants.TRACK_SWITCH_MODE_ALWAYS_REPLACE, video: Constants.TRACK_SWITCH_MODE_NEVER_REPLACE }, - selectionModeForInitialTrack: Constants.TRACK_SELECTION_MODE_HIGHEST_BITRATE, - fragmentRequestTimeout: 0, + selectionModeForInitialTrack: Constants.TRACK_SELECTION_MODE_HIGHEST_SELECTION_PRIORITY, + fragmentRequestTimeout: 20000, retryIntervals: { [HTTPRequest.MPD_TYPE]: 500, [HTTPRequest.XLINK_EXPANSION_TYPE]: 500, @@ -682,6 +884,7 @@ function Settings() { [HTTPRequest.BITSTREAM_SWITCHING_SEGMENT_TYPE]: 1000, [HTTPRequest.INDEX_SEGMENT_TYPE]: 1000, [HTTPRequest.MSS_FRAGMENT_INFO_SEGMENT_TYPE]: 1000, + [HTTPRequest.LICENSE]: 1000, [HTTPRequest.OTHER_TYPE]: 1000, lowLatencyReductionFactor: 10 }, @@ -693,24 +896,49 @@ function Settings() { [HTTPRequest.BITSTREAM_SWITCHING_SEGMENT_TYPE]: 3, [HTTPRequest.INDEX_SEGMENT_TYPE]: 3, [HTTPRequest.MSS_FRAGMENT_INFO_SEGMENT_TYPE]: 3, + [HTTPRequest.LICENSE]: 3, [HTTPRequest.OTHER_TYPE]: 3, lowLatencyMultiplyFactor: 5 }, abr: { movingAverageMethod: Constants.MOVING_AVERAGE_SLIDING_WINDOW, ABRStrategy: Constants.ABR_STRATEGY_DYNAMIC, + additionalAbrRules: { + insufficientBufferRule: true, + switchHistoryRule: true, + droppedFramesRule: true, + abandonRequestsRule: true + }, bandwidthSafetyFactor: 0.9, useDefaultABRRules: true, useDeadTimeLatency: true, limitBitrateByPortal: false, usePixelRatioInLimitBitrateByPortal: false, - maxBitrate: { audio: -1, video: -1 }, - minBitrate: { audio: -1, video: -1 }, - maxRepresentationRatio: { audio: 1, video: 1 }, - initialBitrate: { audio: -1, video: -1 }, - initialRepresentationRatio: { audio: -1, video: -1 }, - autoSwitchBitrate: { audio: true, video: true }, - fetchThroughputCalculationMode: Constants.ABR_FETCH_THROUGHPUT_CALCULATION_DOWNLOADED_DATA + maxBitrate: { + audio: -1, + video: -1 + }, + minBitrate: { + audio: -1, + video: -1 + }, + maxRepresentationRatio: { + audio: 1, + video: 1 + }, + initialBitrate: { + audio: -1, + video: -1 + }, + initialRepresentationRatio: { + audio: -1, + video: -1 + }, + autoSwitchBitrate: { + audio: true, + video: true + }, + fetchThroughputCalculationMode: Constants.ABR_FETCH_THROUGHPUT_CALCULATION_MOOF_PARSING }, cmcd: { enabled: false, @@ -718,7 +946,13 @@ function Settings() { cid: null, rtp: null, rtpSafetyFactor: 5, - mode: Constants.CMCD_MODE_QUERY + mode: Constants.CMCD_MODE_QUERY, + enabledKeys: ['br', 'd', 'ot', 'tb' , 'bl', 'dl', 'mtp', 'nor', 'nrr', 'su' , 'bs', 'rtp' , 'cid', 'pr', 'sf', 'sid', 'st', 'v'] + } + }, + errors: { + recoverAttempts: { + mediaErrorDecode: 5 } } }; @@ -731,11 +965,16 @@ function Settings() { for (let n in source) { if (source.hasOwnProperty(n)) { if (dest.hasOwnProperty(n)) { - if (typeof source[n] === 'object' && source[n] !== null) { + if (typeof source[n] === 'object' && !(source[n] instanceof Array) && source[n] !== null) { mixinSettings(source[n], dest[n], path.slice() + n + '.'); } else { dest[n] = Utils.clone(source[n]); + if (DISPATCH_KEY_MAP[path + n]) { + eventBus.trigger(DISPATCH_KEY_MAP[path + n]); + } } + } else { + console.error('Settings parameter ' + path + n + ' is not supported'); } } } @@ -778,9 +1017,9 @@ function Settings() { } instance = { - get: get, - update: update, - reset: reset + get, + update, + reset }; return instance; diff --git a/src/core/Utils.js b/src/core/Utils.js index b5bda4f2ff..59962fdd1b 100644 --- a/src/core/Utils.js +++ b/src/core/Utils.js @@ -34,6 +34,9 @@ * @ignore */ +import path from 'path-browserify' +import { UAParser } from 'ua-parser-js' + class Utils { static mixin(dest, source, copy) { let s; @@ -96,7 +99,7 @@ class Utils { } } - static parseHttpHeaders (headerStr) { + static parseHttpHeaders(headerStr) { let headers = {}; if (!headerStr) { return headers; @@ -139,6 +142,51 @@ class Utils { } return hash; } + + /** + * Compares both urls and returns a relative url (target relative to original) + * @param {string} original + * @param {string} target + * @return {string|*} + */ + static getRelativeUrl(originalUrl, targetUrl) { + try { + const original = new URL(originalUrl); + const target = new URL(targetUrl); + + // Unify the protocol to compare the origins + original.protocol = target.protocol; + if (original.origin !== target.origin) { + return targetUrl; + } + + // Use the relative path implementation of the path library. We need to cut off the actual filename in the end to get the relative path + let relativePath = path.relative(original.pathname.substr(0, original.pathname.lastIndexOf('/')), target.pathname.substr(0, target.pathname.lastIndexOf('/'))); + + // In case the relative path is empty (both path are equal) return the filename only. Otherwise add a slash in front of the filename + const startIndexOffset = relativePath.length === 0 ? 1 : 0; + relativePath += target.pathname.substr(target.pathname.lastIndexOf('/') + startIndexOffset, target.pathname.length - 1); + + // Build the other candidate, e.g. the 'host relative' path that starts with "/", and return the shortest of the two candidates. + if (target.pathname.length < relativePath.length) { + return target.pathname; + } + return relativePath; + } catch (e) { + return targetUrl + } + } + + static parseUserAgent(ua = null) { + try { + const uaString = ua === null ? typeof navigator !== 'undefined' ? navigator.userAgent.toLowerCase() : '' : ''; + + return UAParser(uaString); + } + catch(e) { + return {}; + } + } } export default Utils; diff --git a/src/core/Version.js b/src/core/Version.js index 9a1788de24..3e2467474b 100644 --- a/src/core/Version.js +++ b/src/core/Version.js @@ -1,4 +1,4 @@ -const VERSION = '3.2.2'; +const VERSION = '__VERSION__'; export function getVersionString() { return VERSION; } diff --git a/src/core/errors/Errors.js b/src/core/errors/Errors.js index c3485b5496..f14488d4c4 100644 --- a/src/core/errors/Errors.js +++ b/src/core/errors/Errors.js @@ -36,64 +36,118 @@ import ErrorsBase from './ErrorsBase'; class Errors extends ErrorsBase { constructor () { super(); + /** * Error code returned when a manifest parsing error occurs */ this.MANIFEST_LOADER_PARSING_FAILURE_ERROR_CODE = 10; + /** * Error code returned when a manifest loading error occurs */ this.MANIFEST_LOADER_LOADING_FAILURE_ERROR_CODE = 11; + /** * Error code returned when a xlink loading error occurs */ this.XLINK_LOADER_LOADING_FAILURE_ERROR_CODE = 12; + /** - * Error code returned when the update of segments list has failed + * Error code returned when no segment ranges could be determined from the sidx box */ - this.SEGMENTS_UPDATE_FAILED_ERROR_CODE = 13; - this.SEGMENTS_UNAVAILABLE_ERROR_CODE = 14; this.SEGMENT_BASE_LOADER_ERROR_CODE = 15; + + /** + * Error code returned when the time synchronization failed + */ this.TIME_SYNC_FAILED_ERROR_CODE = 16; + + /** + * Error code returned when loading a fragment failed + */ this.FRAGMENT_LOADER_LOADING_FAILURE_ERROR_CODE = 17; + + /** + * Error code returned when the FragmentLoader did not receive a request object + */ this.FRAGMENT_LOADER_NULL_REQUEST_ERROR_CODE = 18; + + /** + * Error code returned when the BaseUrl resolution failed + */ this.URL_RESOLUTION_FAILED_GENERIC_ERROR_CODE = 19; + + /** + * Error code returned when the append operation in the SourceBuffer failed + */ this.APPEND_ERROR_CODE = 20; + + /** + * Error code returned when the remove operation in the SourceBuffer failed + */ this.REMOVE_ERROR_CODE = 21; + + /** + * Error code returned when updating the internal objects after loading an MPD failed + */ this.DATA_UPDATE_FAILED_ERROR_CODE = 22; + /** * Error code returned when MediaSource is not supported by the browser */ this.CAPABILITY_MEDIASOURCE_ERROR_CODE = 23; + /** * Error code returned when Protected contents are not supported */ this.CAPABILITY_MEDIAKEYS_ERROR_CODE = 24; + /** + * Error code returned when loading the manifest failed + */ this.DOWNLOAD_ERROR_ID_MANIFEST_CODE = 25; + /** + * Error code returned when loading the sidx failed + */ this.DOWNLOAD_ERROR_ID_SIDX_CODE = 26; + + /** + * Error code returned when loading the media content failed + */ this.DOWNLOAD_ERROR_ID_CONTENT_CODE = 27; + /** + * Error code returned when loading the init segment failed + */ this.DOWNLOAD_ERROR_ID_INITIALIZATION_CODE = 28; + /** + * Error code returned when loading the XLink content failed + */ this.DOWNLOAD_ERROR_ID_XLINK_CODE = 29; - this.MANIFEST_ERROR_ID_CODEC_CODE = 30; + /** + * Error code returned when parsing the MPD resulted in a logical error + */ this.MANIFEST_ERROR_ID_PARSE_CODE = 31; /** * Error code returned when no stream (period) has been detected in the manifest */ this.MANIFEST_ERROR_ID_NOSTREAMS_CODE = 32; + /** - * Error code returned when something wrong has append during subtitles parsing (TTML or VTT) + * Error code returned when something wrong has happened during parsing and appending subtitles (TTML or VTT) */ this.TIMED_TEXT_ERROR_ID_PARSE_CODE = 33; + /** * Error code returned when a 'muxed' media type has been detected in the manifest. This type is not supported */ + this.MANIFEST_ERROR_ID_MULTIPLEXED_CODE = 34; + /** * Error code returned when a media source type is not supported */ @@ -104,14 +158,13 @@ class Errors extends ErrorsBase { this.XLINK_LOADER_LOADING_FAILURE_ERROR_MESSAGE = 'Failed loading Xlink element: '; this.SEGMENTS_UPDATE_FAILED_ERROR_MESSAGE = 'Segments update failed'; this.SEGMENTS_UNAVAILABLE_ERROR_MESSAGE = 'no segments are available yet'; - this.SEGMENT_BASE_LOADER_ERROR_MESSAGE = 'error loading segments'; - this.TIME_SYNC_FAILED_ERROR_MESSAGE = 'Failed to synchronize time'; + this.SEGMENT_BASE_LOADER_ERROR_MESSAGE = 'error loading segment ranges from sidx'; + this.TIME_SYNC_FAILED_ERROR_MESSAGE = 'Failed to synchronize client and server time'; this.FRAGMENT_LOADER_NULL_REQUEST_ERROR_MESSAGE = 'request is null'; this.URL_RESOLUTION_FAILED_GENERIC_ERROR_MESSAGE = 'Failed to resolve a valid URL'; this.APPEND_ERROR_MESSAGE = 'chunk is not defined'; - this.REMOVE_ERROR_MESSAGE = 'buffer is not defined'; + this.REMOVE_ERROR_MESSAGE = 'Removing data from the SourceBuffer'; this.DATA_UPDATE_FAILED_ERROR_MESSAGE = 'Data update failed'; - this.CAPABILITY_MEDIASOURCE_ERROR_MESSAGE = 'mediasource is not supported'; this.CAPABILITY_MEDIAKEYS_ERROR_MESSAGE = 'mediakeys is not supported'; this.TIMED_TEXT_ERROR_MESSAGE_PARSE = 'parsing error :'; @@ -120,4 +173,4 @@ class Errors extends ErrorsBase { } let errors = new Errors(); -export default errors; \ No newline at end of file +export default errors; diff --git a/src/core/events/CoreEvents.js b/src/core/events/CoreEvents.js index 2579f06f80..10fb206b3f 100644 --- a/src/core/events/CoreEvents.js +++ b/src/core/events/CoreEvents.js @@ -43,20 +43,17 @@ class CoreEvents extends EventsBase { this.ATTEMPT_BACKGROUND_SYNC = 'attemptBackgroundSync'; this.BUFFERING_COMPLETED = 'bufferingCompleted'; this.BUFFER_CLEARED = 'bufferCleared'; - this.BUFFER_LEVEL_UPDATED = 'bufferLevelUpdated'; - this.BYTES_APPENDED = 'bytesAppended'; this.BYTES_APPENDED_END_FRAGMENT = 'bytesAppendedEndFragment'; + this.BUFFER_REPLACEMENT_STARTED = 'bufferReplacementStarted'; this.CHECK_FOR_EXISTENCE_COMPLETED = 'checkForExistenceCompleted'; this.CURRENT_TRACK_CHANGED = 'currentTrackChanged'; this.DATA_UPDATE_COMPLETED = 'dataUpdateCompleted'; - this.DATA_UPDATE_STARTED = 'dataUpdateStarted'; this.INBAND_EVENTS = 'inbandEvents'; - this.INITIALIZATION_LOADED = 'initializationLoaded'; + this.INITIAL_STREAM_SWITCH = 'initialStreamSwitch'; this.INIT_FRAGMENT_LOADED = 'initFragmentLoaded'; this.INIT_FRAGMENT_NEEDED = 'initFragmentNeeded'; this.INTERNAL_MANIFEST_LOADED = 'internalManifestLoaded'; this.ORIGINAL_MANIFEST_LOADED = 'originalManifestLoaded'; - this.LIVE_EDGE_SEARCH_COMPLETED = 'liveEdgeSearchCompleted'; this.LOADING_COMPLETED = 'loadingCompleted'; this.LOADING_PROGRESS = 'loadingProgress'; this.LOADING_DATA_PROGRESS = 'loadingDataProgress'; @@ -65,15 +62,16 @@ class CoreEvents extends EventsBase { this.MEDIA_FRAGMENT_LOADED = 'mediaFragmentLoaded'; this.MEDIA_FRAGMENT_NEEDED = 'mediaFragmentNeeded'; this.QUOTA_EXCEEDED = 'quotaExceeded'; - this.REPRESENTATION_UPDATE_STARTED = 'representationUpdateStarted'; - this.REPRESENTATION_UPDATE_COMPLETED = 'representationUpdateCompleted'; - this.SEGMENTS_LOADED = 'segmentsLoaded'; + this.SEGMENT_LOCATION_BLACKLIST_ADD = 'segmentLocationBlacklistAdd'; + this.SEGMENT_LOCATION_BLACKLIST_CHANGED = 'segmentLocationBlacklistChanged'; this.SERVICE_LOCATION_BLACKLIST_ADD = 'serviceLocationBlacklistAdd'; this.SERVICE_LOCATION_BLACKLIST_CHANGED = 'serviceLocationBlacklistChanged'; - this.SOURCEBUFFER_REMOVE_COMPLETED = 'sourceBufferRemoveCompleted'; + this.SET_FRAGMENTED_TEXT_AFTER_DISABLED = 'setFragmentedTextAfterDisabled'; + this.SET_NON_FRAGMENTED_TEXT = 'setNonFragmentedText'; + this.SOURCE_BUFFER_ERROR = 'sourceBufferError'; this.STREAMS_COMPOSED = 'streamsComposed'; this.STREAM_BUFFERING_COMPLETED = 'streamBufferingCompleted'; - this.STREAM_COMPLETED = 'streamCompleted'; + this.STREAM_REQUESTING_COMPLETED = 'streamRequestingCompleted'; this.TEXT_TRACKS_QUEUE_INITIALIZED = 'textTracksQueueInitialized'; this.TIME_SYNCHRONIZATION_COMPLETED = 'timeSynchronizationComplete'; this.UPDATE_TIME_SYNC_OFFSET = 'updateTimeSyncOffset'; @@ -82,9 +80,10 @@ class CoreEvents extends EventsBase { this.WALLCLOCK_TIME_UPDATED = 'wallclockTimeUpdated'; this.XLINK_ELEMENT_LOADED = 'xlinkElementLoaded'; this.XLINK_READY = 'xlinkReady'; - this.SEGMENTBASE_INIT_REQUEST_NEEDED = 'segmentBaseInitRequestNeeded'; - this.SEGMENTBASE_SEGMENTSLIST_REQUEST_NEEDED = 'segmentBaseSegmentsListRequestNeeded'; this.SEEK_TARGET = 'seekTarget'; + this.SETTING_UPDATED_LIVE_DELAY = 'settingUpdatedLiveDelay'; + this.SETTING_UPDATED_LIVE_DELAY_FRAGMENT_COUNT = 'settingUpdatedLiveDelayFragmentCount'; + this.SETTING_UPDATED_CATCHUP_ENABLED = 'settingUpdatedCatchupEnabled'; } } diff --git a/src/dash/DashAdapter.js b/src/dash/DashAdapter.js index e06cbedf87..ca4ecee69b 100644 --- a/src/dash/DashAdapter.js +++ b/src/dash/DashAdapter.js @@ -38,9 +38,11 @@ import Event from './vo/Event'; import FactoryMaker from '../core/FactoryMaker'; import DashManifestModel from './models/DashManifestModel'; import PatchManifestModel from './models/PatchManifestModel'; +import bcp47Normalize from 'bcp-47-normalize'; /** * @module DashAdapter + * @description The DashAdapter module can be accessed using the MediaPlayer API getDashAdapter() */ function DashAdapter() { @@ -48,7 +50,6 @@ function DashAdapter() { dashManifestModel, patchManifestModel, voPeriods, - voAdaptations, currentMediaInfo, constants, cea608parser; @@ -65,14 +66,6 @@ function DashAdapter() { // #region PUBLIC FUNCTIONS // -------------------------------------------------- - function getVoAdaptations() { - return voAdaptations; - } - - function getVoPeriods() { - return voPeriods; - } - function setConfig(config) { if (!config) return; @@ -110,7 +103,6 @@ function DashAdapter() { representationInfo.id = voRepresentation.id; representationInfo.quality = voRepresentation.index; representationInfo.bandwidth = dashManifestModel.getBandwidth(realRepresentation); - representationInfo.DVRWindow = voRepresentation.segmentAvailabilityRange; representationInfo.fragmentDuration = voRepresentation.segmentDuration || (voRepresentation.segments && voRepresentation.segments.length > 0 ? voRepresentation.segments[0].duration : NaN); representationInfo.MSETimeOffset = voRepresentation.MSETimeOffset; representationInfo.mediaInfo = convertAdaptationToMediaInfo(voRepresentation.adaptation); @@ -122,7 +114,7 @@ function DashAdapter() { } /** - * Returns a MediaInfo object for a given media type. + * Returns a MediaInfo object for a given media type and the corresponding streamInfo. * @param {object} streamInfo * @param {MediaType }type * @returns {null|MediaInfo} mediaInfo @@ -137,14 +129,13 @@ function DashAdapter() { let selectedVoPeriod = getPeriodForStreamInfo(streamInfo, voPeriods); if (!selectedVoPeriod) return null; - let periodId = selectedVoPeriod.id; - voAdaptations[periodId] = voAdaptations[periodId] || dashManifestModel.getAdaptationsForPeriod(selectedVoPeriod); + const voAdaptations = dashManifestModel.getAdaptationsForPeriod(selectedVoPeriod); let realAdaptation = getAdaptationForType(streamInfo.index, type, streamInfo); if (!realAdaptation) return null; let idx = dashManifestModel.getIndexForAdaptation(realAdaptation, voPeriods[0].mpd.manifest, streamInfo.index); - return convertAdaptationToMediaInfo(voAdaptations[periodId][idx]); + return convertAdaptationToMediaInfo(voAdaptations[idx]); } /** @@ -161,7 +152,7 @@ function DashAdapter() { } /** - * Returns the AdaptationSet for a given period and a given mediaType. + * Returns the AdaptationSet for a given period index and a given mediaType. * @param {number} periodIndex * @param {MediaType} type * @param {object} streamInfo @@ -207,64 +198,37 @@ function DashAdapter() { } const sameId = mInfoOne.id === mInfoTwo.id; + const sameCodec = mInfoOne.codec === mInfoTwo.codec; const sameViewpoint = mInfoOne.viewpoint === mInfoTwo.viewpoint; const sameLang = mInfoOne.lang === mInfoTwo.lang; const sameRoles = mInfoOne.roles.toString() === mInfoTwo.roles.toString(); const sameAccessibility = mInfoOne.accessibility.toString() === mInfoTwo.accessibility.toString(); const sameAudioChannelConfiguration = mInfoOne.audioChannelConfiguration.toString() === mInfoTwo.audioChannelConfiguration.toString(); - return (sameId && sameViewpoint && sameLang && sameRoles && sameAccessibility && sameAudioChannelConfiguration); + return (sameId && sameCodec && sameViewpoint && sameLang && sameRoles && sameAccessibility && sameAudioChannelConfiguration); } - /** - * Returns the mediaInfo for a given mediaType - * @param {object} streamInfo - * @param {MediaType} type - * @param {object} externalManifest Set to null or undefined if no external manifest is to be used - * @returns {Array} mediaArr - * @memberOf module:DashAdapter - * @instance - */ - function getAllMediaInfoForType(streamInfo, type, externalManifest) { - let voLocalPeriods = voPeriods; - let manifest = externalManifest; + function _getAllMediaInfo(manifest, period, streamInfo, adaptations, type, embeddedText) { let mediaArr = []; let data, media, idx, i, j, - ln, - periodId; - - if (manifest) { - checkConfig(); - - voLocalPeriods = getRegularPeriods(manifest); - } else { - if (voPeriods.length > 0) { - manifest = voPeriods[0].mpd.manifest; - } else { - return mediaArr; - } - } + ln; - const selectedVoPeriod = getPeriodForStreamInfo(streamInfo, voLocalPeriods); - if (selectedVoPeriod) { - periodId = selectedVoPeriod.id; + if (!adaptations || adaptations.length === 0) { + return []; } - const adaptationsForType = dashManifestModel.getAdaptationsForType(manifest, streamInfo ? streamInfo.index : null, type !== constants.EMBEDDED_TEXT ? type : constants.VIDEO); - if (!adaptationsForType || adaptationsForType.length === 0) return mediaArr; + const voAdaptations = dashManifestModel.getAdaptationsForPeriod(period); - voAdaptations[periodId] = voAdaptations[periodId] || dashManifestModel.getAdaptationsForPeriod(selectedVoPeriod); - - for (i = 0, ln = adaptationsForType.length; i < ln; i++) { - data = adaptationsForType[i]; + for (i = 0, ln = adaptations.length; i < ln; i++) { + data = adaptations[i]; idx = dashManifestModel.getIndexForAdaptation(data, manifest, streamInfo.index); - media = convertAdaptationToMediaInfo(voAdaptations[periodId][idx]); + media = convertAdaptationToMediaInfo(voAdaptations[idx]); - if (type === constants.EMBEDDED_TEXT) { + if (embeddedText) { let accessibilityLength = media.accessibility.length; for (j = 0; j < accessibilityLength; j++) { if (!media) { @@ -277,7 +241,7 @@ function DashAdapter() { if (parts[0].substring(0, 2) === 'CC') { for (j = 0; j < parts.length; j++) { if (!media) { - media = convertAdaptationToMediaInfo.call(this, voAdaptations[periodId][idx]); + media = convertAdaptationToMediaInfo.call(this, voAdaptations[idx]); } convertVideoInfoToEmbeddedTextInfo(media, parts[j].substring(0, 3), parts[j].substring(4)); mediaArr.push(media); @@ -286,7 +250,7 @@ function DashAdapter() { } else { for (j = 0; j < parts.length; j++) { // Only languages for CC1, CC2, ... if (!media) { - media = convertAdaptationToMediaInfo.call(this, voAdaptations[periodId][idx]); + media = convertAdaptationToMediaInfo.call(this, voAdaptations[idx]); } convertVideoInfoToEmbeddedTextInfo(media, 'CC' + (j + 1), parts[j]); mediaArr.push(media); @@ -312,6 +276,47 @@ function DashAdapter() { } /** + * Returns all the mediaInfos for a given mediaType and the corresponding streamInfo. + * @param {object} streamInfo + * @param {MediaType} type + * @param {object} externalManifest Set to null or undefined if no external manifest is to be used + * @returns {Array} mediaArr + * @memberOf module:DashAdapter + * @instance + */ + function getAllMediaInfoForType(streamInfo, type, externalManifest) { + let voLocalPeriods = voPeriods; + let manifest = externalManifest; + let mediaArr = []; + + if (manifest) { + checkConfig(); + + voLocalPeriods = getRegularPeriods(manifest); + } else { + if (voPeriods.length > 0) { + manifest = voPeriods[0].mpd.manifest; + } else { + return mediaArr; + } + } + + const selectedVoPeriod = getPeriodForStreamInfo(streamInfo, voLocalPeriods); + let adaptationsForType = dashManifestModel.getAdaptationsForType(manifest, streamInfo ? streamInfo.index : null, type); + + mediaArr = _getAllMediaInfo(manifest, selectedVoPeriod, streamInfo, adaptationsForType, type); + + // Search for embedded text in video track + if (type === constants.TEXT) { + adaptationsForType = dashManifestModel.getAdaptationsForType(manifest, streamInfo ? streamInfo.index : null, constants.VIDEO); + mediaArr = mediaArr.concat(_getAllMediaInfo(manifest, selectedVoPeriod, streamInfo, adaptationsForType, type, true)); + } + + return mediaArr; + } + + /** + * Update the internal voPeriods array with the information from the new manifest * @param {object} newManifest * @returns {*} * @memberOf module:DashAdapter @@ -324,11 +329,10 @@ function DashAdapter() { checkConfig(); voPeriods = getRegularPeriods(newManifest); - - voAdaptations = {}; } /** + * Returns an array of streamInfo objects * @param {object} externalManifest * @param {number} maxStreamsInfo * @returns {Array} streams @@ -359,7 +363,7 @@ function DashAdapter() { } /** - * + * Returns the AdaptationSet as saved in the DashManifestModel * @param {object} streamInfo * @param {object} mediaInfo * @returns {object} realAdaptation @@ -381,6 +385,28 @@ function DashAdapter() { return realAdaptation; } + /** + * Returns the ProducerReferenceTimes as saved in the DashManifestModel if present + * @param {object} streamInfo + * @param {object} mediaInfo + * @returns {object} producerReferenceTimes + * @memberOf module:DashAdapter + * @instance + */ + function getProducerReferenceTimes(streamInfo, mediaInfo) { + let id, realAdaptation; + + const selectedVoPeriod = getPeriodForStreamInfo(streamInfo, voPeriods); + id = mediaInfo ? mediaInfo.id : null; + + if (voPeriods.length > 0 && selectedVoPeriod) { + realAdaptation = id ? dashManifestModel.getAdaptationForId(id, voPeriods[0].mpd.manifest, selectedVoPeriod.index) : dashManifestModel.getAdaptationForIndex(mediaInfo ? mediaInfo.index : null, voPeriods[0].mpd.manifest, selectedVoPeriod.index); + } + + if (!realAdaptation) return []; + return dashManifestModel.getProducerReferenceTimesForAdaptation(realAdaptation); + } + /** * Return all EssentialProperties of a Representation * @param {object} representation @@ -395,7 +421,7 @@ function DashAdapter() { } /** - * Returns the period by index + * Returns the period as defined in the DashManifestModel for a given index * @param {number} index * @return {object} */ @@ -420,10 +446,10 @@ function DashAdapter() { } /** - * + * Returns the event for the given parameters. * @param {object} eventBox * @param {object} eventStreams - * @param {number} mediaStartTime + * @param {number} mediaStartTime - Specified in seconds * @param {object} voRepresentation * @returns {null|Event} * @memberOf module:DashAdapter @@ -447,8 +473,10 @@ function DashAdapter() { const timescale = eventBox.timescale || 1; const periodStart = voRepresentation.adaptation.period.start; const eventStream = eventStreams[schemeIdUri + '/' + value]; + // The PTO in voRepresentation is already specified in seconds const presentationTimeOffset = !isNaN(voRepresentation.presentationTimeOffset) ? voRepresentation.presentationTimeOffset : !isNaN(eventStream.presentationTimeOffset) ? eventStream.presentationTimeOffset : 0; - let presentationTimeDelta = eventBox.presentation_time_delta / timescale; // In case of version 1 events the presentation_time is parsed as presentation_time_delta + // In case of version 1 events the presentation_time is parsed as presentation_time_delta + let presentationTimeDelta = eventBox.presentation_time_delta / timescale; let calculatedPresentationTime; if (eventBox.version === 0) { @@ -457,7 +485,7 @@ function DashAdapter() { calculatedPresentationTime = periodStart - presentationTimeOffset + presentationTimeDelta; } - const duration = eventBox.event_duration; + const duration = eventBox.event_duration / timescale; const id = eventBox.id; const messageData = eventBox.message_data; @@ -477,7 +505,7 @@ function DashAdapter() { } /** - * + * Returns the events for the given info object. info can either be an instance of StreamInfo, MediaInfo or RepresentationInfo * @param {object} info * @param {object} voRepresentation * @returns {Array} @@ -485,18 +513,21 @@ function DashAdapter() { * @instance * @ignore */ - function getEventsFor(info, voRepresentation) { + function getEventsFor(info, voRepresentation, streamInfo) { let events = []; if (voPeriods.length > 0) { const manifest = voPeriods[0].mpd.manifest; if (info instanceof StreamInfo) { - events = dashManifestModel.getEventsForPeriod(getPeriodForStreamInfo(info, voPeriods)); + const period = getPeriodForStreamInfo(info, voPeriods) + events = dashManifestModel.getEventsForPeriod(period); } else if (info instanceof MediaInfo) { - events = dashManifestModel.getEventStreamForAdaptationSet(manifest, getAdaptationForMediaInfo(info)); + const period = getPeriodForStreamInfo(streamInfo, voPeriods) + events = dashManifestModel.getEventStreamForAdaptationSet(manifest, getAdaptationForMediaInfo(info), period); } else if (info instanceof RepresentationInfo) { - events = dashManifestModel.getEventStreamForRepresentation(manifest, voRepresentation); + const period = getPeriodForStreamInfo(streamInfo, voPeriods) + events = dashManifestModel.getEventStreamForRepresentation(manifest, voRepresentation, period); } } @@ -504,7 +535,7 @@ function DashAdapter() { } /** - * + * Sets the current active mediaInfo for a given streamId and a given mediaType * @param {number} streamId * @param {MediaType} type * @param {object} mediaInfo @@ -519,15 +550,15 @@ function DashAdapter() { } /** - * - * @param {String} type + * Check if the given type is a text track + * @param {object} adaptation * @returns {boolean} * @memberOf module:DashAdapter * @instance * @ignore */ - function getIsTextTrack(type) { - return dashManifestModel.getIsTextTrack(type); + function getIsTextTrack(adaptation) { + return dashManifestModel.getIsText(adaptation); } /** @@ -612,6 +643,17 @@ function DashAdapter() { return dashManifestModel.getMpd(manifest); } + /** + * Returns the ContentSteering element of the MPD + * @param {object} manifest + * @returns {object} contentSteering + * @memberOf module:DashAdapter + * @instance + */ + function getContentSteering(manifest) { + return dashManifestModel.getContentSteering(manifest); + } + /** * Returns the location element of the MPD * @param {object} manifest @@ -699,7 +741,7 @@ function DashAdapter() { } /** - * + * Returns the base urls for a given element * @param {object} node * @returns {Array} * @memberOf module:DashAdapter @@ -711,7 +753,7 @@ function DashAdapter() { } /** - * + * Returns the function to sort the Representations * @returns {*} * @memberOf module:DashAdapter * @instance @@ -735,7 +777,7 @@ function DashAdapter() { } /** - * Returns the bandwidth for a given representation id + * Returns the bandwidth for a given representation id and the corresponding period index * @param {number} representationId * @param {number} periodIdx * @returns {number} bandwidth @@ -767,7 +809,6 @@ function DashAdapter() { /** * This method returns the current max index based on what is defined in the MPD. - * * @param {string} bufferType - String 'audio' or 'video', * @param {number} periodIdx - Make sure this is the period index not id * @return {number} @@ -800,13 +841,18 @@ function DashAdapter() { return null; } + /** + * Checks if the given AdaptationSet is from the given media type + * @param {object} adaptation + * @param {string} type + * @return {boolean} + */ function getIsTypeOf(adaptation, type) { return dashManifestModel.getIsTypeOf(adaptation, type); } function reset() { voPeriods = []; - voAdaptations = {}; currentMediaInfo = {}; } @@ -849,7 +895,7 @@ function DashAdapter() { return; } - let {name, target, leaf} = result; + let { name, target, leaf } = result; // short circuit for attribute selectors if (operation.xpath.findsAttribute()) { @@ -945,8 +991,15 @@ function DashAdapter() { } function getAdaptationForMediaInfo(mediaInfo) { - if (!mediaInfo || !mediaInfo.streamInfo || mediaInfo.streamInfo.id === undefined || !voAdaptations[mediaInfo.streamInfo.id]) return null; - return voAdaptations[mediaInfo.streamInfo.id][mediaInfo.index]; + try { + const selectedVoPeriod = getPeriodForStreamInfo(mediaInfo.streamInfo, voPeriods); + const voAdaptations = dashManifestModel.getAdaptationsForPeriod(selectedVoPeriod); + + if (!mediaInfo || !mediaInfo.streamInfo || mediaInfo.streamInfo.id === undefined || !voAdaptations) return null; + return voAdaptations[mediaInfo.index]; + } catch (e) { + return null; + } } function getPeriodForStreamInfo(streamInfo, voPeriodsArray) { @@ -1009,15 +1062,24 @@ function DashAdapter() { mediaInfo.mimeType = dashManifestModel.getMimeType(realAdaptation); mediaInfo.contentProtection = dashManifestModel.getContentProtectionData(realAdaptation); mediaInfo.bitrateList = dashManifestModel.getBitrateListForAdaptation(realAdaptation); + mediaInfo.selectionPriority = dashManifestModel.getSelectionPriority(realAdaptation); if (mediaInfo.contentProtection) { - mediaInfo.contentProtection.forEach(function (item) { - item.KID = dashManifestModel.getKID(item); - }); + // Get the default key ID and apply it to all key systems + const keyIds = mediaInfo.contentProtection.map(cp => dashManifestModel.getKID(cp)).filter(kid => kid !== null); + if (keyIds.length) { + const keyId = keyIds[0]; + mediaInfo.contentProtection.forEach(cp => { + cp.keyId = keyId; + }); + } } - mediaInfo.isText = dashManifestModel.getIsTextTrack(mediaInfo.mimeType); - mediaInfo.supplementalProperties = dashManifestModel.getSupplementalPropperties(realAdaptation); + mediaInfo.isText = dashManifestModel.getIsText(realAdaptation); + mediaInfo.supplementalProperties = dashManifestModel.getSupplementalProperties(realAdaptation); + + mediaInfo.isFragmented = dashManifestModel.getIsFragmented(realAdaptation); + mediaInfo.isEmbedded = false; return mediaInfo; } @@ -1025,11 +1087,11 @@ function DashAdapter() { function convertVideoInfoToEmbeddedTextInfo(mediaInfo, channel, lang) { mediaInfo.id = channel; // CC1, CC2, CC3, or CC4 mediaInfo.index = 100 + parseInt(channel.substring(2, 3)); - mediaInfo.type = constants.EMBEDDED_TEXT; + mediaInfo.type = constants.TEXT; mediaInfo.codec = 'cea-608-in-SEI'; - mediaInfo.isText = true; mediaInfo.isEmbedded = true; - mediaInfo.lang = lang; + mediaInfo.isFragmented = false; + mediaInfo.lang = bcp47Normalize(lang); mediaInfo.roles = ['caption']; } @@ -1054,7 +1116,7 @@ function DashAdapter() { function convertMpdToManifestInfo(mpd) { let manifestInfo = new ManifestInfo(); - manifestInfo.DVRWindowSize = mpd.timeShiftBufferDepth; + manifestInfo.dvrWindowSize = mpd.timeShiftBufferDepth; manifestInfo.loadedTime = mpd.manifest.loadedTime; manifestInfo.availableFrom = mpd.availabilityStartTime; manifestInfo.minBufferTime = mpd.manifest.minBufferTime; @@ -1135,48 +1197,48 @@ function DashAdapter() { // #endregion PRIVATE FUNCTIONS instance = { - getBandwidthForRepresentation: getBandwidthForRepresentation, - getIndexForRepresentation: getIndexForRepresentation, - getMaxIndexForBufferType: getMaxIndexForBufferType, - convertDataToRepresentationInfo: convertRepresentationToRepresentationInfo, - getDataForMedia: getAdaptationForMediaInfo, - getStreamsInfo: getStreamsInfo, - getMediaInfoForType: getMediaInfoForType, - getAllMediaInfoForType: getAllMediaInfoForType, - getAdaptationForType: getAdaptationForType, - getRealAdaptation: getRealAdaptation, + getBandwidthForRepresentation, + getIndexForRepresentation, + getMaxIndexForBufferType, + convertRepresentationToRepresentationInfo, + getStreamsInfo, + getMediaInfoForType, + getAllMediaInfoForType, + getAdaptationForType, + getRealAdaptation, + getProducerReferenceTimes, getRealPeriodByIndex, getEssentialPropertiesForRepresentation, - getVoRepresentations: getVoRepresentations, - getEventsFor: getEventsFor, - getEvent: getEvent, + getVoRepresentations, + getEventsFor, + getEvent, getMpd, - setConfig: setConfig, - updatePeriods: updatePeriods, - getIsTextTrack: getIsTextTrack, - getUTCTimingSources: getUTCTimingSources, - getSuggestedPresentationDelay: getSuggestedPresentationDelay, - getAvailabilityStartTime: getAvailabilityStartTime, + setConfig, + updatePeriods, + getIsTextTrack, + getUTCTimingSources, + getSuggestedPresentationDelay, + getAvailabilityStartTime, getIsTypeOf, - getIsDynamic: getIsDynamic, - getDuration: getDuration, - getRegularPeriods: getRegularPeriods, - getLocation: getLocation, - getPatchLocation: getPatchLocation, - getManifestUpdatePeriod: getManifestUpdatePeriod, + getIsDynamic, + getDuration, + getRegularPeriods, + getContentSteering, + getLocation, + getPatchLocation, + getManifestUpdatePeriod, getPublishTime, - getIsDVB: getIsDVB, - getIsPatch: getIsPatch, - getBaseURLsFromElement: getBaseURLsFromElement, - getRepresentationSortFunction: getRepresentationSortFunction, - getCodec: getCodec, - getVoAdaptations: getVoAdaptations, - getVoPeriods: getVoPeriods, + getIsDVB, + getIsPatch, + getBaseURLsFromElement, + getRepresentationSortFunction, + getCodec, getPeriodById, - setCurrentMediaInfo: setCurrentMediaInfo, - isPatchValid: isPatchValid, - applyPatchToManifest: applyPatchToManifest, - reset: reset + setCurrentMediaInfo, + isPatchValid, + applyPatchToManifest, + areMediaInfosEqual, + reset }; setup(); diff --git a/src/dash/DashHandler.js b/src/dash/DashHandler.js index 17750a2d53..b992d0017e 100644 --- a/src/dash/DashHandler.js +++ b/src/dash/DashHandler.js @@ -31,57 +31,47 @@ import FragmentRequest from '../streaming/vo/FragmentRequest'; import {HTTPRequest} from '../streaming/vo/metrics/HTTPRequest'; import FactoryMaker from '../core/FactoryMaker'; +import MediaPlayerEvents from '../streaming/MediaPlayerEvents'; import { replaceIDForTemplate, - unescapeDollarsInTemplate, replaceTokenForTemplate, - getTimeBasedSegment + unescapeDollarsInTemplate } from './utils/SegmentsUtils'; +import DashConstants from './constants/DashConstants'; + + +const DEFAULT_ADJUST_SEEK_TIME_THRESHOLD = 0.5; -import SegmentsController from './controllers/SegmentsController'; function DashHandler(config) { config = config || {}; - const context = this.context; const eventBus = config.eventBus; - const events = config.events; const debug = config.debug; - const dashConstants = config.dashConstants; const urlUtils = config.urlUtils; const type = config.type; const streamInfo = config.streamInfo; - + const segmentsController = config.segmentsController; const timelineConverter = config.timelineConverter; - const dashMetrics = config.dashMetrics; const baseURLController = config.baseURLController; let instance, logger, - segmentIndex, lastSegment, - requestedTime, isDynamicManifest, - dynamicStreamCompleted, - selectedMimeType, - segmentsController; + mediaHasFinished; function setup() { logger = debug.getLogger(instance); resetInitialSettings(); - segmentsController = SegmentsController(context).create(config); - - eventBus.on(events.INITIALIZATION_LOADED, onInitializationLoaded, instance); - eventBus.on(events.SEGMENTS_LOADED, onSegmentsLoaded, instance); - eventBus.on(events.REPRESENTATION_UPDATE_STARTED, onRepresentationUpdateStarted, instance); - eventBus.on(events.DYNAMIC_TO_STATIC, onDynamicToStatic, instance); + eventBus.on(MediaPlayerEvents.DYNAMIC_TO_STATIC, _onDynamicToStatic, instance); } function initialize(isDynamic) { isDynamicManifest = isDynamic; - dynamicStreamCompleted = false; + mediaHasFinished = false; segmentsController.initialize(isDynamic); } @@ -97,36 +87,16 @@ function DashHandler(config) { return streamInfo; } - function setCurrentIndex(value) { - segmentIndex = value; - } - - function getCurrentIndex() { - return segmentIndex; - } - - function resetIndex() { - segmentIndex = -1; - lastSegment = null; - } - function resetInitialSettings() { - resetIndex(); - requestedTime = null; - segmentsController = null; - selectedMimeType = null; + lastSegment = null; } function reset() { resetInitialSettings(); - - eventBus.off(events.INITIALIZATION_LOADED, onInitializationLoaded, instance); - eventBus.off(events.SEGMENTS_LOADED, onSegmentsLoaded, instance); - eventBus.off(events.REPRESENTATION_UPDATE_STARTED, onRepresentationUpdateStarted, instance); - eventBus.off(events.DYNAMIC_TO_STATIC, onDynamicToStatic, instance); + eventBus.off(MediaPlayerEvents.DYNAMIC_TO_STATIC, _onDynamicToStatic, instance); } - function setRequestUrl(request, destination, representation) { + function _setRequestUrl(request, destination, representation) { const baseURL = baseURLController.resolve(representation.path); let url, serviceLocation; @@ -152,7 +122,12 @@ function DashHandler(config) { return true; } - function generateInitRequest(mediaInfo, representation, mediaType) { + function getInitRequest(mediaInfo, representation) { + if (!representation) return null; + return _generateInitRequest(mediaInfo, representation, getType()); + } + + function _generateInitRequest(mediaInfo, representation, mediaType) { const request = new FragmentRequest(); const period = representation.adaptation.period; const presentationStartTime = period.start; @@ -160,54 +135,19 @@ function DashHandler(config) { request.mediaType = mediaType; request.type = HTTPRequest.INIT_SEGMENT_TYPE; request.range = representation.range; - request.availabilityStartTime = timelineConverter.calcAvailabilityStartTimeFromPresentationTime(presentationStartTime, period.mpd, isDynamicManifest); - request.availabilityEndTime = timelineConverter.calcAvailabilityEndTimeFromPresentationTime(presentationStartTime + period.duration, period.mpd, isDynamicManifest); + request.availabilityStartTime = timelineConverter.calcAvailabilityStartTimeFromPresentationTime(presentationStartTime, representation, isDynamicManifest); + request.availabilityEndTime = timelineConverter.calcAvailabilityEndTimeFromPresentationTime(presentationStartTime + period.duration, representation, isDynamicManifest); request.quality = representation.index; request.mediaInfo = mediaInfo; request.representationId = representation.id; - if (setRequestUrl(request, representation.initialization, representation)) { + if (_setRequestUrl(request, representation.initialization, representation)) { request.url = replaceTokenForTemplate(request.url, 'Bandwidth', representation.bandwidth); return request; } } - function getInitRequest(mediaInfo, representation) { - if (!representation) return null; - const request = generateInitRequest(mediaInfo, representation, getType()); - return request; - } - - function setMimeType(newMimeType) { - selectedMimeType = newMimeType; - } - - function setExpectedLiveEdge(liveEdge) { - timelineConverter.setExpectedLiveEdge(liveEdge); - dashMetrics.updateManifestUpdateInfo({presentationStartTime: liveEdge}); - } - - function onRepresentationUpdateStarted(e) { - processRepresentation(e.representation); - } - - function processRepresentation(voRepresentation) { - const hasInitialization = voRepresentation.hasInitialization(); - const hasSegments = voRepresentation.hasSegments(); - - // If representation has initialization and segments information, REPRESENTATION_UPDATE_COMPLETED can be triggered immediately - // otherwise, it means that a request has to be made to get initialization and/or segments informations - if (hasInitialization && hasSegments) { - eventBus.trigger(events.REPRESENTATION_UPDATE_COMPLETED, - { representation: voRepresentation }, - { streamId: streamInfo.id, mediaType: type } - ); - } else { - segmentsController.update(voRepresentation, selectedMimeType, hasInitialization, hasSegments); - } - } - - function getRequestForSegment(mediaInfo, segment) { + function _getRequestForSegment(mediaInfo, segment) { if (segment === null || segment === undefined) { return null; } @@ -232,77 +172,76 @@ function DashHandler(config) { request.timescale = representation.timescale; request.availabilityStartTime = segment.availabilityStartTime; request.availabilityEndTime = segment.availabilityEndTime; + request.availabilityTimeComplete = representation.availabilityTimeComplete; request.wallStartTime = segment.wallStartTime; request.quality = representation.index; - request.index = segment.availabilityIdx; + request.index = segment.index; request.mediaInfo = mediaInfo; request.adaptationIndex = representation.adaptation.index; request.representationId = representation.id; - if (setRequestUrl(request, url, representation)) { + if (_setRequestUrl(request, url, representation)) { return request; } } - function isMediaFinished(representation) { - let isFinished = false; + function isLastSegmentRequested(representation, bufferingTime) { + if (!representation || !lastSegment) { + return false; + } + + // Either transition from dynamic to static was done or no next static segment found + if (mediaHasFinished) { + return true; + } + + // Period is endless + if (!isFinite(representation.adaptation.period.duration)) { + return false; + } - if (!representation) return isFinished; + // we are replacing existing stuff in the buffer for instance after a track switch + if (lastSegment.presentationStartTime + lastSegment.duration > bufferingTime) { + return false; + } - if (!isDynamicManifest) { - if (segmentIndex >= representation.availableSegmentsNumber) { - isFinished = true; + // Additional segment references may be added to the last period. + // Additional periods may be added to the end of the MPD. + // Segment references SHALL NOT be added to any period other than the last period. + // An MPD update MAY combine adding segment references to the last period with adding of new periods. An MPD update that adds content MAY be combined with an MPD update that removes content. + // The index of the last requested segment is higher than the number of available segments. + // For SegmentTimeline and SegmentTemplate the index does not include the startNumber. + // For SegmentList the index includes the startnumber which is why the numberOfSegments includes this as well + if (representation.mediaFinishedInformation && !isNaN(representation.mediaFinishedInformation.numberOfSegments) && !isNaN(lastSegment.index) && lastSegment.index >= (representation.mediaFinishedInformation.numberOfSegments - 1)) { + // For static manifests and Template addressing we can compare the index against the number of available segments + if (!isDynamicManifest || representation.segmentInfoType === DashConstants.SEGMENT_TEMPLATE) { + return true; } - } else { - if (dynamicStreamCompleted) { - isFinished = true; - } else if (lastSegment) { - const time = parseFloat((lastSegment.presentationStartTime - representation.adaptation.period.start).toFixed(5)); - const endTime = lastSegment.duration > 0 ? time + 1.5 * lastSegment.duration : time; - const duration = representation.adaptation.period.duration; - - isFinished = endTime >= duration; + // For SegmentList we need to check if the next period is signaled + else if (isDynamicManifest && representation.segmentInfoType === DashConstants.SEGMENT_LIST && representation.adaptation.period.nextPeriodId) { + return true } } - return isFinished; + + // For dynamic SegmentTimeline manifests we need to check if the next period is already signaled and the segment we fetched before is the last one that is signaled. + // We can not simply use the index, as numberOfSegments might have decreased after an MPD update + return !!(isDynamicManifest && representation.adaptation.period.nextPeriodId && representation.segmentInfoType === DashConstants.SEGMENT_TIMELINE && representation.mediaFinishedInformation && + !isNaN(representation.mediaFinishedInformation.mediaTimeOfLastSignaledSegment) && lastSegment && !isNaN(lastSegment.mediaStartTime) && !isNaN(lastSegment.duration) && lastSegment.mediaStartTime + lastSegment.duration >= (representation.mediaFinishedInformation.mediaTimeOfLastSignaledSegment - 0.05)); } - function getSegmentRequestForTime(mediaInfo, representation, time, options) { + + function getSegmentRequestForTime(mediaInfo, representation, time) { let request = null; if (!representation || !representation.segmentInfoType) { return request; } - const idx = segmentIndex; - const keepIdx = options ? options.keepIdx : false; - const ignoreIsFinished = (options && options.ignoreIsFinished) ? true : false; - - if (requestedTime !== time) { // When playing at live edge with 0 delay we may loop back with same time and index until it is available. Reduces verboseness of logs. - requestedTime = time; - logger.debug('Getting the request for time : ' + time); - } - const segment = segmentsController.getSegmentByTime(representation, time); if (segment) { - segmentIndex = segment.availabilityIdx; lastSegment = segment; - logger.debug('Index for time ' + time + ' is ' + segmentIndex); - request = getRequestForSegment(mediaInfo, segment); - } else { - const finished = !ignoreIsFinished ? isMediaFinished(representation) : false; - if (finished) { - request = new FragmentRequest(); - request.action = FragmentRequest.ACTION_COMPLETE; - request.index = segmentIndex - 1; - request.mediaType = type; - request.mediaInfo = mediaInfo; - logger.debug('Signal complete in getSegmentRequestForTime'); - } - } - - if (keepIdx && idx >= 0) { - segmentIndex = representation.segmentInfoType === dashConstants.SEGMENT_TIMELINE && isDynamicManifest ? segmentIndex : idx; + logger.debug('Index for time ' + time + ' is ' + segment.index); + request = _getRequestForSegment(mediaInfo, segment); } return request; @@ -316,14 +255,14 @@ function DashHandler(config) { */ function getNextSegmentRequestIdempotent(mediaInfo, representation) { let request = null; - let indexToRequest = segmentIndex + 1; + let indexToRequest = lastSegment ? lastSegment.index + 1 : 0; const segment = segmentsController.getSegmentByIndex( representation, indexToRequest, lastSegment ? lastSegment.mediaStartTime : -1 ); if (!segment) return null; - request = getRequestForSegment(mediaInfo, segment); + request = _getRequestForSegment(mediaInfo, segment); return request; } @@ -340,141 +279,208 @@ function DashHandler(config) { return null; } - requestedTime = null; + let indexToRequest = lastSegment ? lastSegment.index + 1 : 0; - let indexToRequest = segmentIndex + 1; - - logger.debug('Getting the next request at index: ' + indexToRequest); - // check that there is a segment in this index const segment = segmentsController.getSegmentByIndex(representation, indexToRequest, lastSegment ? lastSegment.mediaStartTime : -1); - if (!segment && isEndlessMedia(representation) && !dynamicStreamCompleted) { - logger.debug(getType() + ' No segment found at index: ' + indexToRequest + '. Wait for next loop'); - return null; - } else { - if (segment) { - request = getRequestForSegment(mediaInfo, segment); - segmentIndex = segment.availabilityIdx; + + // No segment found + if (!segment) { + // Dynamic manifest there might be something available in the next iteration + if (isDynamicManifest && !mediaHasFinished) { + logger.debug(getType() + ' No segment found at index: ' + indexToRequest + '. Wait for next loop'); + return null; } else { - if (isDynamicManifest) { - segmentIndex = indexToRequest - 1; - } else { - segmentIndex = indexToRequest; - } + mediaHasFinished = true; } - } - - if (segment) { - lastSegment = segment; } else { - const finished = isMediaFinished(representation, segment); - if (finished) { - request = new FragmentRequest(); - request.action = FragmentRequest.ACTION_COMPLETE; - request.index = segmentIndex - 1; - request.mediaType = getType(); - request.mediaInfo = mediaInfo; - logger.debug('Signal complete'); - } + request = _getRequestForSegment(mediaInfo, segment); + lastSegment = segment; } return request; } - function isEndlessMedia(representation) { - return !isFinite(representation.adaptation.period.duration); - } + /** + * This function returns a time for which we can generate a request. It is supposed to be as close as possible to the target time. + * This is useful in scenarios in which the user seeks into a gap. We will not find a valid request then and need to adjust the seektime. + * @param {number} time + * @param {object} mediaInfo + * @param {object} representation + * @param {number} targetThreshold + */ + function getValidTimeCloseToTargetTime(time, mediaInfo, representation, targetThreshold) { + try { - function onInitializationLoaded(e) { - const representation = e.representation; - if (!representation.segments) return; + if (isNaN(time) || !mediaInfo || !representation) { + return NaN; + } - eventBus.trigger(events.REPRESENTATION_UPDATE_COMPLETED, - { representation: representation }, - { streamId: streamInfo.id, mediaType: type } - ); - } + if (time < 0) { + time = 0; + } - function onSegmentsLoaded(e) { - if (e.error) return; - - const fragments = e.segments; - const representation = e.representation; - const segments = []; - let count = 0; - - let i, - len, - s, - seg; - - for (i = 0, len = fragments ? fragments.length : 0; i < len; i++) { - s = fragments[i]; - - seg = getTimeBasedSegment( - timelineConverter, - isDynamicManifest, - representation, - s.startTime, - s.duration, - s.timescale, - s.media, - s.mediaRange, - count); - - if (seg) { - segments.push(seg); - seg = null; - count++; + if (isNaN(targetThreshold)) { + targetThreshold = DEFAULT_ADJUST_SEEK_TIME_THRESHOLD; } - } - if (segments.length > 0) { - representation.segmentAvailabilityRange = { - start: segments[0].presentationStartTime, - end: segments[segments.length - 1].presentationStartTime - }; - representation.availableSegmentsNumber = segments.length; - representation.segments = segments; - - if (isDynamicManifest) { - const lastSegment = segments[segments.length - 1]; - const liveEdge = lastSegment.presentationStartTime - 8; - // the last segment is the Expected, not calculated, live edge. - setExpectedLiveEdge(liveEdge); + if (getSegmentRequestForTime(mediaInfo, representation, time)) { + return time; + } + + const start = representation.adaptation.period.start; + const end = representation.adaptation.period.start + representation.adaptation.period.duration; + let currentUpperTime = Math.min(time + targetThreshold, end); + let currentLowerTime = Math.max(time - targetThreshold, start); + let adjustedTime = NaN; + let targetRequest = null; + + while (currentUpperTime <= end || currentLowerTime >= start) { + let upperRequest = null; + let lowerRequest = null; + if (currentUpperTime <= end) { + upperRequest = getSegmentRequestForTime(mediaInfo, representation, currentUpperTime); + } + if (currentLowerTime >= start) { + lowerRequest = getSegmentRequestForTime(mediaInfo, representation, currentLowerTime); + } + + if (lowerRequest) { + adjustedTime = currentLowerTime; + targetRequest = lowerRequest; + break; + } else if (upperRequest) { + adjustedTime = currentUpperTime; + targetRequest = upperRequest; + break; + } + + currentUpperTime += targetThreshold; + currentLowerTime -= targetThreshold; + } + + if (targetRequest) { + const requestEndTime = targetRequest.startTime + targetRequest.duration; + + // Keep the original start time in case it is covered by a segment + if (time >= targetRequest.startTime && requestEndTime - time > targetThreshold) { + return time; + } + + // If target time is before the start of the request use request starttime + if (time < targetRequest.startTime) { + return targetRequest.startTime; + } + + return Math.min(requestEndTime - targetThreshold, adjustedTime); } + + return adjustedTime; + + + } catch (e) { + return NaN; } + } - if (!representation.hasInitialization()) { - return; + /** + * This function returns a time larger than the current time for which we can generate a request. + * This is useful in scenarios in which the user seeks into a gap in a dynamic Timeline manifest. We will not find a valid request then and need to adjust the seektime. + * @param {number} time + * @param {object} mediaInfo + * @param {object} representation + * @param {number} targetThreshold + */ + function getValidTimeAheadOfTargetTime(time, mediaInfo, representation, targetThreshold) { + try { + + if (isNaN(time) || !mediaInfo || !representation) { + return NaN; + } + + if (time < 0) { + time = 0; + } + + if (isNaN(targetThreshold)) { + targetThreshold = DEFAULT_ADJUST_SEEK_TIME_THRESHOLD; + } + + if (getSegmentRequestForTime(mediaInfo, representation, time)) { + return time; + } + + if (representation.adaptation.period.start + representation.adaptation.period.duration < time) { + return NaN; + } + + // Only look 30 seconds ahead + const end = Math.min(representation.adaptation.period.start + representation.adaptation.period.duration, time + 30); + let currentUpperTime = Math.min(time + targetThreshold, end); + let adjustedTime = NaN; + let targetRequest = null; + + while (currentUpperTime <= end) { + let upperRequest = null; + + if (currentUpperTime <= end) { + upperRequest = getSegmentRequestForTime(mediaInfo, representation, currentUpperTime); + } + + if (upperRequest) { + adjustedTime = currentUpperTime; + targetRequest = upperRequest; + break; + } + + currentUpperTime += targetThreshold; + } + + if (targetRequest) { + const requestEndTime = targetRequest.startTime + targetRequest.duration; + + // Keep the original start time in case it is covered by a segment + if (time > targetRequest.startTime && requestEndTime - time > targetThreshold) { + return time; + } + + if (!isNaN(targetRequest.startTime) && time < targetRequest.startTime && adjustedTime > targetRequest.startTime) { + return targetRequest.startTime; + } + + return Math.min(requestEndTime - targetThreshold, adjustedTime); + } + + return adjustedTime; + + + } catch (e) { + return NaN; } + } - eventBus.trigger(events.REPRESENTATION_UPDATE_COMPLETED, - { representation: representation }, - { streamId: streamInfo.id, mediaType: type } - ); + function getCurrentIndex() { + return lastSegment ? lastSegment.index : -1; } - function onDynamicToStatic() { + function _onDynamicToStatic() { logger.debug('Dynamic stream complete'); - dynamicStreamCompleted = true; + mediaHasFinished = true; } instance = { - initialize: initialize, - getStreamId: getStreamId, - getType: getType, - getStreamInfo: getStreamInfo, - getInitRequest: getInitRequest, - getRequestForSegment: getRequestForSegment, - getSegmentRequestForTime: getSegmentRequestForTime, - getNextSegmentRequest: getNextSegmentRequest, - setCurrentIndex: setCurrentIndex, - getCurrentIndex: getCurrentIndex, - isMediaFinished: isMediaFinished, - reset: reset, - resetIndex: resetIndex, - setMimeType: setMimeType, - getNextSegmentRequestIdempotent + initialize, + getStreamId, + getType, + getStreamInfo, + getInitRequest, + getSegmentRequestForTime, + getCurrentIndex, + getNextSegmentRequest, + isLastSegmentRequested, + reset, + getNextSegmentRequestIdempotent, + getValidTimeCloseToTargetTime, + getValidTimeAheadOfTargetTime }; setup(); diff --git a/src/dash/DashMetrics.js b/src/dash/DashMetrics.js index 6a9ec7b264..e1f08022b8 100644 --- a/src/dash/DashMetrics.js +++ b/src/dash/DashMetrics.js @@ -42,6 +42,7 @@ import { /** * @module DashMetrics + * @description The DashMetrics module can be accessed using the MediaPlayer API getDashMetrics() * @param {object} config */ @@ -69,6 +70,7 @@ function DashMetrics(config) { } /** + * Returns the latest Representation switch for a given media type * @param {MediaType} mediaType * @returns {*} * @memberof module:DashMetrics @@ -94,24 +96,26 @@ function DashMetrics(config) { } /** - * @param {MediaType} type + * Returns the current buffer state for a given media type + * @param {MediaType} mediaType * @returns {number} * @memberof module:DashMetrics * @instance */ - function getCurrentBufferState(type) { - const metrics = metricsModel.getMetricsFor(type, true); + function getCurrentBufferState(mediaType) { + const metrics = metricsModel.getMetricsFor(mediaType, true); return getCurrent(metrics, MetricsConstants.BUFFER_STATE); } /** - * @param {MediaType} type + * Returns the current buffer level for a given media type + * @param {MediaType} mediaType * @returns {number} * @memberof module:DashMetrics * @instance */ - function getCurrentBufferLevel(type) { - const metrics = metricsModel.getMetricsFor(type, true); + function getCurrentBufferLevel(mediaType) { + const metrics = metricsModel.getMetricsFor(mediaType, true); const metric = getCurrent(metrics, MetricsConstants.BUFFER_LEVEL); if (metric) { @@ -155,6 +159,7 @@ function DashMetrics(config) { } /** + * Returns the latest HTTP request for a given media type * @param {MediaType} mediaType * @returns {*} * @memberof module:DashMetrics @@ -189,6 +194,7 @@ function DashMetrics(config) { } /** + * Returns all HTTP requests for a given media type * @param {MediaType} mediaType * @returns {*} * @memberof module:DashMetrics @@ -216,6 +222,7 @@ function DashMetrics(config) { } /** + * Returns the latest metrics for a given metric list and specific metric name * @param {MetricsList} metrics * @param {string} metricName * @returns {*} @@ -231,10 +238,10 @@ function DashMetrics(config) { } /** + * Returns the number of dropped frames * @returns {*} * @memberof module:DashMetrics * @instance - * @ignore */ function getCurrentDroppedFrames() { const metrics = metricsModel.getMetricsFor(Constants.VIDEO, true); @@ -252,6 +259,7 @@ function DashMetrics(config) { } /** + * Returns the current scheduling info for a given media type * @param {MediaType} mediaType * @returns {*} * @memberof module:DashMetrics @@ -283,6 +291,7 @@ function DashMetrics(config) { } /** + * Returns the current manifest update information * @returns {*} * @memberof module:DashMetrics * @instance @@ -351,7 +360,8 @@ function DashMetrics(config) { responseStatus, request.duration, responseHeaders, - traces); + traces, + request.fileLoaderType); } /** @@ -369,6 +379,7 @@ function DashMetrics(config) { } /** + * Returns the current DVR window * @param {MediaType} mediaType * @returns {*} * @memberof module:DashMetrics @@ -394,12 +405,17 @@ function DashMetrics(config) { } /** + * Returns the value for a specific request headers used in the latest MPD request * @param {string} id * @returns {*} * @memberof module:DashMetrics * @instance */ function getLatestMPDRequestHeaderValueByID(id) { + if (!id) { + return null; + } + let headers = {}; let httpRequestList, httpRequest, @@ -416,23 +432,31 @@ function DashMetrics(config) { } } - return headers[id] === undefined ? null : headers[id]; + const value = headers[id.toLowerCase()]; + return value === undefined ? null : value; } /** - * @param {string} type + * Returns the value for a specific request headers used in the latest fragment request + * @param {MediaType} mediaType * @param {string} id * @returns {*} * @memberof module:DashMetrics * @instance */ - function getLatestFragmentRequestHeaderValueByID(type, id) { + function getLatestFragmentRequestHeaderValueByID(mediaType, id) { + if (!id) { + return null; + } + let headers = {}; - let httpRequest = getCurrentHttpRequest(type, true); + let httpRequest = getCurrentHttpRequest(mediaType); if (httpRequest) { headers = Utils.parseHttpHeaders(httpRequest._responseHeaders); } - return headers[id] === undefined ? null : headers[id]; + + const value = headers[id.toLowerCase()]; + return value === undefined ? null : value; } /** @@ -447,6 +471,12 @@ function DashMetrics(config) { } } + /** + * Create a new playlist metric + * @param {number} mediaStartTime + * @param {string} startReason + * @ignore + */ function createPlaylistMetrics(mediaStartTime, startReason) { playListMetrics = new PlayList(); @@ -455,6 +485,13 @@ function DashMetrics(config) { playListMetrics.starttype = startReason; } + /** + * Create a playlist trace metric + * @param {number} representationId + * @param {number} mediaStartTime + * @param {number} speed + * @ignore + */ function createPlaylistTraceMetrics(representationId, mediaStartTime, speed) { if (playListTraceMetricsClosed === true ) { playListTraceMetricsClosed = false; @@ -467,6 +504,11 @@ function DashMetrics(config) { } } + /** + * Update existing playlist trace metric + * @param {object} traceToUpdate + * @ignore + */ function updatePlayListTraceMetrics(traceToUpdate) { if (playListTraceMetrics) { for (let field in playListTraceMetrics) { @@ -475,6 +517,12 @@ function DashMetrics(config) { } } + /** + * Push a new playlist trace metric + * @param endTime + * @param reason + * @ignore + */ function pushPlayListTraceMetrics(endTime, reason) { if (playListTraceMetricsClosed === false && playListMetrics && playListTraceMetrics && playListTraceMetrics.start) { const startTime = playListTraceMetrics.start; @@ -497,36 +545,36 @@ function DashMetrics(config) { } instance = { - getCurrentRepresentationSwitch: getCurrentRepresentationSwitch, - getCurrentBufferState: getCurrentBufferState, - getCurrentBufferLevel: getCurrentBufferLevel, - getCurrentHttpRequest: getCurrentHttpRequest, - getHttpRequests: getHttpRequests, - getCurrentDroppedFrames: getCurrentDroppedFrames, - getCurrentSchedulingInfo: getCurrentSchedulingInfo, - getCurrentDVRInfo: getCurrentDVRInfo, - getCurrentManifestUpdate: getCurrentManifestUpdate, - getLatestFragmentRequestHeaderValueByID: getLatestFragmentRequestHeaderValueByID, - getLatestMPDRequestHeaderValueByID: getLatestMPDRequestHeaderValueByID, - addRepresentationSwitch: addRepresentationSwitch, - addDVRInfo: addDVRInfo, - updateManifestUpdateInfo: updateManifestUpdateInfo, - addManifestUpdateStreamInfo: addManifestUpdateStreamInfo, - addManifestUpdateRepresentationInfo: addManifestUpdateRepresentationInfo, - addManifestUpdate: addManifestUpdate, - addHttpRequest: addHttpRequest, - addSchedulingInfo: addSchedulingInfo, - addRequestsQueue: addRequestsQueue, - addBufferLevel: addBufferLevel, - addBufferState: addBufferState, - addDroppedFrames: addDroppedFrames, - addPlayList: addPlayList, - addDVBErrors: addDVBErrors, - createPlaylistMetrics: createPlaylistMetrics, - createPlaylistTraceMetrics: createPlaylistTraceMetrics, - updatePlayListTraceMetrics: updatePlayListTraceMetrics, - pushPlayListTraceMetrics: pushPlayListTraceMetrics, - clearAllCurrentMetrics: clearAllCurrentMetrics + getCurrentRepresentationSwitch, + getCurrentBufferState, + getCurrentBufferLevel, + getCurrentHttpRequest, + getHttpRequests, + getCurrentDroppedFrames, + getCurrentSchedulingInfo, + getCurrentDVRInfo, + getCurrentManifestUpdate, + getLatestFragmentRequestHeaderValueByID, + getLatestMPDRequestHeaderValueByID, + addRepresentationSwitch, + addDVRInfo, + updateManifestUpdateInfo, + addManifestUpdateStreamInfo, + addManifestUpdateRepresentationInfo, + addManifestUpdate, + addHttpRequest, + addSchedulingInfo, + addRequestsQueue, + addBufferLevel, + addBufferState, + addDroppedFrames, + addPlayList, + addDVBErrors, + createPlaylistMetrics, + createPlaylistTraceMetrics, + updatePlayListTraceMetrics, + pushPlayListTraceMetrics, + clearAllCurrentMetrics }; setup(); diff --git a/src/dash/SegmentBaseLoader.js b/src/dash/SegmentBaseLoader.js index 9668fff815..a1bbe8452a 100644 --- a/src/dash/SegmentBaseLoader.js +++ b/src/dash/SegmentBaseLoader.js @@ -44,11 +44,8 @@ function SegmentBaseLoader() { boxParser, requestModifier, dashMetrics, - settings, mediaPlayerModel, urlLoader, - events, - eventBus, errors, constants, dashConstants, @@ -64,7 +61,6 @@ function SegmentBaseLoader() { dashMetrics: dashMetrics, mediaPlayerModel: mediaPlayerModel, requestModifier: requestModifier, - useFetch: settings ? settings.get().streaming.lowLatencyEnabled : null, boxParser: boxParser, errors: errors, urlUtils: urlUtils, @@ -90,22 +86,10 @@ function SegmentBaseLoader() { errHandler = config.errHandler; } - if (config.settings) { - settings = config.settings; - } - if (config.boxParser) { boxParser = config.boxParser; } - if (config.events) { - events = config.events; - } - - if (config.eventBus) { - eventBus = config.eventBus; - } - if (config.debug) { logger = config.debug.getLogger(instance); } @@ -131,14 +115,13 @@ function SegmentBaseLoader() { } } - function checkConfig() { - if (!baseURLController || !baseURLController.hasOwnProperty('resolve')) { - throw new Error('setConfig function has to be called previously'); - } + function loadInitialization(representation, mediaType) { + return new Promise((resolve) => { + _loadInitializationRecursively(representation, mediaType, resolve); + }); } - function loadInitialization(streamId, mediaType, representation, loadingInfo) { - checkConfig(); + function _loadInitializationRecursively(representation, mediaType, resolve, loadingInfo) { let initRange = null; const baseUrl = representation ? baseURLController.resolve(representation.path) : null; const info = loadingInfo || { @@ -166,33 +149,32 @@ function SegmentBaseLoader() { representation.range = initRange; // note that we don't explicitly set rep.initialization as this // will be computed when all BaseURLs are resolved later - eventBus.trigger(events.INITIALIZATION_LOADED, - { representation: representation }, - { streamId: streamId, mediaType: mediaType } - ); + resolve(representation); } else { info.range.end = info.bytesLoaded + info.bytesToLoad; - loadInitialization(streamId, mediaType, representation, info); + return _loadInitializationRecursively(representation, mediaType, resolve, info); } }; const onerror = function () { - eventBus.trigger(events.INITIALIZATION_LOADED, - { representation: representation }, - { streamId: streamId, mediaType: mediaType } - ); + resolve(representation); }; - urlLoader.load({request: request, success: onload, error: onerror}); + urlLoader.load({ request: request, success: onload, error: onerror }); logger.debug('Perform init search: ' + info.url); } - function loadSegments(streamId, mediaType, representation, range, callback, loadingInfo) { - checkConfig(); + function loadSegments(representation, mediaType, range) { + return new Promise((resolve) => { + _loadSegmentsRecursively(representation, mediaType, range, resolve); + }); + } + + function _loadSegmentsRecursively(representation, mediaType, range, resolve, callback, loadingInfo) { if (range && (range.start === undefined || range.end === undefined)) { const parts = range ? range.toString().split('-') : null; - range = parts ? {start: parseFloat(parts[0]), end: parseFloat(parts[1])} : null; + range = parts ? { start: parseFloat(parts[0]), end: parseFloat(parts[1]) } : null; } callback = !callback ? onLoaded : callback; @@ -226,7 +208,7 @@ function SegmentBaseLoader() { info.range.end = info.range.start + (sidx.size || extraBytes); } else if (loadedLength < info.bytesLoaded) { // if we have reached a search limit or if we have reached the end of the file we have to stop trying to find sidx - callback(streamId, mediaType, null, representation); + callback(null, representation, resolve); return; } else { const lastBox = isoFile.getLastBox(); @@ -238,7 +220,7 @@ function SegmentBaseLoader() { info.range.end += extraBytes; } } - loadSegments(streamId, mediaType, representation, info.range, callback, info); + _loadSegmentsRecursively(representation, mediaType, info.range, resolve, null, info); } else { const ref = sidx.references; let loadMultiSidx, @@ -256,7 +238,7 @@ function SegmentBaseLoader() { let segs = []; let count = 0; let offset = (sidx.offset || info.range.start) + sidx.size; - const tmpCallback = function (streamId, mediaType, result) { + const tmpCallback = function (result) { if (result) { segs = segs.concat(result); count++; @@ -266,10 +248,10 @@ function SegmentBaseLoader() { segs.sort(function (a, b) { return a.startTime - b.startTime < 0 ? -1 : 0; }); - callback(streamId, mediaType, segs, representation); + callback(segs, representation, resolve); } } else { - callback(streamId, mediaType, null, representation); + callback(null, representation, resolve); } }; @@ -277,32 +259,39 @@ function SegmentBaseLoader() { ss = offset; se = offset + ref[j].referenced_size - 1; offset = offset + ref[j].referenced_size; - r = {start: ss, end: se}; - loadSegments(streamId, mediaType, representation, r, tmpCallback, info); + r = { start: ss, end: se }; + _loadSegmentsRecursively(representation, mediaType, r, resolve, tmpCallback, info); } } else { logger.debug('Parsing segments from SIDX. representation ' + mediaType + ' - id: ' + representation.id + ' for range : ' + info.range.start + ' - ' + info.range.end); segments = getSegmentsForSidx(sidx, info); - callback(streamId, mediaType, segments, representation); + callback(segments, representation, resolve); } } }; const onerror = function () { - callback(streamId, mediaType, null, representation); + callback(null, representation, resolve); }; - urlLoader.load({request: request, success: onload, error: onerror}); - logger.debug('Perform SIDX load: ' + info.url + ' with range : ' + info.range.start + ' - ' + info.range.end); + urlLoader.load({ request: request, success: onload, error: onerror }); + logger.debug(`Perform SIDX load for type ${mediaType} : ${info.url} with range ${info.range.start} - ${info.range.end}`); + } + + function onLoaded(segments, representation, resolve) { + resolve({ + segments: segments, + representation: representation, + error: segments ? undefined : new DashJSError(errors.SEGMENT_BASE_LOADER_ERROR_CODE, errors.SEGMENT_BASE_LOADER_ERROR_MESSAGE) + }); } function reset() { - urlLoader.abort(); - urlLoader = null; - errHandler = null; - boxParser = null; - requestModifier = null; + if (urlLoader) { + urlLoader.abort(); + urlLoader = null; + } } function getSegmentsForSidx(sidx, info) { @@ -346,23 +335,12 @@ function SegmentBaseLoader() { return request; } - function onLoaded(streamId, mediaType, segments, representation) { - eventBus.trigger(events.SEGMENTS_LOADED, - { - segments: segments, - representation: representation, - error: segments ? undefined : new DashJSError(errors.SEGMENT_BASE_LOADER_ERROR_CODE, errors.SEGMENT_BASE_LOADER_ERROR_MESSAGE) - }, - { streamId: streamId, mediaType: mediaType } - ); - } - instance = { - setConfig: setConfig, - initialize: initialize, - loadInitialization: loadInitialization, - loadSegments: loadSegments, - reset: reset + setConfig, + initialize, + loadInitialization, + loadSegments, + reset }; setup(); diff --git a/src/dash/WebmSegmentBaseLoader.js b/src/dash/WebmSegmentBaseLoader.js index 888912bcd5..cbaafb0eba 100644 --- a/src/dash/WebmSegmentBaseLoader.js +++ b/src/dash/WebmSegmentBaseLoader.js @@ -18,9 +18,6 @@ function WebmSegmentBaseLoader() { dashMetrics, mediaPlayerModel, urlLoader, - settings, - eventBus, - events, errors, baseURLController; @@ -96,7 +93,6 @@ function WebmSegmentBaseLoader() { dashMetrics: dashMetrics, mediaPlayerModel: mediaPlayerModel, requestModifier: requestModifier, - useFetch: settings ? settings.get().streaming.lowLatencyEnabled : null, errors: errors }); } @@ -109,9 +105,6 @@ function WebmSegmentBaseLoader() { dashMetrics = config.dashMetrics; mediaPlayerModel = config.mediaPlayerModel; errHandler = config.errHandler; - settings = config.settings; - events = config.events; - eventBus = config.eventBus; errors = config.errors; logger = config.debug.getLogger(instance); requestModifier = config.requestModifier; @@ -128,14 +121,14 @@ function WebmSegmentBaseLoader() { ebmlParser.consumeTagAndSize(WebM.Segment.Cues); while (ebmlParser.moreData() && - ebmlParser.consumeTagAndSize(WebM.Segment.Cues.CuePoint, true)) { + ebmlParser.consumeTagAndSize(WebM.Segment.Cues.CuePoint, true)) { cue = {}; cue.CueTime = ebmlParser.parseTag(WebM.Segment.Cues.CuePoint.CueTime); cue.CueTracks = []; while (ebmlParser.moreData() && - ebmlParser.consumeTag(WebM.Segment.Cues.CuePoint.CueTrackPositions, true)) { + ebmlParser.consumeTag(WebM.Segment.Cues.CuePoint.CueTrackPositions, true)) { const cueTrackPositionSize = ebmlParser.getMatroskaCodedNum(); const startPos = ebmlParser.getPos(); cueTrack = {}; @@ -250,11 +243,11 @@ function WebmSegmentBaseLoader() { // skip over any top level elements to get to the segment info while (ebmlParser.moreData() && - !ebmlParser.consumeTagAndSize(WebM.Segment.Info, true)) { + !ebmlParser.consumeTagAndSize(WebM.Segment.Info, true)) { if (!(ebmlParser.skipOverElement(WebM.Segment.SeekHead, true) || - ebmlParser.skipOverElement(WebM.Segment.Tracks, true) || - ebmlParser.skipOverElement(WebM.Segment.Cues, true) || - ebmlParser.skipOverElement(WebM.Void, true))) { + ebmlParser.skipOverElement(WebM.Segment.Tracks, true) || + ebmlParser.skipOverElement(WebM.Segment.Cues, true) || + ebmlParser.skipOverElement(WebM.Void, true))) { throw new Error('no valid top level element found'); } } @@ -277,7 +270,7 @@ function WebmSegmentBaseLoader() { // once we have what we need from segment info, we jump right to the // cues - request = getFragmentRequest(info); + request = _getFragmentRequest(info); const onload = function (response) { segments = parseSegments(response, segmentStart, segmentEnd, duration); @@ -298,129 +291,118 @@ function WebmSegmentBaseLoader() { logger.debug('Perform cues load: ' + info.url + ' bytes=' + info.range.start + '-' + info.range.end); } - function checkConfig() { - if (!baseURLController || !baseURLController.hasOwnProperty('resolve')) { - throw new Error('setConfig function has to be called previously'); - } - } - - function loadInitialization(streamId, mediaType, representation, loadingInfo) { - checkConfig(); - let request = null; - let baseUrl = representation ? baseURLController.resolve(representation.path) : null; - let initRange = representation ? representation.range.split('-') : null; - let info = loadingInfo || { - range: { - start: initRange ? parseFloat(initRange[0]) : null, - end: initRange ? parseFloat(initRange[1]) : null - }, - request: request, - url: baseUrl ? baseUrl.url : undefined, - init: true, - mediaType: mediaType - }; - - logger.info('Start loading initialization.'); - - request = getFragmentRequest(info); - - const onload = function () { - // note that we don't explicitly set rep.initialization as this - // will be computed when all BaseURLs are resolved later - eventBus.trigger(events.INITIALIZATION_LOADED, - { representation: representation }, - { streamId: streamId, mediaType: mediaType } - ); - }; - - const onloadend = function () { - eventBus.trigger(events.INITIALIZATION_LOADED, - { representation: representation }, - { streamId: streamId, mediaType: mediaType } - ); - }; + function loadInitialization(representation, mediaType) { + return new Promise((resolve) => { + let request = null; + let baseUrl = representation ? baseURLController.resolve(representation.path) : null; + let initRange = representation ? representation.range.split('-') : null; + let info = { + range: { + start: initRange ? parseFloat(initRange[0]) : null, + end: initRange ? parseFloat(initRange[1]) : null + }, + request: request, + url: baseUrl ? baseUrl.url : undefined, + init: true, + mediaType: mediaType + }; + + logger.info('Start loading initialization.'); + + request = _getFragmentRequest(info); + + const onload = function () { + // note that we don't explicitly set rep.initialization as this + // will be computed when all BaseURLs are resolved later + resolve(representation); + }; + + const onloadend = function () { + resolve(representation); + }; + + urlLoader.load({ + request: request, + success: onload, + error: onloadend + }); - urlLoader.load({ - request: request, - success: onload, - error: onloadend + logger.debug('Perform init load: ' + info.url); }); - - logger.debug('Perform init load: ' + info.url); } - function loadSegments(streamId, mediaType, representation, theRange, callback) { - checkConfig(); - let request = null; - let baseUrl = representation ? baseURLController.resolve(representation.path) : null; - let media = baseUrl ? baseUrl.url : undefined; - let bytesToLoad = 8192; - let info = { - bytesLoaded: 0, - bytesToLoad: bytesToLoad, - range: { - start: 0, - end: bytesToLoad - }, - request: request, - url: media, - init: false, - mediaType: mediaType - }; - - callback = !callback ? onLoaded : callback; - request = getFragmentRequest(info); - - // first load the header, but preserve the manifest range so we can - // load the cues after parsing the header - // NOTE: we expect segment info to appear in the first 8192 bytes - logger.debug('Parsing ebml header'); - - const onload = function (response) { - parseEbmlHeader(response, media, theRange, function (segments) { - callback(streamId, mediaType, segments, representation); + function loadSegments(representation, mediaType, theRange) { + return new Promise((resolve) => { + let request = null; + let baseUrl = representation ? baseURLController.resolve(representation.path) : null; + let media = baseUrl ? baseUrl.url : undefined; + let bytesToLoad = 8192; + let info = { + bytesLoaded: 0, + bytesToLoad: bytesToLoad, + range: { + start: 0, + end: bytesToLoad + }, + request: request, + url: media, + init: false, + mediaType: mediaType + }; + + request = _getFragmentRequest(info); + + // first load the header, but preserve the manifest range so we can + // load the cues after parsing the header + // NOTE: we expect segment info to appear in the first 8192 bytes + logger.debug('Parsing ebml header'); + + const onload = function (response) { + parseEbmlHeader(response, media, theRange, function (segments) { + resolve({ + segments: segments, + representation: representation, + error: segments ? undefined : new DashJSError(errors.SEGMENT_BASE_LOADER_ERROR_CODE, errors.SEGMENT_BASE_LOADER_ERROR_MESSAGE) + }); + }); + }; + + const onloadend = function () { + resolve({ + representation: representation, + error: new DashJSError(errors.SEGMENT_BASE_LOADER_ERROR_CODE, errors.SEGMENT_BASE_LOADER_ERROR_MESSAGE) + }); + }; + + urlLoader.load({ + request: request, + success: onload, + error: onloadend }); - }; - - const onloadend = function () { - callback(streamId, mediaType, null, representation); - }; - - urlLoader.load({ - request: request, - success: onload, - error: onloadend }); - } - function onLoaded(streamId, mediaType, segments, representation) { - eventBus.trigger(events.SEGMENTS_LOADED, - { - segments: segments, - representation: representation, - error: segments ? undefined : new DashJSError(errors.SEGMENT_BASE_LOADER_ERROR_CODE, errors.SEGMENT_BASE_LOADER_ERROR_MESSAGE) - }, - { streamId: streamId, mediaType: mediaType } - ); } - function getFragmentRequest(info) { + + function _getFragmentRequest(info) { const request = new FragmentRequest(); request.setInfo(info); return request; } function reset() { - errHandler = null; - requestModifier = null; + if (urlLoader) { + urlLoader.abort(); + urlLoader = null; + } } instance = { - setConfig: setConfig, - initialize: initialize, - loadInitialization: loadInitialization, - loadSegments: loadSegments, - reset: reset + setConfig, + initialize, + loadInitialization, + loadSegments, + reset }; setup(); diff --git a/src/dash/constants/DashConstants.js b/src/dash/constants/DashConstants.js index 6cd85535df..06473db468 100644 --- a/src/dash/constants/DashConstants.js +++ b/src/dash/constants/DashConstants.js @@ -90,6 +90,7 @@ class DashConstants { this.ESSENTIAL_PROPERTY = 'EssentialProperty'; this.SUPPLEMENTAL_PROPERTY = 'SupplementalProperty'; this.INBAND_EVENT_STREAM = 'InbandEventStream'; + this.PRODUCER_REFERENCE_TIME = 'ProducerReferenceTime'; this.ACCESSIBILITY = 'Accessibility'; this.ROLE = 'Role'; this.RATING = 'Rating'; @@ -98,6 +99,8 @@ class DashConstants { this.LANG = 'lang'; this.VIEWPOINT = 'Viewpoint'; this.ROLE_ASARRAY = 'Role_asArray'; + this.REPRESENTATION_ASARRAY = 'Representation_asArray'; + this.PRODUCERREFERENCETIME_ASARRAY = 'ProducerReferenceTime_asArray'; this.ACCESSIBILITY_ASARRAY = 'Accessibility_asArray'; this.AUDIOCHANNELCONFIGURATION_ASARRAY = 'AudioChannelConfiguration_asArray'; this.CONTENTPROTECTION_ASARRAY = 'ContentProtection_asArray'; @@ -131,10 +134,27 @@ class DashConstants { this.SERVICE_DESCRIPTION_SCOPE = 'Scope'; this.SERVICE_DESCRIPTION_LATENCY = 'Latency'; this.SERVICE_DESCRIPTION_PLAYBACK_RATE = 'PlaybackRate'; + this.SERVICE_DESCRIPTION_OPERATING_QUALITY = 'OperatingQuality'; + this.SERVICE_DESCRIPTION_OPERATING_BANDWIDTH = 'OperatingBandwidth'; this.PATCH_LOCATION = 'PatchLocation'; this.PUBLISH_TIME = 'publishTime'; this.ORIGINAL_PUBLISH_TIME = 'originalPublishTime'; this.ORIGINAL_MPD_ID = 'mpdId'; + this.WALL_CLOCK_TIME = 'wallClockTime'; + this.PRESENTATION_TIME = 'presentationTime'; + this.LABEL = 'Label'; + this.GROUP_LABEL = 'GroupLabel'; + this.CONTENT_STEERING = 'ContentSteering'; + this.CONTENT_STEERING_AS_ARRAY = 'ContentSteering_asArray'; + this.DEFAULT_SERVICE_LOCATION = 'defaultServiceLocation'; + this.QUERY_BEFORE_START = 'queryBeforeStart'; + this.PROXY_SERVER_URL = 'proxyServerURL'; + this.CONTENT_STEERING_RESPONSE = { + VERSION: 'VERSION', + TTL: 'TTL', + RELOAD_URI: 'RELOAD-URI', + SERVICE_LOCATION_PRIORITY : 'SERVICE-LOCATION-PRIORITY' + } } constructor () { diff --git a/src/dash/controllers/ContentSteeringController.js b/src/dash/controllers/ContentSteeringController.js new file mode 100644 index 0000000000..cdac314472 --- /dev/null +++ b/src/dash/controllers/ContentSteeringController.js @@ -0,0 +1,292 @@ +/** + * The copyright in this software is being made available under the BSD License, + * included below. This software may be subject to other third party and contributor + * rights, including patent rights, and no such rights are granted under this license. + * + * Copyright (c) 2013, Dash Industry Forum. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * * Neither the name of Dash Industry Forum nor the names of its + * contributors may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +import FactoryMaker from '../../core/FactoryMaker'; +import Debug from '../../core/Debug'; +import URLLoader from '../../streaming/net/URLLoader'; +import Errors from '../../core/errors/Errors'; +import ContentSteeringRequest from '../vo/ContentSteeringRequest'; +import ContentSteeringResponse from '../vo/ContentSteeringResponse'; +import DashConstants from '../constants/DashConstants'; +import MediaPlayerEvents from '../../streaming/MediaPlayerEvents'; +import Events from '../../core/events/Events'; +import Constants from '../../streaming/constants/Constants'; +import Utils from '../../core/Utils'; +import URLUtils from '../../streaming/utils/URLUtils'; + +const QUERY_PARAMETER_KEYS = { + THROUGHPUT: '_DASH_throughput', + PATHWAY: '_DASH_pathway', + URL: 'url' +} + +function ContentSteeringController() { + const context = this.context; + const urlUtils = URLUtils(context).getInstance(); + + let instance, + logger, + currentSteeringResponseData, + activeStreamInfo, + currentSelectedServiceLocation, + nextRequestTimer, + urlLoader, + errHandler, + dashMetrics, + mediaPlayerModel, + manifestModel, + requestModifier, + abrController, + eventBus, + adapter; + + function setup() { + logger = Debug(context).getInstance().getLogger(instance); + _resetInitialSettings(); + } + + function setConfig(config) { + if (!config) return; + + if (config.adapter) { + adapter = config.adapter; + } + if (config.errHandler) { + errHandler = config.errHandler; + } + if (config.dashMetrics) { + dashMetrics = config.dashMetrics; + } + if (config.mediaPlayerModel) { + mediaPlayerModel = config.mediaPlayerModel; + } + if (config.requestModifier) { + requestModifier = config.requestModifier; + } + if (config.manifestModel) { + manifestModel = config.manifestModel; + } + if (config.abrController) { + abrController = config.abrController; + } + if (config.eventBus) { + eventBus = config.eventBus; + } + } + + function initialize() { + urlLoader = URLLoader(context).create({ + errHandler, + dashMetrics, + mediaPlayerModel, + requestModifier, + errors: Errors + }); + eventBus.on(MediaPlayerEvents.PERIOD_SWITCH_COMPLETED, _onPeriodSwitchCompleted, instance); + eventBus.on(Events.FRAGMENT_LOADING_STARTED, _onFragmentLoadingStarted, instance); + } + + function _onPeriodSwitchCompleted(e) { + if (e && e.toStreamInfo) { + activeStreamInfo = e.toStreamInfo; + } + } + + function _onFragmentLoadingStarted(e) { + if (e && e.request && e.request.serviceLocation) { + currentSelectedServiceLocation = e.request.serviceLocation; + } + } + + function getSteeringDataFromManifest() { + const manifest = manifestModel.getValue() + return adapter.getContentSteering(manifest); + } + + function shouldQueryBeforeStart() { + const steeringDataFromManifest = getSteeringDataFromManifest(); + return steeringDataFromManifest && steeringDataFromManifest.queryBeforeStart; + } + + function loadSteeringData() { + return new Promise((resolve) => { + try { + const steeringDataFromManifest = getSteeringDataFromManifest(); + if (!steeringDataFromManifest || !steeringDataFromManifest.serverUrl) { + resolve(); + return; + } + + const url = _getSteeringServerUrl(steeringDataFromManifest); + const request = new ContentSteeringRequest(url); + urlLoader.load({ + request: request, + success: (data) => { + _handleSteeringResponse(data); + eventBus.trigger(MediaPlayerEvents.CONTENT_STEERING_REQUEST_COMPLETED, { + currentSteeringResponseData, + url + }); + resolve(); + }, + error: (e) => { + _handleSteeringResponseError(e); + resolve(e); + } + }); + } catch (e) { + resolve(e); + } + }) + } + + function _getSteeringServerUrl(steeringDataFromManifest) { + let url = steeringDataFromManifest.proxyServerUrl ? steeringDataFromManifest.proxyServerUrl : steeringDataFromManifest.serverUrl; + if (currentSteeringResponseData && currentSteeringResponseData.reloadUri) { + if (urlUtils.isRelative(currentSteeringResponseData.reloadUri)) { + url = urlUtils.resolve(currentSteeringResponseData.reloadUri, steeringDataFromManifest.serverUrl); + } else { + url = currentSteeringResponseData.reloadUri; + } + } + + const additionalQueryParameter = []; + + // Add throughput value to list of query parameters + if (activeStreamInfo) { + const isDynamic = adapter.getIsDynamic(); + const mediaType = adapter.getAllMediaInfoForType(activeStreamInfo, Constants.VIDEO).length > 0 ? Constants.VIDEO : Constants.AUDIO; + const throughputHistory = abrController.getThroughputHistory(); + const throughput = throughputHistory ? throughputHistory.getAverageThroughput(mediaType, isDynamic) : NaN; + if (!isNaN(throughput)) { + additionalQueryParameter.push({ key: QUERY_PARAMETER_KEYS.THROUGHPUT, value: throughput * 1000 }); + } + } + + // Ass pathway parameter/currently selected service location to list of query parameters + if (currentSelectedServiceLocation) { + additionalQueryParameter.push({ key: QUERY_PARAMETER_KEYS.PATHWAY, value: currentSelectedServiceLocation }); + } + + // If we use the value in proxyServerUrl we add the original url as query parameter + if (steeringDataFromManifest.proxyServerUrl && steeringDataFromManifest.proxyServerUrl === url && steeringDataFromManifest.serverUrl) { + additionalQueryParameter.push({ + key: QUERY_PARAMETER_KEYS.URL, + value: encodeURI(steeringDataFromManifest.serverUrl) + }) + } + + url = Utils.addAditionalQueryParameterToUrl(url, additionalQueryParameter); + return url; + } + + + function _handleSteeringResponse(data) { + if (!data || !data[DashConstants.CONTENT_STEERING_RESPONSE.VERSION] || parseInt(data[DashConstants.CONTENT_STEERING_RESPONSE.VERSION]) !== 1) { + return; + } + + // Update the data for other classes to use + currentSteeringResponseData = new ContentSteeringResponse(); + currentSteeringResponseData.version = data[DashConstants.CONTENT_STEERING_RESPONSE.VERSION]; + + if (data[DashConstants.CONTENT_STEERING_RESPONSE.TTL] && !isNaN(data[DashConstants.CONTENT_STEERING_RESPONSE.TTL])) { + currentSteeringResponseData.ttl = data[DashConstants.CONTENT_STEERING_RESPONSE.TTL]; + } + if (data[DashConstants.CONTENT_STEERING_RESPONSE.RELOAD_URI]) { + currentSteeringResponseData.reloadUri = data[DashConstants.CONTENT_STEERING_RESPONSE.RELOAD_URI] + } + if (data[DashConstants.CONTENT_STEERING_RESPONSE.SERVICE_LOCATION_PRIORITY]) { + currentSteeringResponseData.serviceLocationPriority = data[DashConstants.CONTENT_STEERING_RESPONSE.SERVICE_LOCATION_PRIORITY] + } + + _startSteeringRequestTimer(); + } + + function _startSteeringRequestTimer() { + // Start timer for next request + if (currentSteeringResponseData && currentSteeringResponseData.ttl && !isNaN(currentSteeringResponseData.ttl)) { + if (nextRequestTimer) { + clearTimeout(nextRequestTimer); + } + nextRequestTimer = setTimeout(() => { + loadSteeringData(); + }, currentSteeringResponseData.ttl * 1000); + } + } + + function stopSteeringRequestTimer() { + if (nextRequestTimer) { + clearTimeout(nextRequestTimer); + } + nextRequestTimer = null; + } + + function _handleSteeringResponseError(e) { + logger.warn(`Error fetching data from content steering server`, e); + _startSteeringRequestTimer(); + } + + function getCurrentSteeringResponseData() { + return currentSteeringResponseData; + } + + function reset() { + _resetInitialSettings(); + eventBus.off(MediaPlayerEvents.PERIOD_SWITCH_COMPLETED, _onPeriodSwitchCompleted, instance); + eventBus.off(Events.FRAGMENT_LOADING_STARTED, _onFragmentLoadingStarted, instance); + } + + function _resetInitialSettings() { + currentSteeringResponseData = null; + activeStreamInfo = null; + currentSelectedServiceLocation = null; + stopSteeringRequestTimer() + } + + + instance = { + reset, + setConfig, + loadSteeringData, + getCurrentSteeringResponseData, + shouldQueryBeforeStart, + getSteeringDataFromManifest, + stopSteeringRequestTimer, + initialize + }; + + setup(); + + return instance; +} + +ContentSteeringController.__dashjs_factory_name = 'ContentSteeringController'; +export default FactoryMaker.getSingletonFactory(ContentSteeringController); diff --git a/src/dash/controllers/RepresentationController.js b/src/dash/controllers/RepresentationController.js index 8e4e9257c6..03374505aa 100644 --- a/src/dash/controllers/RepresentationController.js +++ b/src/dash/controllers/RepresentationController.js @@ -29,15 +29,15 @@ * POSSIBILITY OF SUCH DAMAGE. */ import Constants from '../../streaming/constants/Constants'; -import DashJSError from '../../streaming/vo/DashJSError'; import FactoryMaker from '../../core/FactoryMaker'; +import MediaPlayerEvents from '../../streaming/MediaPlayerEvents'; +import {getTimeBasedSegment} from '../utils/SegmentsUtils'; function RepresentationController(config) { config = config || {}; const eventBus = config.eventBus; const events = config.events; - const errors = config.errors; const abrController = config.abrController; const dashMetrics = config.dashMetrics; const playbackController = config.playbackController; @@ -45,6 +45,8 @@ function RepresentationController(config) { const type = config.type; const streamInfo = config.streamInfo; const dashConstants = config.dashConstants; + const segmentsController = config.segmentsController; + const isDynamic = config.isDynamic; let instance, realAdaptation, @@ -55,10 +57,7 @@ function RepresentationController(config) { function setup() { resetInitialSettings(); - eventBus.on(events.QUALITY_CHANGE_REQUESTED, onQualityChanged, instance); - eventBus.on(events.REPRESENTATION_UPDATE_COMPLETED, onRepresentationUpdated, instance); - eventBus.on(events.WALLCLOCK_TIME_UPDATED, onWallclockTimeUpdated, instance); - eventBus.on(events.MANIFEST_VALIDITY_CHANGED, onManifestValidityChanged, instance); + eventBus.on(MediaPlayerEvents.MANIFEST_VALIDITY_CHANGED, onManifestValidityChanged, instance); } function getStreamId() { @@ -94,33 +93,118 @@ function RepresentationController(config) { } function reset() { - eventBus.off(events.QUALITY_CHANGE_REQUESTED, onQualityChanged, instance); - eventBus.off(events.REPRESENTATION_UPDATE_COMPLETED, onRepresentationUpdated, instance); - eventBus.off(events.WALLCLOCK_TIME_UPDATED, onWallclockTimeUpdated, instance); - eventBus.off(events.MANIFEST_VALIDITY_CHANGED, onManifestValidityChanged, instance); + eventBus.off(MediaPlayerEvents.MANIFEST_VALIDITY_CHANGED, onManifestValidityChanged, instance); resetInitialSettings(); } - function updateData(newRealAdaptation, availableRepresentations, type, quality) { + function updateData(newRealAdaptation, availableRepresentations, type, isFragmented, quality) { checkConfig(); - startDataUpdate(); + updating = true; voAvailableRepresentations = availableRepresentations; - currentVoRepresentation = getRepresentationForQuality(quality); + const rep = getRepresentationForQuality(quality) + _setCurrentVoRepresentation(rep); realAdaptation = newRealAdaptation; - if (type !== Constants.VIDEO && type !== Constants.AUDIO && type !== Constants.FRAGMENTED_TEXT) { + if (type !== Constants.VIDEO && type !== Constants.AUDIO && (type !== Constants.TEXT || !isFragmented)) { endDataUpdate(); - return; + return Promise.resolve(); + } + + const promises = []; + for (let i = 0, ln = voAvailableRepresentations.length; i < ln; i++) { + const currentRep = voAvailableRepresentations[i]; + promises.push(_updateRepresentation(currentRep)); } - updateAvailabilityWindow(playbackController.getIsDynamic(), true); + return Promise.all(promises); + } + + function _updateRepresentation(currentRep) { + return new Promise((resolve, reject) => { + const hasInitialization = currentRep.hasInitialization(); + const hasSegments = currentRep.hasSegments(); + + // If representation has initialization and segments information we are done + // otherwise, it means that a request has to be made to get initialization and/or segments information + const promises = []; + + promises.push(segmentsController.updateInitData(currentRep, hasInitialization)); + promises.push(segmentsController.updateSegmentData(currentRep, hasSegments)); + + Promise.all(promises) + .then((data) => { + if (data[0] && !data[0].error) { + currentRep = _onInitLoaded(currentRep, data[0]); + } + if (data[1] && !data[1].error) { + currentRep = _onSegmentsLoaded(currentRep, data[1]); + } + _setMediaFinishedInformation(currentRep); + _onRepresentationUpdated(currentRep); + resolve(); + }) + .catch((e) => { + reject(e); + }); + }); + } + + function _setMediaFinishedInformation(representation) { + representation.mediaFinishedInformation = segmentsController.getMediaFinishedInformation(representation); + } + + function _onInitLoaded(representation, e) { + if (!e || e.error || !e.representation) { + return representation; + } + return e.representation; } - function addRepresentationSwitch() { + function _onSegmentsLoaded(representation, e) { + if (!e || e.error) return; + + const fragments = e.segments; + const segments = []; + let count = 0; + + let i, + len, + s, + seg; + + for (i = 0, len = fragments ? fragments.length : 0; i < len; i++) { + s = fragments[i]; + + seg = getTimeBasedSegment( + timelineConverter, + isDynamic, + representation, + s.startTime, + s.duration, + s.timescale, + s.media, + s.mediaRange, + count); + + if (seg) { + segments.push(seg); + seg = null; + count++; + } + } + + if (segments.length > 0) { + representation.segments = segments; + } + + return representation; + } + + function _addRepresentationSwitch() { checkConfig(); const now = new Date(); const currentRepresentation = getCurrentRepresentation(); @@ -128,6 +212,13 @@ function RepresentationController(config) { if (currentRepresentation) { dashMetrics.addRepresentationSwitch(currentRepresentation.adaptation.type, now, currentVideoTimeMs, currentRepresentation.id); } + + eventBus.trigger(MediaPlayerEvents.REPRESENTATION_SWITCH, { + mediaType: type, + streamId: streamInfo.id, + currentRepresentation, + numberOfRepresentations: voAvailableRepresentations.length + }, { streamId: streamInfo.id, mediaType: type }) } function getRepresentationForQuality(quality) { @@ -141,7 +232,7 @@ function RepresentationController(config) { function isAllRepresentationsUpdated() { for (let i = 0, ln = voAvailableRepresentations.length; i < ln; i++) { let segmentInfoType = voAvailableRepresentations[i].segmentInfoType; - if (voAvailableRepresentations[i].segmentAvailabilityRange === null || !voAvailableRepresentations[i].hasInitialization() || + if (!voAvailableRepresentations[i].hasInitialization() || ((segmentInfoType === dashConstants.SEGMENT_BASE || segmentInfoType === dashConstants.BASE_URL) && !voAvailableRepresentations[i].segments) ) { return false; @@ -151,53 +242,6 @@ function RepresentationController(config) { return true; } - function setExpectedLiveEdge(liveEdge) { - timelineConverter.setExpectedLiveEdge(liveEdge); - dashMetrics.updateManifestUpdateInfo({presentationStartTime: liveEdge}); - } - - function updateRepresentation(representation, isDynamic) { - representation.segmentAvailabilityRange = timelineConverter.calcSegmentAvailabilityRange(representation, isDynamic); - - if (representation.segmentAvailabilityRange.end < representation.segmentAvailabilityRange.start) { - let error = new DashJSError(errors.SEGMENTS_UNAVAILABLE_ERROR_CODE, errors.SEGMENTS_UNAVAILABLE_ERROR_MESSAGE, {availabilityDelay: representation.segmentAvailabilityRange.start - representation.segmentAvailabilityRange.end}); - endDataUpdate(error); - return; - } - - if (isDynamic) { - setExpectedLiveEdge(representation.segmentAvailabilityRange.end); - } - } - - function updateAvailabilityWindow(isDynamic, notifyUpdate) { - checkConfig(); - - for (let i = 0, ln = voAvailableRepresentations.length; i < ln; i++) { - updateRepresentation(voAvailableRepresentations[i], isDynamic); - if (notifyUpdate) { - eventBus.trigger(events.REPRESENTATION_UPDATE_STARTED, - { representation: voAvailableRepresentations[i] }, - { streamId: streamInfo.id, mediaType: type } - ); - } - } - } - - function resetAvailabilityWindow() { - voAvailableRepresentations.forEach(rep => { - rep.segmentAvailabilityRange = null; - }); - } - - function startDataUpdate() { - updating = true; - eventBus.trigger(events.DATA_UPDATE_STARTED, - {}, - { streamId: streamInfo.id, mediaType: type } - ); - } - function endDataUpdate(error) { updating = false; eventBus.trigger(events.DATA_UPDATE_COMPLETED, @@ -210,50 +254,14 @@ function RepresentationController(config) { ); } - function postponeUpdate(postponeTimePeriod) { - let delay = postponeTimePeriod; - let update = function () { - if (isUpdating()) return; - - startDataUpdate(); - - // clear the segmentAvailabilityRange for all reps. - // this ensures all are updated before the live edge search starts - resetAvailabilityWindow(); - - updateAvailabilityWindow(playbackController.getIsDynamic(), true); - }; - eventBus.trigger(events.AST_IN_FUTURE, { delay: delay }); - setTimeout(update, delay); - } - - function onRepresentationUpdated(e) { + function _onRepresentationUpdated(r) { if (!isUpdating()) return; - if (e.error) { - endDataUpdate(e.error); - return; - } - - let r = e.representation; let manifestUpdateInfo = dashMetrics.getCurrentManifestUpdate(); let alreadyAdded = false; - let postponeTimePeriod = 0; let repInfo, - err, repSwitch; - if (r.adaptation.period.mpd.manifest.type === dashConstants.DYNAMIC && !r.adaptation.period.mpd.manifest.ignorePostponeTimePeriod && playbackController.getStreamController().getStreams().length <= 1) { - // We must put things to sleep unless till e.g. the startTime calculation in ScheduleController.onLiveEdgeSearchCompleted fall after the segmentAvailabilityRange.start - postponeTimePeriod = getRepresentationUpdatePostponeTimePeriod(r); - } - - if (postponeTimePeriod > 0) { - postponeUpdate(postponeTimePeriod); - err = new DashJSError(errors.SEGMENTS_UPDATE_FAILED_ERROR_CODE, errors.SEGMENTS_UPDATE_FAILED_ERROR_MESSAGE); - endDataUpdate(err); - return; - } if (manifestUpdateInfo) { for (let i = 0; i < manifestUpdateInfo.representationInfo.length; i++) { @@ -270,47 +278,29 @@ function RepresentationController(config) { } if (isAllRepresentationsUpdated()) { - abrController.setPlaybackQuality(getType(), streamInfo, getQualityForRepresentation(currentVoRepresentation)); - dashMetrics.updateManifestUpdateInfo({latency: currentVoRepresentation.segmentAvailabilityRange.end - playbackController.getTime()}); + abrController.setPlaybackQuality(type, streamInfo, getQualityForRepresentation(currentVoRepresentation)); + const dvrInfo = dashMetrics.getCurrentDVRInfo(type); + if (dvrInfo) { + dashMetrics.updateManifestUpdateInfo({ latency: dvrInfo.range.end - playbackController.getTime() }); + } repSwitch = dashMetrics.getCurrentRepresentationSwitch(getCurrentRepresentation().adaptation.type); if (!repSwitch) { - addRepresentationSwitch(); + _addRepresentationSwitch(); } endDataUpdate(); } } - function getRepresentationUpdatePostponeTimePeriod(representation) { - try { - const streamController = playbackController.getStreamController(); - const activeStreamInfo = streamController.getActiveStreamInfo(); - let startTimeAnchor = representation.segmentAvailabilityRange.start; - - if (activeStreamInfo && activeStreamInfo.id && activeStreamInfo.id !== streamInfo.id) { - // We need to consider the currently playing period if a period switch is performed. - startTimeAnchor = Math.min(playbackController.getTime(), startTimeAnchor); - } - - let segmentAvailabilityTimePeriod = representation.segmentAvailabilityRange.end - startTimeAnchor; - let liveDelay = playbackController.getLiveDelay(); - - return (liveDelay - segmentAvailabilityTimePeriod) * 1000; - } catch (e) { - return 0; - } - } - - function onWallclockTimeUpdated(e) { - if (e.isDynamic) { - updateAvailabilityWindow(e.isDynamic); - } + function prepareQualityChange(newQuality) { + const newRep = getRepresentationForQuality(newQuality) + _setCurrentVoRepresentation(newRep); + _addRepresentationSwitch(); } - function onQualityChanged(e) { - currentVoRepresentation = getRepresentationForQuality(e.newQuality); - addRepresentationSwitch(); + function _setCurrentVoRepresentation(value) { + currentVoRepresentation = value; } function onManifestValidityChanged(e) { @@ -324,15 +314,15 @@ function RepresentationController(config) { } instance = { - getStreamId: getStreamId, - getType: getType, - getData: getData, - isUpdating: isUpdating, - updateData: updateData, - updateRepresentation: updateRepresentation, - getCurrentRepresentation: getCurrentRepresentation, - getRepresentationForQuality: getRepresentationForQuality, - reset: reset + getStreamId, + getType, + getData, + isUpdating, + updateData, + getCurrentRepresentation, + getRepresentationForQuality, + prepareQualityChange, + reset }; setup(); diff --git a/src/dash/controllers/SegmentBaseController.js b/src/dash/controllers/SegmentBaseController.js index 8feadcf039..91697d0b21 100644 --- a/src/dash/controllers/SegmentBaseController.js +++ b/src/dash/controllers/SegmentBaseController.js @@ -88,37 +88,37 @@ function SegmentBaseController(config) { } function initialize() { - eventBus.on(events.SEGMENTBASE_INIT_REQUEST_NEEDED, onInitSegmentBaseNeeded, instance); - eventBus.on(events.SEGMENTBASE_SEGMENTSLIST_REQUEST_NEEDED, onSegmentsListSegmentBaseNeeded, instance); - segmentBaseLoader.initialize(); webmSegmentBaseLoader.initialize(); } - function onInitSegmentBaseNeeded(e) { - if (isWebM(e.mimeType)) { - webmSegmentBaseLoader.loadInitialization(e.streamId, e.mediaType, e.representation); + function getSegmentBaseInitSegment(data) { + if (isWebM(data.representation.mimeType)) { + return webmSegmentBaseLoader.loadInitialization(data.representation, data.mediaType); } else { - segmentBaseLoader.loadInitialization(e.streamId, e.mediaType, e.representation); + return segmentBaseLoader.loadInitialization(data.representation, data.mediaType); } } - function onSegmentsListSegmentBaseNeeded(e) { + function getSegmentList(e) { if (isWebM(e.mimeType)) { - webmSegmentBaseLoader.loadSegments(e.streamId, e.mediaType, e.representation, e.representation ? e.representation.indexRange : null, e.callback); + return webmSegmentBaseLoader.loadSegments(e.representation, e.mediaType, e.representation ? e.representation.indexRange : null); } else { - segmentBaseLoader.loadSegments(e.streamId, e.mediaType, e.representation, e.representation ? e.representation.indexRange : null, e.callback); + return segmentBaseLoader.loadSegments(e.representation, e.mediaType, e.representation ? e.representation.indexRange : null); } } function reset() { - eventBus.off(events.SEGMENTBASE_INIT_REQUEST_NEEDED, onInitSegmentBaseNeeded, instance); - eventBus.off(events.SEGMENTBASE_SEGMENTSLIST_REQUEST_NEEDED, onSegmentsListSegmentBaseNeeded, instance); + segmentBaseLoader.reset(); + webmSegmentBaseLoader.reset(); } + instance = { - initialize: initialize, - reset: reset + initialize, + getSegmentBaseInitSegment, + getSegmentList, + reset }; setup(); diff --git a/src/dash/controllers/SegmentsController.js b/src/dash/controllers/SegmentsController.js index 00e0cdf800..ecccad8180 100644 --- a/src/dash/controllers/SegmentsController.js +++ b/src/dash/controllers/SegmentsController.js @@ -38,11 +38,9 @@ function SegmentsController(config) { config = config || {}; const context = this.context; - const events = config.events; - const eventBus = config.eventBus; const dashConstants = config.dashConstants; - const streamInfo = config.streamInfo; const type = config.type; + const segmentBaseController = config.segmentBaseController; let instance, getters; @@ -58,24 +56,25 @@ function SegmentsController(config) { getters[dashConstants.SEGMENT_BASE] = SegmentBaseGetter(context).create(config, isDynamic); } - function update(voRepresentation, mimeType, hasInitialization, hasSegments) { - if (!hasInitialization) { - eventBus.trigger(events.SEGMENTBASE_INIT_REQUEST_NEEDED, { - streamId: streamInfo.id, - mediaType: type, - mimeType: mimeType, - representation: voRepresentation - }); + function updateInitData(voRepresentation, hasInitialization) { + if (hasInitialization) { + return Promise.resolve(); } + return segmentBaseController.getSegmentBaseInitSegment({ + representation: voRepresentation, + mediaType: type + }); + } - if (!hasSegments) { - eventBus.trigger(events.SEGMENTBASE_SEGMENTSLIST_REQUEST_NEEDED, { - streamId: streamInfo.id, - mediaType: type, - mimeType: mimeType, - representation: voRepresentation - }); + function updateSegmentData(voRepresentation, hasSegments) { + if (hasSegments) { + return Promise.resolve(); } + return segmentBaseController.getSegmentList({ + mimeType: voRepresentation.mimeType, + representation: voRepresentation, + mediaType: type + }); } function getSegmentsGetter(representation) { @@ -92,11 +91,21 @@ function SegmentsController(config) { return getter ? getter.getSegmentByTime(representation, time) : null; } + function getMediaFinishedInformation(representation) { + const getter = getSegmentsGetter(representation); + return getter ? getter.getMediaFinishedInformation(representation) : { + numberOfSegments: 0, + mediaTimeOfLastSignaledSegment: NaN + }; + } + instance = { - initialize: initialize, - update: update, - getSegmentByIndex: getSegmentByIndex, - getSegmentByTime: getSegmentByTime + initialize, + updateInitData, + updateSegmentData, + getSegmentByIndex, + getSegmentByTime, + getMediaFinishedInformation }; setup(); diff --git a/src/dash/controllers/ServiceDescriptionController.js b/src/dash/controllers/ServiceDescriptionController.js new file mode 100644 index 0000000000..97e0c7577c --- /dev/null +++ b/src/dash/controllers/ServiceDescriptionController.js @@ -0,0 +1,378 @@ +/** + * The copyright in this software is being made available under the BSD License, + * included below. This software may be subject to other third party and contributor + * rights, including patent rights, and no such rights are granted under this license. + * + * Copyright (c) 2013, Dash Industry Forum. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * * Neither the name of Dash Industry Forum nor the names of its + * contributors may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +import FactoryMaker from '../../core/FactoryMaker'; +import Debug from '../../core/Debug'; +import Constants from '../../streaming/constants/Constants'; +import DashConstants from '../constants/DashConstants'; + +const SUPPORTED_SCHEMES = [Constants.SERVICE_DESCRIPTION_DVB_LL_SCHEME]; +const MEDIA_TYPES = { + VIDEO: 'video', + AUDIO: 'audio', + ANY: 'any', + ALL: 'all' +} + +function ServiceDescriptionController() { + const context = this.context; + + let instance, + serviceDescriptionSettings, + prftOffsets, + logger, + adapter; + + function setup() { + logger = Debug(context).getInstance().getLogger(instance); + _resetInitialSettings(); + } + + function setConfig(config) { + if (!config) return; + + if (config.adapter) { + adapter = config.adapter; + } + } + + function reset() { + _resetInitialSettings(); + } + + function _resetInitialSettings() { + serviceDescriptionSettings = { + liveDelay: NaN, + liveCatchup: { + maxDrift: NaN, + playbackRate: NaN + }, + minBitrate: {}, + maxBitrate: {}, + initialBitrate: {} + }; + prftOffsets = []; + } + + /** + * Returns the service description settings for latency, catchup and bandwidth + */ + function getServiceDescriptionSettings() { + return serviceDescriptionSettings + } + + /** + * Check for potential ServiceDescriptor elements in the MPD and update the settings accordingly + * @param {object} manifestInfo + * @private + */ + function applyServiceDescription(manifestInfo) { + if (!manifestInfo || !manifestInfo.serviceDescriptions) { + return; + } + + const supportedServiceDescriptions = manifestInfo.serviceDescriptions.filter(sd => SUPPORTED_SCHEMES.includes(sd.schemeIdUri)); + const allClientsServiceDescriptions = manifestInfo.serviceDescriptions.filter(sd => sd.schemeIdUri == null); + let sd = (supportedServiceDescriptions.length > 0) + ? supportedServiceDescriptions[supportedServiceDescriptions.length - 1] + : allClientsServiceDescriptions[allClientsServiceDescriptions.length - 1]; + if (!sd) return; + + if (sd.latency && sd.latency.target > 0) { + _applyServiceDescriptionLatency(sd); + } + + if (sd.playbackRate && sd.playbackRate.max > 1.0) { + _applyServiceDescriptionPlaybackRate(sd); + } + + if (sd.operatingQuality) { + _applyServiceDescriptionOperatingQuality(sd); + } + + if (sd.operatingBandwidth) { + _applyServiceDescriptionOperatingBandwidth(sd); + } + } + + /** + * Adjust the latency targets for the service. + * @param {object} sd - service description element + * @private + */ + function _applyServiceDescriptionLatency(sd) { + let params; + + if (sd.schemeIdUri === Constants.SERVICE_DESCRIPTION_DVB_LL_SCHEME) { + params = _getDvbServiceDescriptionLatencyParameters(sd); + } else { + params = _getStandardServiceDescriptionLatencyParameters(sd); + } + + if (prftOffsets.length > 0) { + let { to, id } = _calculateTimeOffset(params); + + // TS 103 285 Clause 10.20.4. 3) Subtract calculated offset from Latency@target converted from milliseconds + // liveLatency does not consider ST@availabilityTimeOffset so leave out that step + // Since maxDrift is a difference rather than absolute it does not need offset applied + serviceDescriptionSettings.liveDelay = params.liveDelay - to; + serviceDescriptionSettings.liveCatchup.maxDrift = params.maxDrift; + + logger.debug(` + Found latency properties coming from service description. Applied time offset of ${to} from ProducerReferenceTime element with id ${id}. + Live Delay: ${params.liveDelay - to}, Live catchup max drift: ${params.maxDrift} + `); + } else { + serviceDescriptionSettings.liveDelay = params.liveDelay; + serviceDescriptionSettings.liveCatchup.maxDrift = params.maxDrift; + + logger.debug(`Found latency properties coming from service description: Live Delay: ${params.liveDelay}, Live catchup max drift: ${params.maxDrift}`); + } + } + + /** + * Get default parameters for liveDelay,maxDrift + * @param {object} sd + * @return {{maxDrift: (number|undefined), liveDelay: number, referenceId: (number|undefined)}} + * @private + */ + function _getStandardServiceDescriptionLatencyParameters(sd) { + const liveDelay = sd.latency.target / 1000; + let maxDrift = !isNaN(sd.latency.max) && sd.latency.max > sd.latency.target ? (sd.latency.max - sd.latency.target + 500) / 1000 : NaN; + const referenceId = sd.latency.referenceId || NaN; + + return { + liveDelay, + maxDrift, + referenceId + } + } + + /** + * Get DVB DASH parameters for liveDelay,maxDrift + * @param sd + * @return {{maxDrift: (number|undefined), liveDelay: number, referenceId: (number|undefined)}} + * @private + */ + function _getDvbServiceDescriptionLatencyParameters(sd) { + const liveDelay = sd.latency.target / 1000; + let maxDrift = !isNaN(sd.latency.max) && sd.latency.max > sd.latency.target ? (sd.latency.max - sd.latency.target + 500) / 1000 : NaN; + const referenceId = sd.latency.referenceId || NaN; + + return { + liveDelay, + maxDrift, + referenceId + } + } + + /** + * Adjust the playback rate targets for the service + * @param {object} sd + * @private + */ + function _applyServiceDescriptionPlaybackRate(sd) { + const playbackRate = (Math.round((sd.playbackRate.max - 1.0) * 1000) / 1000) + + serviceDescriptionSettings.liveCatchup.playbackRate = playbackRate; + logger.debug(`Found latency properties coming from service description: Live catchup playback rate: ${playbackRate}`); + + } + + /** + * Used to specify a quality ranking. We do not support this yet. + * @private + */ + function _applyServiceDescriptionOperatingQuality() { + return; + } + + /** + * Adjust the operating quality targets for the service + * @param {object} sd + * @private + */ + function _applyServiceDescriptionOperatingBandwidth(sd) { + + // Aggregation of media types is not supported yet + if (!sd || !sd.operatingBandwidth || !sd.operatingBandwidth.mediaType || sd.operatingBandwidth.mediaType === MEDIA_TYPES.ALL) { + return; + } + + const params = {}; + + params.minBandwidth = sd.operatingBandwidth.min; + params.maxBandwidth = sd.operatingBandwidth.max; + params.targetBandwidth = sd.operatingBandwidth.target; + + const mediaTypesToApply = []; + + if (sd.operatingBandwidth.mediaType === MEDIA_TYPES.VIDEO || sd.operatingBandwidth.mediaType === MEDIA_TYPES.AUDIO) { + mediaTypesToApply.push(sd.operatingBandwidth.mediaType); + } else if (sd.operatingBandwidth.mediaType === MEDIA_TYPES.ANY) { + mediaTypesToApply.push(MEDIA_TYPES.AUDIO); + mediaTypesToApply.push(MEDIA_TYPES.VIDEO); + } + + mediaTypesToApply.forEach((mediaType) => { + + if (!isNaN(params.minBandwidth)) { + _updateBandwidthSetting('minBitrate', mediaType, params.minBandwidth); + } + + if (!isNaN(params.maxBandwidth)) { + _updateBandwidthSetting('maxBitrate', mediaType, params.maxBandwidth); + } + + if (!isNaN(params.targetBandwidth)) { + _updateBandwidthSetting('initialBitrate', mediaType, params.targetBandwidth); + } + }) + } + + /** + * Update the bandwidth settings vor a specific field and media type + * @param {string} field + * @param {string} mediaType + * @param {number} value + * @private + */ + function _updateBandwidthSetting(field, mediaType, value) { + try { + // Service description values are specified in bps. Settings expect the value in kbps + serviceDescriptionSettings[field][mediaType] = value / 1000; + } catch (e) { + logger.error(e); + } + } + + /** + * Returns the current calculated time offsets based on ProducerReferenceTime elements + * @returns {array} + */ + function getProducerReferenceTimeOffsets() { + return prftOffsets; + } + + /** + * Calculates an array of time offsets each with matching ProducerReferenceTime id. + * Call before applyServiceDescription if producer reference time elements should be considered. + * @param {array} streamInfos + * @returns {array} + * @private + */ + function calculateProducerReferenceTimeOffsets(streamInfos) { + try { + let timeOffsets = []; + if (streamInfos && streamInfos.length > 0) { + const mediaTypes = [Constants.VIDEO, Constants.AUDIO, Constants.TEXT]; + const astInSeconds = adapter.getAvailabilityStartTime() / 1000; + + streamInfos.forEach((streamInfo) => { + const offsets = mediaTypes + .reduce((acc, mediaType) => { + acc = acc.concat(adapter.getAllMediaInfoForType(streamInfo, mediaType)); + return acc; + }, []) + .reduce((acc, mediaInfo) => { + const prts = adapter.getProducerReferenceTimes(streamInfo, mediaInfo); + prts.forEach((prt) => { + const voRepresentations = adapter.getVoRepresentations(mediaInfo); + if (voRepresentations && voRepresentations.length > 0 && voRepresentations[0].adaptation && voRepresentations[0].segmentInfoType === DashConstants.SEGMENT_TEMPLATE) { + const voRep = voRepresentations[0]; + const d = new Date(prt[DashConstants.WALL_CLOCK_TIME]); + const wallClockTime = d.getTime() / 1000; + // TS 103 285 Clause 10.20.4 + // 1) Calculate PRT0 + // i) take the PRT@presentationTime and subtract any ST@presentationTimeOffset + // ii) convert this time to seconds by dividing by ST@timescale + // iii) Add this to start time of period that contains PRT. + // N.B presentationTimeOffset is already divided by timescale at this point + const prt0 = wallClockTime - (((prt[DashConstants.PRESENTATION_TIME] / voRep[DashConstants.TIMESCALE]) - voRep[DashConstants.PRESENTATION_TIME_OFFSET]) + streamInfo.start); + // 2) Calculate TO between PRT at the start of MPD timeline and the AST + const to = astInSeconds - prt0; + // 3) Not applicable as liveLatency does not consider ST@availabilityTimeOffset + acc.push({ id: prt[DashConstants.ID], to }); + } + }); + return acc; + }, []) + + timeOffsets = timeOffsets.concat(offsets); + }) + } + prftOffsets = timeOffsets; + } catch (e) { + logger.error(e); + prftOffsets = []; + } + }; + + /** + * Calculates offset to apply to live delay as described in TS 103 285 Clause 10.20.4 + * @param {object} sdLatency - service description latency element + * @returns {number} + * @private + */ + function _calculateTimeOffset(sdLatency) { + let to = 0, id; + let offset = prftOffsets.filter(prt => { + return prt.id === sdLatency.referenceId; + }); + + // If only one ProducerReferenceTime to generate one TO, then use that regardless of matching ids + if (offset.length === 0) { + to = (prftOffsets.length > 0) ? prftOffsets[0].to : 0; + id = prftOffsets[0].id || NaN; + } else { + // If multiple id matches, use the first but this should be invalid + to = offset[0].to || 0; + id = offset[0].id || NaN; + } + + return { to, id } + } + + instance = { + getServiceDescriptionSettings, + getProducerReferenceTimeOffsets, + calculateProducerReferenceTimeOffsets, + applyServiceDescription, + reset, + setConfig + }; + + setup(); + + return instance; +} + +ServiceDescriptionController.__dashjs_factory_name = 'ServiceDescriptionController'; +export default FactoryMaker.getSingletonFactory(ServiceDescriptionController); diff --git a/src/dash/models/DashManifestModel.js b/src/dash/models/DashManifestModel.js index f345c82e54..a5603c6d2a 100644 --- a/src/dash/models/DashManifestModel.js +++ b/src/dash/models/DashManifestModel.js @@ -38,6 +38,8 @@ import UTCTiming from '../vo/UTCTiming'; import Event from '../vo/Event'; import BaseURL from '../vo/BaseURL'; import EventStream from '../vo/EventStream'; +import ProducerReferenceTime from '../vo/ProducerReferenceTime'; +import ContentSteering from '../vo/ContentSteering'; import ObjectUtils from '../../streaming/utils/ObjectUtils'; import URLUtils from '../../streaming/utils/URLUtils'; import FactoryMaker from '../../core/FactoryMaker'; @@ -66,16 +68,6 @@ function DashManifestModel() { } function getIsTypeOf(adaptation, type) { - - let i, - len, - representation, - col, - mimeTypeRegEx, - codecs; - let result = false; - let found = false; - if (!adaptation) { throw new Error('adaptation is not defined'); } @@ -84,57 +76,72 @@ function DashManifestModel() { throw new Error('type is not defined'); } - if (adaptation.hasOwnProperty('ContentComponent_asArray')) { - col = adaptation.ContentComponent_asArray; - } - - mimeTypeRegEx = (type !== Constants.TEXT) ? new RegExp(type) : new RegExp('(vtt|ttml)'); - - if (adaptation.Representation_asArray && adaptation.Representation_asArray.length && adaptation.Representation_asArray.length > 0) { - let essentialProperties = getEssentialPropertiesForRepresentation(adaptation.Representation_asArray[0]); + // Check for thumbnail images + if (adaptation.Representation_asArray && adaptation.Representation_asArray.length) { + const essentialProperties = getEssentialPropertiesForRepresentation(adaptation.Representation_asArray[0]); if (essentialProperties && essentialProperties.length > 0 && THUMBNAILS_SCHEME_ID_URIS.indexOf(essentialProperties[0].schemeIdUri) >= 0) { - return type === Constants.IMAGE; - } - if (adaptation.Representation_asArray[0].hasOwnProperty(DashConstants.CODECS)) { - // Just check the start of the codecs string - codecs = adaptation.Representation_asArray[0].codecs; - if (codecs.search(Constants.STPP) === 0 || codecs.search(Constants.WVTT) === 0) { - return type === Constants.FRAGMENTED_TEXT; - } + return (type === Constants.IMAGE); } } - if (col) { - if (col.length > 1) { + // Check ContentComponent.contentType + if (adaptation.ContentComponent_asArray && adaptation.ContentComponent_asArray.length > 0) { + if (adaptation.ContentComponent_asArray.length > 1) { return (type === Constants.MUXED); - } else if (col[0] && col[0].contentType === type) { - result = true; - found = true; + } else if (adaptation.ContentComponent_asArray[0].contentType === type) { + return true; } } + const mimeTypeRegEx = (type === Constants.TEXT) ? new RegExp('(ttml|vtt|wvtt|stpp)') : new RegExp(type); + + // Check codecs + if (adaptation.Representation_asArray && adaptation.Representation_asArray.length) { + const codecs = adaptation.Representation_asArray[0].codecs; + if (mimeTypeRegEx.test(codecs)) { + return true; + } + } + + // Check Adaptation's mimeType if (adaptation.hasOwnProperty(DashConstants.MIME_TYPE)) { - result = mimeTypeRegEx.test(adaptation.mimeType); - found = true; + return mimeTypeRegEx.test(adaptation.mimeType); } - // couldn't find on adaptationset, so check a representation - if (!found) { - i = 0; - len = adaptation.Representation_asArray && adaptation.Representation_asArray.length ? adaptation.Representation_asArray.length : 0; - while (!found && i < len) { + // Check Representation's mimeType + if (adaptation.Representation_asArray) { + let representation; + for (let i = 0; i < adaptation.Representation_asArray.length; i++) { representation = adaptation.Representation_asArray[i]; - if (representation.hasOwnProperty(DashConstants.MIME_TYPE)) { - result = mimeTypeRegEx.test(representation.mimeType); - found = true; + return mimeTypeRegEx.test(representation.mimeType); } - - i++; } } - return result; + return false; + } + + function getIsFragmented(adaptation) { + if (!adaptation) { + throw new Error('adaptation is not defined'); + } + if (adaptation.hasOwnProperty(DashConstants.SEGMENT_TEMPLATE) || + adaptation.hasOwnProperty(DashConstants.SEGMENT_TIMELINE) || + adaptation.hasOwnProperty(DashConstants.SEGMENT_LIST) || + adaptation.hasOwnProperty(DashConstants.SEGMENT_BASE)) { + return true; + } + if (adaptation.Representation_asArray && adaptation.Representation_asArray.length > 0) { + const representation = adaptation.Representation_asArray[0]; + if (representation.hasOwnProperty(DashConstants.SEGMENT_TEMPLATE) || + representation.hasOwnProperty(DashConstants.SEGMENT_TIMELINE) || + representation.hasOwnProperty(DashConstants.SEGMENT_LIST) || + representation.hasOwnProperty(DashConstants.SEGMENT_BASE)) { + return true; + } + } + return false; } function getIsAudio(adaptation) { @@ -145,8 +152,8 @@ function DashManifestModel() { return getIsTypeOf(adaptation, Constants.VIDEO); } - function getIsFragmentedText(adaptation) { - return getIsTypeOf(adaptation, Constants.FRAGMENTED_TEXT); + function getIsText(adaptation) { + return getIsTypeOf(adaptation, Constants.TEXT); } function getIsMuxed(adaptation) { @@ -157,16 +164,58 @@ function DashManifestModel() { return getIsTypeOf(adaptation, Constants.IMAGE); } - function getIsTextTrack(type) { - return (type === 'text/vtt' || type === 'application/ttml+xml'); + function getProducerReferenceTimesForAdaptation(adaptation) { + const prtArray = adaptation && adaptation.hasOwnProperty(DashConstants.PRODUCERREFERENCETIME_ASARRAY) ? adaptation[DashConstants.PRODUCERREFERENCETIME_ASARRAY] : []; + + // ProducerReferenceTime elements can also be contained in Representations + const representationsArray = adaptation && adaptation.hasOwnProperty(DashConstants.REPRESENTATION_ASARRAY) ? adaptation[DashConstants.REPRESENTATION_ASARRAY] : []; + + representationsArray.forEach((rep) => { + if (rep.hasOwnProperty(DashConstants.PRODUCERREFERENCETIME_ASARRAY)) { + prtArray.push(...rep[DashConstants.PRODUCERREFERENCETIME_ASARRAY]); + } + }); + + const prtsForAdaptation = []; + + // Unlikely to have multiple ProducerReferenceTimes. + prtArray.forEach((prt) => { + const entry = new ProducerReferenceTime(); + + if (prt.hasOwnProperty(DashConstants.ID)) { + entry[DashConstants.ID] = prt[DashConstants.ID]; + } else { + // Ignore. Missing mandatory attribute + return; + } + + if (prt.hasOwnProperty(DashConstants.WALL_CLOCK_TIME)) { + entry[DashConstants.WALL_CLOCK_TIME] = prt[DashConstants.WALL_CLOCK_TIME]; + } else { + // Ignore. Missing mandatory attribute + return; + } + + if (prt.hasOwnProperty(DashConstants.PRESENTATION_TIME)) { + entry[DashConstants.PRESENTATION_TIME] = prt[DashConstants.PRESENTATION_TIME]; + } else { + // Ignore. Missing mandatory attribute + return; + } + + // Not interested in other attributes for now + // UTC element contained must be same as that in the MPD + prtsForAdaptation.push(entry); + }) + + return prtsForAdaptation; } function getLanguageForAdaptation(adaptation) { let lang = ''; if (adaptation && adaptation.hasOwnProperty(DashConstants.LANG)) { - //Filter out any other characters not allowed according to RFC5646 - lang = adaptation.lang.replace(/[^A-Za-z0-9-]/g, ''); + lang = adaptation.lang; } return lang; @@ -405,6 +454,16 @@ function DashManifestModel() { }); } + function getSelectionPriority(realAdaption) { + try { + const priority = realAdaption && typeof realAdaption.selectionPriority !== 'undefined' ? parseInt(realAdaption.selectionPriority) : 1; + + return isNaN(priority) ? 1 : priority; + } catch (e) { + return 1; + } + } + function getEssentialPropertiesForRepresentation(realRepresentation) { if (!realRepresentation || !realRepresentation.EssentialProperty_asArray || !realRepresentation.EssentialProperty_asArray.length) return null; @@ -459,6 +518,9 @@ function DashManifestModel() { if (realRepresentation.hasOwnProperty(DashConstants.CODECS)) { voRepresentation.codecs = realRepresentation.codecs; } + if (realRepresentation.hasOwnProperty(DashConstants.MIME_TYPE)) { + voRepresentation.mimeType = realRepresentation[DashConstants.MIME_TYPE]; + } if (realRepresentation.hasOwnProperty(DashConstants.CODEC_PRIVATE_DATA)) { voRepresentation.codecPrivateData = realRepresentation.codecPrivateData; } @@ -521,7 +583,10 @@ function DashManifestModel() { // initialization source url will be determined from // BaseURL when resolved at load time. } - } else if (realRepresentation.hasOwnProperty(DashConstants.MIME_TYPE) && getIsTextTrack(realRepresentation.mimeType)) { + } else if (getIsText(processedRealAdaptation) && + getIsFragmented(processedRealAdaptation) && + processedRealAdaptation.mimeType && + processedRealAdaptation.mimeType.indexOf('application/mp4') === -1) { voRepresentation.range = 0; } @@ -575,6 +640,9 @@ function DashManifestModel() { } function calcSegmentDuration(segmentTimeline) { + if (!segmentTimeline || !segmentTimeline.S_asArray) { + return NaN; + } let s0 = segmentTimeline.S_asArray[0]; let s1 = segmentTimeline.S_asArray[1]; return s0.hasOwnProperty('d') ? s0.d : (s1.t - s0.t); @@ -610,12 +678,12 @@ function DashManifestModel() { voAdaptationSet.type = Constants.AUDIO; } else if (getIsVideo(realAdaptationSet)) { voAdaptationSet.type = Constants.VIDEO; - } else if (getIsFragmentedText(realAdaptationSet)) { - voAdaptationSet.type = Constants.FRAGMENTED_TEXT; + } else if (getIsText(realAdaptationSet)) { + voAdaptationSet.type = Constants.TEXT; } else if (getIsImage(realAdaptationSet)) { voAdaptationSet.type = Constants.IMAGE; } else { - voAdaptationSet.type = Constants.TEXT; + logger.warn('Unknown Adaptation stream type'); } voAdaptations.push(voAdaptationSet); } @@ -682,6 +750,10 @@ function DashManifestModel() { voPeriod.duration = realPeriod.duration; } + if (voPreviousPeriod) { + voPreviousPeriod.nextPeriodId = voPeriod.id; + } + voPeriods.push(voPeriod); realPreviousPeriod = realPeriod; voPreviousPeriod = voPeriod; @@ -825,14 +897,17 @@ function DashManifestModel() { if (currentMpdEvent.hasOwnProperty(DashConstants.PRESENTATION_TIME)) { event.presentationTime = currentMpdEvent.presentationTime; - const presentationTimeOffset = eventStream.presentationTimeOffset ? eventStream.presentationTimeOffset / eventStream.timescale : 0; - event.calculatedPresentationTime = event.presentationTime / eventStream.timescale + period.start - presentationTimeOffset; } + const presentationTimeOffset = eventStream.presentationTimeOffset ? eventStream.presentationTimeOffset / eventStream.timescale : 0; + event.calculatedPresentationTime = event.presentationTime / eventStream.timescale + period.start - presentationTimeOffset; + if (currentMpdEvent.hasOwnProperty(DashConstants.DURATION)) { event.duration = currentMpdEvent.duration / eventStream.timescale; } if (currentMpdEvent.hasOwnProperty(DashConstants.ID)) { event.id = currentMpdEvent.id; + } else { + event.id = null; } if (currentMpdEvent.Signal && currentMpdEvent.Signal.Binary) { @@ -845,6 +920,7 @@ function DashManifestModel() { // string representation'. event.messageData = currentMpdEvent.messageData || + currentMpdEvent.__cdata || currentMpdEvent.__text; } @@ -856,7 +932,7 @@ function DashManifestModel() { return events; } - function getEventStreams(inbandStreams, representation) { + function getEventStreams(inbandStreams, representation, period) { const eventStreams = []; let i; @@ -879,12 +955,13 @@ function DashManifestModel() { eventStream.value = inbandStreams[i].value; } eventStreams.push(eventStream); + eventStream.period = period; } return eventStreams; } - function getEventStreamForAdaptationSet(manifest, adaptation) { + function getEventStreamForAdaptationSet(manifest, adaptation, period) { let inbandStreams, periodArray, adaptationArray; @@ -899,10 +976,10 @@ function DashManifestModel() { } } - return getEventStreams(inbandStreams, null); + return getEventStreams(inbandStreams, null, period); } - function getEventStreamForRepresentation(manifest, representation) { + function getEventStreamForRepresentation(manifest, representation, period) { let inbandStreams, periodArray, adaptationArray, @@ -921,7 +998,7 @@ function DashManifestModel() { } } - return getEventStreams(inbandStreams, representation); + return getEventStreams(inbandStreams, representation, period); } function getUTCTimingSources(manifest) { @@ -1039,6 +1116,32 @@ function DashManifestModel() { return baseUrls; } + function getContentSteering(manifest) { + if (manifest && manifest.hasOwnProperty(DashConstants.CONTENT_STEERING_AS_ARRAY)) { + // Only one ContentSteering element is supported on MPD level + const element = manifest[DashConstants.CONTENT_STEERING_AS_ARRAY][0]; + const entry = new ContentSteering(); + + entry.serverUrl = element.__text; + + if (element.hasOwnProperty(DashConstants.DEFAULT_SERVICE_LOCATION)) { + entry.defaultServiceLocation = element[DashConstants.DEFAULT_SERVICE_LOCATION]; + } + + if (element.hasOwnProperty(DashConstants.QUERY_BEFORE_START)) { + entry.queryBeforeStart = element[DashConstants.QUERY_BEFORE_START].toLowerCase() === 'true'; + } + + if (element.hasOwnProperty(DashConstants.PROXY_SERVER_URL)) { + entry.proxyServerUrl = element[DashConstants.PROXY_SERVER_URL]; + } + + return entry; + } + + return undefined; + } + function getLocation(manifest) { if (manifest && manifest.hasOwnProperty(Constants.LOCATION)) { // for now, do not support multiple Locations - @@ -1077,7 +1180,13 @@ function DashManifestModel() { if (manifest && manifest.hasOwnProperty(DashConstants.SERVICE_DESCRIPTION)) { for (const sd of manifest.ServiceDescription_asArray) { // Convert each of the properties defined in - let id, schemeIdUri, latency, playbackRate; + let id = null, + schemeIdUri = null, + latency = null, + playbackRate = null, + operatingQuality = null, + operatingBandwidth = null; + for (const prop in sd) { if (sd.hasOwnProperty(prop)) { if (prop === DashConstants.ID) { @@ -1086,34 +1195,51 @@ function DashManifestModel() { schemeIdUri = sd[prop].schemeIdUri; } else if (prop === DashConstants.SERVICE_DESCRIPTION_LATENCY) { latency = { - target: sd[prop].target, - max: sd[prop].max, - min: sd[prop].min + target: parseInt(sd[prop].target), + max: parseInt(sd[prop].max), + min: parseInt(sd[prop].min), + referenceId: parseInt(sd[prop].referenceId) }; } else if (prop === DashConstants.SERVICE_DESCRIPTION_PLAYBACK_RATE) { playbackRate = { - max: sd[prop].max, - min: sd[prop].min + max: parseFloat(sd[prop].max), + min: parseFloat(sd[prop].min) }; + } else if (prop === DashConstants.SERVICE_DESCRIPTION_OPERATING_QUALITY) { + operatingQuality = { + mediaType: sd[prop].mediaType, + max: parseInt(sd[prop].max), + min: parseInt(sd[prop].min), + target: parseInt(sd[prop].target), + type: sd[prop].type, + maxQualityDifference: parseInt(sd[prop].maxQualityDifference) + } + } else if (prop === DashConstants.SERVICE_DESCRIPTION_OPERATING_BANDWIDTH) { + operatingBandwidth = { + mediaType: sd[prop].mediaType, + max: parseInt(sd[prop].max), + min: parseInt(sd[prop].min), + target: parseInt(sd[prop].target) + } } } } - // we have a ServiceDescription for low latency. Add it if it really has parameters defined - if (schemeIdUri === Constants.SERVICE_DESCRIPTION_LL_SCHEME && (latency || playbackRate)) { - serviceDescriptions.push({ - id, - schemeIdUri, - latency, - playbackRate - }); - } + + serviceDescriptions.push({ + id, + schemeIdUri, + latency, + playbackRate, + operatingQuality, + operatingBandwidth + }); } } return serviceDescriptions; } - function getSupplementalPropperties(adaptation) { + function getSupplementalProperties(adaptation) { const supplementalProperties = {}; if (adaptation && adaptation.hasOwnProperty(DashConstants.SUPPLEMENTAL_PROPERTY)) { @@ -1139,53 +1265,57 @@ function DashManifestModel() { } instance = { - getIsTypeOf: getIsTypeOf, - getIsTextTrack: getIsTextTrack, - getLanguageForAdaptation: getLanguageForAdaptation, - getViewpointForAdaptation: getViewpointForAdaptation, - getRolesForAdaptation: getRolesForAdaptation, - getAccessibilityForAdaptation: getAccessibilityForAdaptation, - getAudioChannelConfigurationForAdaptation: getAudioChannelConfigurationForAdaptation, - getAudioChannelConfigurationForRepresentation: getAudioChannelConfigurationForRepresentation, - getAdaptationForIndex: getAdaptationForIndex, - getIndexForAdaptation: getIndexForAdaptation, - getAdaptationForId: getAdaptationForId, - getAdaptationsForType: getAdaptationsForType, + getIsTypeOf, + getIsText, + getIsFragmented, + getProducerReferenceTimesForAdaptation, + getLanguageForAdaptation, + getViewpointForAdaptation, + getRolesForAdaptation, + getAccessibilityForAdaptation, + getAudioChannelConfigurationForAdaptation, + getAudioChannelConfigurationForRepresentation, + getAdaptationForIndex, + getIndexForAdaptation, + getAdaptationForId, + getAdaptationsForType, getRealPeriods, getRealPeriodForIndex, - getCodec: getCodec, - getMimeType: getMimeType, - getKID: getKID, - getLabelsForAdaptation: getLabelsForAdaptation, - getContentProtectionData: getContentProtectionData, - getIsDynamic: getIsDynamic, - getId: getId, - hasProfile: hasProfile, - getDuration: getDuration, - getBandwidth: getBandwidth, - getManifestUpdatePeriod: getManifestUpdatePeriod, - getPublishTime: getPublishTime, - getRepresentationCount: getRepresentationCount, - getBitrateListForAdaptation: getBitrateListForAdaptation, - getRepresentationFor: getRepresentationFor, - getRepresentationsForAdaptation: getRepresentationsForAdaptation, - getAdaptationsForPeriod: getAdaptationsForPeriod, - getRegularPeriods: getRegularPeriods, - getMpd: getMpd, - getEventsForPeriod: getEventsForPeriod, + getCodec, + getSelectionPriority, + getMimeType, + getKID, + getLabelsForAdaptation, + getContentProtectionData, + getIsDynamic, + getId, + hasProfile, + getDuration, + getBandwidth, + getManifestUpdatePeriod, + getPublishTime, + getRepresentationCount, + getBitrateListForAdaptation, + getRepresentationFor, + getRepresentationsForAdaptation, + getAdaptationsForPeriod, + getRegularPeriods, + getMpd, + getEventsForPeriod, getEssentialPropertiesForRepresentation, - getEventStreamForAdaptationSet: getEventStreamForAdaptationSet, - getEventStreamForRepresentation: getEventStreamForRepresentation, - getUTCTimingSources: getUTCTimingSources, - getBaseURLsFromElement: getBaseURLsFromElement, - getRepresentationSortFunction: getRepresentationSortFunction, - getLocation: getLocation, - getPatchLocation: getPatchLocation, - getSuggestedPresentationDelay: getSuggestedPresentationDelay, - getAvailabilityStartTime: getAvailabilityStartTime, - getServiceDescriptions: getServiceDescriptions, - getSupplementalPropperties: getSupplementalPropperties, - setConfig: setConfig + getEventStreamForAdaptationSet, + getEventStreamForRepresentation, + getUTCTimingSources, + getBaseURLsFromElement, + getRepresentationSortFunction, + getContentSteering, + getLocation, + getPatchLocation, + getSuggestedPresentationDelay, + getAvailabilityStartTime, + getServiceDescriptions, + getSupplementalProperties, + setConfig }; setup(); diff --git a/src/dash/parser/DashParser.js b/src/dash/parser/DashParser.js index 50a9f4a036..cc417c7b14 100644 --- a/src/dash/parser/DashParser.js +++ b/src/dash/parser/DashParser.js @@ -35,6 +35,7 @@ import StringMatcher from './matchers/StringMatcher'; import DurationMatcher from './matchers/DurationMatcher'; import DateTimeMatcher from './matchers/DateTimeMatcher'; import NumericMatcher from './matchers/NumericMatcher'; +import LangMatcher from './matchers/LangMatcher'; import RepresentationBaseValuesMap from './maps/RepresentationBaseValuesMap'; import SegmentValuesMap from './maps/SegmentValuesMap'; @@ -56,6 +57,7 @@ function DashParser(config) { new DurationMatcher(), new DateTimeMatcher(), new NumericMatcher(), + new LangMatcher(), new StringMatcher() // last in list to take precedence over NumericMatcher ]; diff --git a/src/dash/parser/matchers/LangMatcher.js b/src/dash/parser/matchers/LangMatcher.js new file mode 100644 index 0000000000..64d40162ac --- /dev/null +++ b/src/dash/parser/matchers/LangMatcher.js @@ -0,0 +1,71 @@ +/** + * The copyright in this software is being made available under the BSD License, + * included below. This software may be subject to other third party and contributor + * rights, including patent rights, and no such rights are granted under this license. + * + * Copyright (c) 2013, Dash Industry Forum. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * * Neither the name of Dash Industry Forum nor the names of its + * contributors may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +/** + * @classdesc Matches and converts any ISO 639 language tag to BCP-47 language tags + */ +import BaseMatcher from './BaseMatcher'; +import DashConstants from '../../constants/DashConstants'; +import bcp47Normalize from 'bcp-47-normalize'; + +class LangMatcher extends BaseMatcher { + constructor() { + super( + (attr, nodeName) => { + const stringAttrsInElements = { + [DashConstants.ADAPTATION_SET]: [ DashConstants.LANG ], + [DashConstants.REPRESENTATION]: [ DashConstants.LANG ], + [DashConstants.CONTENT_COMPONENT]: [ DashConstants.LANG ], + [DashConstants.LABEL]: [ DashConstants.LANG ], + [DashConstants.GROUP_LABEL]: [ DashConstants.LANG ] + // still missing from 23009-1: Preselection@lang, ProgramInformation@lang + }; + if (stringAttrsInElements.hasOwnProperty(nodeName)) { + let attrNames = stringAttrsInElements[nodeName]; + if (attrNames !== undefined) { + return attrNames.indexOf(attr.name) >= 0; + } else { + return false; + } + } + return false; + }, + str => { + let lang = bcp47Normalize(str); + if (lang !== undefined) { + return lang; + } + return String(str); + } + ); + } +} + +export default LangMatcher; diff --git a/src/dash/utils/ListSegmentsGetter.js b/src/dash/utils/ListSegmentsGetter.js index 5c458e00cd..973c047eeb 100644 --- a/src/dash/utils/ListSegmentsGetter.js +++ b/src/dash/utils/ListSegmentsGetter.js @@ -47,6 +47,22 @@ function ListSegmentsGetter(config, isDynamic) { } } + function getMediaFinishedInformation(representation) { + const mediaFinishedInformation = { numberOfSegments: 0, mediaTimeOfLastSignaledSegment: NaN } + + if (!representation) { + return mediaFinishedInformation; + } + + const list = representation.adaptation.period.mpd.manifest.Period_asArray[representation.adaptation.period.index].AdaptationSet_asArray[representation.adaptation.index].Representation_asArray[representation.index].SegmentList; + const startNumber = representation && !isNaN(representation.startNumber) ? representation.startNumber : 1; + const offset = Math.max(startNumber - 1, 0); + + mediaFinishedInformation.numberOfSegments = offset + list.SegmentURL_asArray.length; + + return mediaFinishedInformation + } + function getSegmentByIndex(representation, index) { checkConfig(); @@ -71,13 +87,10 @@ function ListSegmentsGetter(config, isDynamic) { segment.replacementTime = (startNumber + index - 1) * representation.segmentDuration; segment.media = s.media ? s.media : ''; segment.mediaRange = s.mediaRange; - segment.index = index; segment.indexRange = s.indexRange; } } - representation.availableSegmentsNumber = len; - return segment; } @@ -101,8 +114,9 @@ function ListSegmentsGetter(config, isDynamic) { } instance = { - getSegmentByIndex: getSegmentByIndex, - getSegmentByTime: getSegmentByTime + getSegmentByIndex, + getSegmentByTime, + getMediaFinishedInformation }; return instance; diff --git a/src/dash/utils/SegmentBaseGetter.js b/src/dash/utils/SegmentBaseGetter.js index 3bddfcfdf5..e1491574b1 100644 --- a/src/dash/utils/SegmentBaseGetter.js +++ b/src/dash/utils/SegmentBaseGetter.js @@ -46,6 +46,18 @@ function SegmentBaseGetter(config) { } } + function getMediaFinishedInformation(representation) { + const mediaFinishedInformation = { numberOfSegments: 0, mediaTimeOfLastSignaledSegment: NaN } + + if (!representation || !representation.segments) { + return mediaFinishedInformation + } + + mediaFinishedInformation.numberOfSegments = representation.segments.length; + + return mediaFinishedInformation; + } + function getSegmentByIndex(representation, index) { checkConfig(); @@ -57,7 +69,7 @@ function SegmentBaseGetter(config) { let seg; if (index < len) { seg = representation.segments[index]; - if (seg && seg.availabilityIdx === index) { + if (seg && seg.index === index) { return seg; } } @@ -65,7 +77,7 @@ function SegmentBaseGetter(config) { for (let i = 0; i < len; i++) { seg = representation.segments[i]; - if (seg && seg.availabilityIdx === index) { + if (seg && seg.index === index) { return seg; } } @@ -91,21 +103,21 @@ function SegmentBaseGetter(config) { let idx = -1; let epsilon, - frag, + seg, ft, fd, i; if (segments && ln > 0) { for (i = 0; i < ln; i++) { - frag = segments[i]; - ft = frag.presentationStartTime; - fd = frag.duration; + seg = segments[i]; + ft = seg.presentationStartTime; + fd = seg.duration; epsilon = fd / 2; if ((time + epsilon) >= ft && (time - epsilon) < (ft + fd)) { - idx = frag.availabilityIdx; + idx = seg.index; break; } } @@ -115,8 +127,9 @@ function SegmentBaseGetter(config) { } instance = { - getSegmentByIndex: getSegmentByIndex, - getSegmentByTime: getSegmentByTime + getSegmentByIndex, + getSegmentByTime, + getMediaFinishedInformation }; return instance; diff --git a/src/dash/utils/SegmentsUtils.js b/src/dash/utils/SegmentsUtils.js index f5785332e7..7ea5ae8c14 100644 --- a/src/dash/utils/SegmentsUtils.js +++ b/src/dash/utils/SegmentsUtils.js @@ -31,6 +31,7 @@ import Segment from './../vo/Segment'; + function zeroPadToLength(numStr, minStrLength) { while (numStr.length < minStrLength) { numStr = '0' + numStr; @@ -128,38 +129,43 @@ export function replaceTokenForTemplate(url, token, value) { } } -function getSegment(representation, duration, presentationStartTime, mediaStartTime, availabilityStartTime, - timelineConverter, presentationEndTime, isDynamic, index) { +function getSegment(representation, duration, presentationStartTime, mediaStartTime, timelineConverter, presentationEndTime, isDynamic, index) { let seg = new Segment(); seg.representation = representation; seg.duration = duration; seg.presentationStartTime = presentationStartTime; seg.mediaStartTime = mediaStartTime; - seg.availabilityStartTime = availabilityStartTime; - seg.availabilityEndTime = timelineConverter.calcAvailabilityEndTimeFromPresentationTime(presentationEndTime, representation.adaptation.period.mpd, isDynamic); + seg.availabilityStartTime = timelineConverter.calcAvailabilityStartTimeFromPresentationTime(presentationEndTime, representation, isDynamic); + seg.availabilityEndTime = timelineConverter.calcAvailabilityEndTimeFromPresentationTime(presentationEndTime + duration, representation, isDynamic); seg.wallStartTime = timelineConverter.calcWallTimeForSegment(seg, isDynamic); seg.replacementNumber = getNumberForSegment(seg, index); - seg.availabilityIdx = index; + seg.index = index; return seg; } function isSegmentAvailable(timelineConverter, representation, segment, isDynamic) { - const periodEnd = timelineConverter.getPeriodEnd(representation, isDynamic); - const periodRelativeEnd = timelineConverter.calcPeriodRelativeTimeFromMpdRelativeTime(representation, periodEnd); - - const segmentTime = timelineConverter.calcPeriodRelativeTimeFromMpdRelativeTime(representation, segment.presentationStartTime); - if (segmentTime >= periodRelativeEnd) { - if (isDynamic) { - // segment is not available in current period, but it may be segment available in another period that current one (in DVR window) - // if not (time > segmentAvailabilityRange.end), then return false - if (representation.segmentAvailabilityRange && segment.presentationStartTime >= representation.segmentAvailabilityRange.end) { - return false; - } - } else { - return false; + const voPeriod = representation.adaptation.period; + + // Avoid requesting segments that overlap the period boundary + if (isFinite(voPeriod.duration) && voPeriod.start + voPeriod.duration <= segment.presentationStartTime) { + return false; + } + + if (isDynamic) { + + if (representation.availabilityTimeOffset === 'INF') { + return true; } + + // For dynamic manifests we check if the presentation start time + duration is included in the availability window + // SAST = Period@start + seg@presentationStartTime + seg@duration + // ASAST = SAST - ATO + // SAET = SAST + TSBD + seg@duration + // refTime serves as an anchor time to compare the availability time of the segments against. + const refTime = timelineConverter.getClientReferenceTime(); + return segment.availabilityStartTime.getTime() <= refTime && (!isFinite(segment.availabilityEndTime) || segment.availabilityEndTime.getTime() >= refTime); } return true; @@ -170,6 +176,7 @@ export function getIndexBasedSegment(timelineConverter, isDynamic, representatio presentationStartTime, presentationEndTime; + duration = representation.segmentDuration; /* @@ -184,9 +191,9 @@ export function getIndexBasedSegment(timelineConverter, isDynamic, representatio presentationStartTime = parseFloat((representation.adaptation.period.start + (index * duration)).toFixed(5)); presentationEndTime = parseFloat((presentationStartTime + duration).toFixed(5)); - const segment = getSegment(representation, duration, presentationStartTime, - timelineConverter.calcMediaTimeFromPresentationTime(presentationStartTime, representation), - timelineConverter.calcAvailabilityStartTimeFromPresentationTime(presentationStartTime, representation.adaptation.period.mpd, isDynamic), + const mediaTime = timelineConverter.calcMediaTimeFromPresentationTime(presentationStartTime, representation); + + const segment = getSegment(representation, duration, presentationStartTime, mediaTime, timelineConverter, presentationEndTime, isDynamic, index); if (!isSegmentAvailable(timelineConverter, representation, segment, isDynamic)) { @@ -198,7 +205,7 @@ export function getIndexBasedSegment(timelineConverter, isDynamic, representatio export function getTimeBasedSegment(timelineConverter, isDynamic, representation, time, duration, fTimescale, url, range, index, tManifest) { const scaledTime = time / fTimescale; - const scaledDuration = Math.min(duration / fTimescale, representation.adaptation.period.mpd.maxSegmentDuration); + const scaledDuration = duration / fTimescale; let presentationStartTime, presentationEndTime, @@ -209,7 +216,6 @@ export function getTimeBasedSegment(timelineConverter, isDynamic, representation seg = getSegment(representation, scaledDuration, presentationStartTime, scaledTime, - representation.adaptation.period.mpd.manifest.loadedTime, timelineConverter, presentationEndTime, isDynamic, index); if (!isSegmentAvailable(timelineConverter, representation, seg, isDynamic)) { diff --git a/src/dash/utils/TemplateSegmentsGetter.js b/src/dash/utils/TemplateSegmentsGetter.js index 30d742d60c..9167e1635c 100644 --- a/src/dash/utils/TemplateSegmentsGetter.js +++ b/src/dash/utils/TemplateSegmentsGetter.js @@ -32,7 +32,7 @@ import FactoryMaker from '../../core/FactoryMaker'; import Constants from '../../streaming/constants/Constants'; -import { replaceTokenForTemplate, getIndexBasedSegment } from './SegmentsUtils'; +import {replaceTokenForTemplate, getIndexBasedSegment} from './SegmentsUtils'; function TemplateSegmentsGetter(config, isDynamic) { config = config || {}; @@ -46,6 +46,22 @@ function TemplateSegmentsGetter(config, isDynamic) { } } + function getMediaFinishedInformation(representation) { + const mediaFinishedInformation = { numberOfSegments: 0, mediaTimeOfLastSignaledSegment: NaN } + if (!representation) { + return mediaFinishedInformation + } + + const duration = representation.segmentDuration; + if (isNaN(duration)) { + mediaFinishedInformation.numberOfSegments = 1; + } else { + mediaFinishedInformation.numberOfSegments = Math.ceil(representation.adaptation.period.duration / duration); + } + + return mediaFinishedInformation; + } + function getSegmentByIndex(representation, index) { checkConfig(); @@ -53,14 +69,14 @@ function TemplateSegmentsGetter(config, isDynamic) { return null; } - const template = representation.adaptation.period.mpd.manifest.Period_asArray[representation.adaptation.period.index]. - AdaptationSet_asArray[representation.adaptation.index].Representation_asArray[representation.index].SegmentTemplate; + const template = representation.adaptation.period.mpd.manifest.Period_asArray[representation.adaptation.period.index].AdaptationSet_asArray[representation.adaptation.index].Representation_asArray[representation.index].SegmentTemplate; + // This is the index without @startNumber index = Math.max(index, 0); const seg = getIndexBasedSegment(timelineConverter, isDynamic, representation, index); if (seg) { - seg.replacementTime = Math.round((index - 1) * representation.segmentDuration * representation.timescale,10); + seg.replacementTime = Math.round((index - 1) * representation.segmentDuration * representation.timescale, 10); let url = template.media; url = replaceTokenForTemplate(url, 'Number', seg.replacementNumber); @@ -68,15 +84,6 @@ function TemplateSegmentsGetter(config, isDynamic) { seg.media = url; } - const duration = representation.segmentDuration; - const availabilityWindow = representation.segmentAvailabilityRange; - if (isNaN(duration)) { - representation.availableSegmentsNumber = 1; - } - else { - representation.availableSegmentsNumber = Math.ceil((availabilityWindow.end - availabilityWindow.start) / duration); - } - return seg; } @@ -93,15 +100,17 @@ function TemplateSegmentsGetter(config, isDynamic) { return null; } - const periodTime = timelineConverter.calcPeriodRelativeTimeFromMpdRelativeTime(representation, requestedTime); + // Calculate the relative time for the requested time in this period + let periodTime = timelineConverter.calcPeriodRelativeTimeFromMpdRelativeTime(representation, requestedTime); const index = Math.floor(periodTime / duration); return getSegmentByIndex(representation, index); } instance = { - getSegmentByIndex: getSegmentByIndex, - getSegmentByTime: getSegmentByTime + getSegmentByIndex, + getSegmentByTime, + getMediaFinishedInformation }; return instance; diff --git a/src/dash/utils/TimelineConverter.js b/src/dash/utils/TimelineConverter.js index 34c8d6652d..83fe18587f 100644 --- a/src/dash/utils/TimelineConverter.js +++ b/src/dash/utils/TimelineConverter.js @@ -34,6 +34,9 @@ import FactoryMaker from '../../core/FactoryMaker'; import DashConstants from '../constants/DashConstants'; import DashManifestModel from '../models/DashManifestModel'; import Settings from '../../core/Settings'; +import Constants from '../../streaming/constants/Constants'; +import MediaPlayerEvents from '../../streaming/MediaPlayerEvents'; +import ConformanceViolationConstants from '../../streaming/constants/ConformanceViolationConstants'; function TimelineConverter() { @@ -43,9 +46,8 @@ function TimelineConverter() { let instance, dashManifestModel, - clientServerTimeShift, - isClientServerTimeSyncCompleted, - expectedLiveEdge; + timelineAnchorAvailabilityOffset, // In case we calculate the TSBD using _calcTimeShiftBufferWindowForDynamicTimelineManifest we use the segments as anchor times. We apply this offset when calculating if a segment is available or not. + clientServerTimeShift; function setup() { dashManifestModel = DashManifestModel(context).getInstance(); @@ -65,44 +67,51 @@ function TimelineConverter() { clientServerTimeShift = value; } - function getExpectedLiveEdge() { - return expectedLiveEdge; + /** + * Returns a "now" reference time for the client to compare the availability time of a segment against. + * Takes the client/server drift into account + */ + function getClientReferenceTime() { + return Date.now() - (timelineAnchorAvailabilityOffset * 1000) + (clientServerTimeShift * 1000); } - function setExpectedLiveEdge(value) { - expectedLiveEdge = value; - } - - function calcAvailabilityTimeFromPresentationTime(presentationTime, mpd, isDynamic, calculateEnd) { - let availabilityTime = NaN; + function _calcAvailabilityTimeFromPresentationTime(presentationEndTime, representation, isDynamic, calculateAvailabilityEndTime) { + let availabilityTime; + let mpd = representation.adaptation.period.mpd; + const availabilityStartTime = mpd.availabilityStartTime; - if (calculateEnd) { + if (calculateAvailabilityEndTime) { //@timeShiftBufferDepth specifies the duration of the time shifting buffer that is guaranteed // to be available for a Media Presentation with type 'dynamic'. // When not present, the value is infinite. - if (isDynamic && (mpd.timeShiftBufferDepth != Number.POSITIVE_INFINITY)) { - availabilityTime = new Date(mpd.availabilityStartTime.getTime() + ((presentationTime + mpd.timeShiftBufferDepth) * 1000)); + if (isDynamic && mpd.timeShiftBufferDepth !== Number.POSITIVE_INFINITY) { + // SAET = SAST + TSBD + seg@duration + availabilityTime = new Date(availabilityStartTime.getTime() + ((presentationEndTime + mpd.timeShiftBufferDepth) * 1000)); } else { availabilityTime = mpd.availabilityEndTime; } } else { if (isDynamic) { - availabilityTime = new Date(mpd.availabilityStartTime.getTime() + (presentationTime - clientServerTimeShift) * 1000); + // SAST = Period@start + seg@presentationStartTime + seg@duration + // ASAST = SAST - ATO + const availabilityTimeOffset = representation.availabilityTimeOffset; + // presentationEndTime = Period@start + seg@presentationStartTime + Segment@duration + availabilityTime = new Date(availabilityStartTime.getTime() + (presentationEndTime - availabilityTimeOffset) * 1000); } else { // in static mpd, all segments are available at the same time - availabilityTime = mpd.availabilityStartTime; + availabilityTime = availabilityStartTime; } } return availabilityTime; } - function calcAvailabilityStartTimeFromPresentationTime(presentationTime, mpd, isDynamic) { - return calcAvailabilityTimeFromPresentationTime.call(this, presentationTime, mpd, isDynamic); + function calcAvailabilityStartTimeFromPresentationTime(presentationEndTime, representation, isDynamic) { + return _calcAvailabilityTimeFromPresentationTime(presentationEndTime, representation, isDynamic); } - function calcAvailabilityEndTimeFromPresentationTime(presentationTime, mpd, isDynamic) { - return calcAvailabilityTimeFromPresentationTime.call(this, presentationTime, mpd, isDynamic, true); + function calcAvailabilityEndTimeFromPresentationTime(presentationEndTime, representation, isDynamic) { + return _calcAvailabilityTimeFromPresentationTime(presentationEndTime, representation, isDynamic, true); } function calcPresentationTimeFromWallTime(wallTime, period) { @@ -137,43 +146,177 @@ function TimelineConverter() { return wallTime; } - function calcSegmentAvailabilityRange(voRepresentation, isDynamic) { - // Static Range Finder - const voPeriod = voRepresentation.adaptation.period; - const range = {start: voPeriod.start, end: voPeriod.start + voPeriod.duration}; - if (!isDynamic) return range; + /** + * Calculates the timeshiftbuffer range. This range might overlap multiple periods and is not limited to period boundaries. However, we make sure that the range is potentially covered by period. + * @param {Array} streams + * @param {boolean} isDynamic + * @return {} + */ + function calcTimeShiftBufferWindow(streams, isDynamic) { + // Static manifests. The availability window is equal to the DVR window + if (!isDynamic) { + return _calcTimeshiftBufferForStaticManifest(streams); + } - if (!isClientServerTimeSyncCompleted && voRepresentation.segmentAvailabilityRange) { - return voRepresentation.segmentAvailabilityRange; + // Specific use case of SegmentTimeline + if (settings.get().streaming.timeShiftBuffer.calcFromSegmentTimeline) { + const data = _calcTimeShiftBufferWindowForDynamicTimelineManifest(streams); + _adjustTimelineAnchorAvailabilityOffset(data.now, data.range); + + return data.range; } - // Dynamic Range Finder - const d = voRepresentation.segmentDuration || (voRepresentation.segments && voRepresentation.segments.length ? voRepresentation.segments[voRepresentation.segments.length - 1].duration : 0); + return _calcTimeShiftBufferWindowForDynamicManifest(streams); + } + + function _calcTimeshiftBufferForStaticManifest(streams) { + // Static Range Finder. We iterate over all periods and return the total duration + const range = { start: NaN, end: NaN }; + let duration = 0; + let start = NaN; + streams.forEach((stream) => { + const streamInfo = stream.getStreamInfo(); + duration += streamInfo.duration; + + if (isNaN(start) || streamInfo.start < start) { + start = streamInfo.start; + } + }); + + range.start = start; + range.end = start + duration; - // Specific use case of SegmentTimeline without timeShiftBufferDepth - if (voRepresentation.segmentInfoType === DashConstants.SEGMENT_TIMELINE && settings.get().streaming.calcSegmentAvailabilityRangeFromTimeline) { - return calcSegmentAvailabilityRangeFromTimeline(voRepresentation); + return range; + } + + function _calcTimeShiftBufferWindowForDynamicManifest(streams) { + const range = { start: NaN, end: NaN }; + + if (!streams || streams.length === 0) { + return range; } + const voPeriod = streams[0].getAdapter().getRegularPeriods()[0]; const now = calcPresentationTimeFromWallTime(new Date(), voPeriod); - const periodEnd = voPeriod.start + voPeriod.duration; - range.start = Math.max((now - voPeriod.mpd.timeShiftBufferDepth), voPeriod.start); - - const endOffset = voRepresentation.availabilityTimeOffset !== undefined && - voRepresentation.availabilityTimeOffset < d ? d - voRepresentation.availabilityTimeOffset : d; + const timeShiftBufferDepth = voPeriod.mpd.timeShiftBufferDepth; + const start = !isNaN(timeShiftBufferDepth) ? now - timeShiftBufferDepth : 0; + // check if we find a suitable period for that starttime. Otherwise we use the time closest to that + range.start = _adjustTimeBasedOnPeriodRanges(streams, start); + range.end = !isNaN(range.start) && now < range.start ? now : _adjustTimeBasedOnPeriodRanges(streams, now, true); + + if (!isNaN(timeShiftBufferDepth) && range.end < now - timeShiftBufferDepth) { + range.end = NaN; + } - range.end = now >= periodEnd && now - endOffset < periodEnd ? periodEnd : now - endOffset; + // If we have SegmentTimeline as a reference we can verify that the calculated DVR window is at least partially included in the DVR window exposed by the timeline. + // If that is not the case we stick to the DVR window defined by SegmentTimeline + if (settings.get().streaming.timeShiftBuffer.fallbackToSegmentTimeline) { + const timelineRefData = _calcTimeShiftBufferWindowForDynamicTimelineManifest(streams); + if (timelineRefData.range.end < range.start) { + eventBus.trigger(MediaPlayerEvents.CONFORMANCE_VIOLATION, { + level: ConformanceViolationConstants.LEVELS.WARNING, + event: ConformanceViolationConstants.EVENTS.INVALID_DVR_WINDOW + }); + _adjustTimelineAnchorAvailabilityOffset(timelineRefData.now, timelineRefData.range); + return timelineRefData.range; + } + } return range; } - function calcSegmentAvailabilityRangeFromTimeline(voRepresentation) { + function _calcTimeShiftBufferWindowForDynamicTimelineManifest(streams) { + const range = { start: NaN, end: NaN }; + const voPeriod = streams[0].getAdapter().getRegularPeriods()[0]; + const now = calcPresentationTimeFromWallTime(new Date(), voPeriod); + + if (!streams || streams.length === 0) { + return { range, now }; + } + + streams.forEach((stream) => { + const adapter = stream.getAdapter(); + const mediaInfo = adapter.getMediaInfoForType(stream.getStreamInfo(), Constants.VIDEO) || adapter.getMediaInfoForType(stream.getStreamInfo(), Constants.AUDIO); + const voRepresentations = adapter.getVoRepresentations(mediaInfo); + const voRepresentation = voRepresentations[0]; + let periodRange = { start: NaN, end: NaN }; + + if (voRepresentation) { + if (voRepresentation.segmentInfoType === DashConstants.SEGMENT_TIMELINE) { + periodRange = _calcRangeForTimeline(voRepresentation); + } else { + const currentVoPeriod = voRepresentation.adaptation.period; + periodRange.start = currentVoPeriod.start; + periodRange.end = Math.max(now, currentVoPeriod.start + currentVoPeriod.duration); + } + } + + if (!isNaN(periodRange.start) && (isNaN(range.start) || range.start > periodRange.start)) { + range.start = periodRange.start; + } + if (!isNaN(periodRange.end) && (isNaN(range.end) || range.end < periodRange.end)) { + range.end = periodRange.end; + } + }); + + + range.end = Math.min(now, range.end); + const adjustedEndTime = _adjustTimeBasedOnPeriodRanges(streams, range.end, true); + + // if range is NaN all periods are in the future. we should return range.start > range.end in this case + range.end = isNaN(adjustedEndTime) ? range.end : adjustedEndTime; + + range.start = voPeriod && voPeriod.mpd && voPeriod.mpd.timeShiftBufferDepth && !isNaN(voPeriod.mpd.timeShiftBufferDepth) && !isNaN(range.end) ? Math.max(range.end - voPeriod.mpd.timeShiftBufferDepth, range.start) : range.start; + range.start = _adjustTimeBasedOnPeriodRanges(streams, range.start); + + return { range, now }; + } + + function _adjustTimelineAnchorAvailabilityOffset(now, range) { + timelineAnchorAvailabilityOffset = now - range.end; + } + + function _adjustTimeBasedOnPeriodRanges(streams, time, isEndOfDvrWindow = false) { + try { + let i = 0; + let found = false; + let adjustedTime = NaN; + + while (!found && i < streams.length) { + const streamInfo = streams[i].getStreamInfo(); + + // We found a period which contains the target time. + if (streamInfo.start <= time && (!isFinite(streamInfo.duration) || (streamInfo.start + streamInfo.duration >= time))) { + adjustedTime = time; + found = true; + } + + // Adjust the time for the start of the DVR window. The current period starts after the target time. We use the starttime of this period as adjusted time + else if (!isEndOfDvrWindow && (streamInfo.start > time && (isNaN(adjustedTime) || streamInfo.start < adjustedTime))) { + adjustedTime = streamInfo.start; + } + + // Adjust the time for the end of the DVR window. The current period ends before the targettime. We use the end time of this period as the adjusted time + else if (isEndOfDvrWindow && ((streamInfo.start + streamInfo.duration) < time && (isNaN(adjustedTime) || (streamInfo.start + streamInfo.duration > adjustedTime)))) { + adjustedTime = streamInfo.start + streamInfo.duration; + } + + i += 1; + } + + return adjustedTime; + } catch (e) { + return time; + } + } + + function _calcRangeForTimeline(voRepresentation) { const adaptation = voRepresentation.adaptation.period.mpd.manifest.Period_asArray[voRepresentation.adaptation.period.index].AdaptationSet_asArray[voRepresentation.adaptation.index]; const representation = dashManifestModel.getRepresentationFor(voRepresentation.index, adaptation); const timeline = representation.SegmentTemplate.SegmentTimeline; const timescale = representation.SegmentTemplate.timescale; const segments = timeline.S_asArray; - const range = {start: 0, end: 0}; + const range = { start: 0, end: 0 }; let d = 0; let segment, repeat, @@ -188,57 +331,28 @@ function TimelineConverter() { if (segment.hasOwnProperty('r')) { repeat = segment.r; } - d += (segment.d / timescale) * (1 + repeat); + d += segment.d * (1 + repeat); } - range.end = range.start + d; + range.end = calcPresentationTimeFromMediaTime((segments[0].t + d) / timescale, voRepresentation); return range; } - function getPeriodEnd(voRepresentation, isDynamic) { - // Static Range Finder - const voPeriod = voRepresentation.adaptation.period; - if (!isDynamic) { - return voPeriod.start + voPeriod.duration; - } - - if (!isClientServerTimeSyncCompleted && voRepresentation.segmentAvailabilityRange) { - return voRepresentation.segmentAvailabilityRange; - } - - // Dynamic Range Finder - const d = voRepresentation.segmentDuration || (voRepresentation.segments && voRepresentation.segments.length ? voRepresentation.segments[voRepresentation.segments.length - 1].duration : 0); - const now = calcPresentationTimeFromWallTime(new Date(), voPeriod); - const periodEnd = voPeriod.start + voPeriod.duration; - - const endOffset = voRepresentation.availabilityTimeOffset !== undefined && - voRepresentation.availabilityTimeOffset < d ? d - voRepresentation.availabilityTimeOffset : d; - - return Math.min(now - endOffset, periodEnd); - } - function calcPeriodRelativeTimeFromMpdRelativeTime(representation, mpdRelativeTime) { const periodStartTime = representation.adaptation.period.start; return mpdRelativeTime - periodStartTime; } - /* - * We need to figure out if we want to timesync for segmentTimeine where useCalculatedLiveEdge = true - * seems we figure out client offset based on logic in liveEdgeFinder getLiveEdge timelineConverter.setClientTimeOffset(liveEdge - representationInfo.DVRWindow.end); - * FYI StreamController's onManifestUpdated entry point to timeSync - * */ function _onUpdateTimeSyncOffset(e) { - if (e.offset !== undefined) { + if (e.offset !== undefined && !isNaN(e.offset)) { setClientTimeOffset(e.offset / 1000); - isClientServerTimeSyncCompleted = true; } } function resetInitialSettings() { clientServerTimeShift = 0; - isClientServerTimeSyncCompleted = false; - expectedLiveEdge = NaN; + timelineAnchorAvailabilityOffset = 0; } function reset() { @@ -247,21 +361,19 @@ function TimelineConverter() { } instance = { - initialize: initialize, - getClientTimeOffset: getClientTimeOffset, - setClientTimeOffset: setClientTimeOffset, - getExpectedLiveEdge: getExpectedLiveEdge, - setExpectedLiveEdge: setExpectedLiveEdge, - calcAvailabilityStartTimeFromPresentationTime: calcAvailabilityStartTimeFromPresentationTime, - calcAvailabilityEndTimeFromPresentationTime: calcAvailabilityEndTimeFromPresentationTime, - calcPresentationTimeFromWallTime: calcPresentationTimeFromWallTime, - calcPresentationTimeFromMediaTime: calcPresentationTimeFromMediaTime, - calcPeriodRelativeTimeFromMpdRelativeTime: calcPeriodRelativeTimeFromMpdRelativeTime, - calcMediaTimeFromPresentationTime: calcMediaTimeFromPresentationTime, - calcSegmentAvailabilityRange: calcSegmentAvailabilityRange, - getPeriodEnd: getPeriodEnd, - calcWallTimeForSegment: calcWallTimeForSegment, - reset: reset + initialize, + getClientTimeOffset, + setClientTimeOffset, + getClientReferenceTime, + calcAvailabilityStartTimeFromPresentationTime, + calcAvailabilityEndTimeFromPresentationTime, + calcPresentationTimeFromWallTime, + calcPresentationTimeFromMediaTime, + calcPeriodRelativeTimeFromMpdRelativeTime, + calcMediaTimeFromPresentationTime, + calcWallTimeForSegment, + calcTimeShiftBufferWindow, + reset }; setup(); diff --git a/src/dash/utils/TimelineSegmentsGetter.js b/src/dash/utils/TimelineSegmentsGetter.js index 6df815066e..3f4c15aca7 100644 --- a/src/dash/utils/TimelineSegmentsGetter.js +++ b/src/dash/utils/TimelineSegmentsGetter.js @@ -32,33 +32,34 @@ import FactoryMaker from '../../core/FactoryMaker'; import Constants from '../../streaming/constants/Constants'; -import { getTimeBasedSegment } from './SegmentsUtils'; +import {getTimeBasedSegment} from './SegmentsUtils'; function TimelineSegmentsGetter(config, isDynamic) { config = config || {}; const timelineConverter = config.timelineConverter; + const dashMetrics = config.dashMetrics; let instance; function checkConfig() { - if (!timelineConverter || !timelineConverter.hasOwnProperty('calcMediaTimeFromPresentationTime') || - !timelineConverter.hasOwnProperty('calcSegmentAvailabilityRange')) { + if (!timelineConverter) { throw new Error(Constants.MISSING_CONFIG_ERROR); } } - function iterateSegments(representation, iterFunc) { - const base = representation.adaptation.period.mpd.manifest.Period_asArray[representation.adaptation.period.index]. - AdaptationSet_asArray[representation.adaptation.index].Representation_asArray[representation.index].SegmentTemplate || - representation.adaptation.period.mpd.manifest.Period_asArray[representation.adaptation.period.index]. - AdaptationSet_asArray[representation.adaptation.index].Representation_asArray[representation.index].SegmentList; + function getMediaFinishedInformation(representation) { + if (!representation) { + return 0; + } + + const base = representation.adaptation.period.mpd.manifest.Period_asArray[representation.adaptation.period.index].AdaptationSet_asArray[representation.adaptation.index].Representation_asArray[representation.index].SegmentTemplate || + representation.adaptation.period.mpd.manifest.Period_asArray[representation.adaptation.period.index].AdaptationSet_asArray[representation.adaptation.index].Representation_asArray[representation.index].SegmentList; const timeline = base.SegmentTimeline; - const list = base.SegmentURL_asArray; let time = 0; let scaledTime = 0; - let availabilityIdx = -1; + let availableSegments = 0; let fragments, frag, @@ -66,16 +67,14 @@ function TimelineSegmentsGetter(config, isDynamic) { len, j, repeat, - repeatEndTime, - nextFrag, fTimescale; fTimescale = representation.timescale; fragments = timeline.S_asArray; - let breakIterator = false; + len = fragments.length; - for (i = 0, len = fragments.length; i < len && !breakIterator; i++) { + for (i = 0; i < len; i++) { frag = fragments[i]; repeat = 0; if (frag.hasOwnProperty('r')) { @@ -91,41 +90,104 @@ function TimelineSegmentsGetter(config, isDynamic) { // This is a special case: "A negative value of the @r attribute of the S element indicates that the duration indicated in @d attribute repeats until the start of the next S element, the end of the Period or until the // next MPD update." if (repeat < 0) { - nextFrag = fragments[i + 1]; + const nextFrag = fragments[i + 1]; + repeat = _calculateRepeatCountForNegativeR(representation, nextFrag, frag, fTimescale, scaledTime); + } - if (nextFrag && nextFrag.hasOwnProperty('t')) { - repeatEndTime = nextFrag.t / fTimescale; - } else { - const availabilityEnd = representation.segmentAvailabilityRange ? representation.segmentAvailabilityRange.end : (timelineConverter.calcSegmentAvailabilityRange(representation, isDynamic).end); - repeatEndTime = timelineConverter.calcMediaTimeFromPresentationTime(availabilityEnd, representation); - representation.segmentDuration = frag.d / fTimescale; - } + for (j = 0; j <= repeat; j++) { + availableSegments++; + + time += frag.d; + scaledTime = time / fTimescale; + } + } + + // We need to account for the index of the segments starting at 0. We subtract 1 + return { numberOfSegments: availableSegments, mediaTimeOfLastSignaledSegment: scaledTime }; + } + + function iterateSegments(representation, iterFunc) { + const base = representation.adaptation.period.mpd.manifest.Period_asArray[representation.adaptation.period.index].AdaptationSet_asArray[representation.adaptation.index].Representation_asArray[representation.index].SegmentTemplate || + representation.adaptation.period.mpd.manifest.Period_asArray[representation.adaptation.period.index].AdaptationSet_asArray[representation.adaptation.index].Representation_asArray[representation.index].SegmentList; + const timeline = base.SegmentTimeline; + const list = base.SegmentURL_asArray; + + let time = 0; + let relativeIdx = -1; + + let fragments, + frag, + i, + len, + j, + repeat, + fTimescale; + + fTimescale = representation.timescale; + fragments = timeline.S_asArray; + + let breakIterator = false; + + for (i = 0, len = fragments.length; i < len && !breakIterator; i++) { + frag = fragments[i]; + repeat = 0; + if (frag.hasOwnProperty('r')) { + repeat = frag.r; + } - repeat = Math.ceil((repeatEndTime - scaledTime) / (frag.d / fTimescale)) - 1; + // For a repeated S element, t belongs only to the first segment + if (frag.hasOwnProperty('t')) { + time = frag.t; + } + + // This is a special case: "A negative value of the @r attribute of the S element indicates that the duration indicated in @d attribute repeats until the start of the next S element, the end of the Period or until the + // next MPD update." + if (repeat < 0) { + const nextFrag = fragments[i + 1]; + repeat = _calculateRepeatCountForNegativeR(representation, nextFrag, frag, fTimescale, time / fTimescale); } for (j = 0; j <= repeat && !breakIterator; j++) { - availabilityIdx++; + relativeIdx++; - breakIterator = iterFunc(time, scaledTime, base, list, frag, fTimescale, availabilityIdx, i); + breakIterator = iterFunc(time, base, list, frag, fTimescale, relativeIdx, i); if (breakIterator) { representation.segmentDuration = frag.d / fTimescale; - - // check if there is at least one more segment - if (j < repeat - 1 || i < len - 1) { - availabilityIdx++; - } } time += frag.d; - scaledTime = time / fTimescale; } } + } - representation.availableSegmentsNumber = availabilityIdx; + function _calculateRepeatCountForNegativeR(representation, nextFrag, frag, fTimescale, scaledTime) { + let repeatEndTime; + + if (nextFrag && nextFrag.hasOwnProperty('t')) { + repeatEndTime = nextFrag.t / fTimescale; + } else { + try { + let availabilityEnd = 0; + if (!isNaN(representation.adaptation.period.start) && !isNaN(representation.adaptation.period.duration) && isFinite(representation.adaptation.period.duration)) { + // use end of the Period + availabilityEnd = representation.adaptation.period.start + representation.adaptation.period.duration; + } else { + // use DVR window + const dvrWindow = dashMetrics.getCurrentDVRInfo(); + availabilityEnd = !isNaN(dvrWindow.end) ? dvrWindow.end : 0; + } + repeatEndTime = timelineConverter.calcMediaTimeFromPresentationTime(availabilityEnd, representation); + representation.segmentDuration = frag.d / fTimescale; + } catch (e) { + repeatEndTime = 0; + } + } + + return Math.max(Math.ceil((repeatEndTime - scaledTime) / (frag.d / fTimescale)) - 1, 0); } + function getSegmentByIndex(representation, index, lastSegmentTime) { checkConfig(); @@ -136,7 +198,7 @@ function TimelineSegmentsGetter(config, isDynamic) { let segment = null; let found = false; - iterateSegments(representation, function (time, scaledTime, base, list, frag, fTimescale, availabilityIdx, i) { + iterateSegments(representation, function (time, base, list, frag, fTimescale, relativeIdx, i) { if (found || lastSegmentTime < 0) { let media = base.media; let mediaRange = frag.mediaRange; @@ -155,12 +217,12 @@ function TimelineSegmentsGetter(config, isDynamic) { fTimescale, media, mediaRange, - availabilityIdx, + relativeIdx, frag.tManifest); return true; - } else if (scaledTime >= lastSegmentTime - frag.d * 0.5 / fTimescale) { // same logic, if deviation is - // 50% of segment duration, segment is found if scaledTime is greater than or equal to (startTime of previous segment - half of the previous segment duration) + } else if (time >= (lastSegmentTime * fTimescale) - (frag.d * 0.5)) { // same logic, if deviation is + // 50% of segment duration, segment is found if time is greater than or equal to (startTime of previous segment - half of the previous segment duration) found = true; } @@ -184,11 +246,12 @@ function TimelineSegmentsGetter(config, isDynamic) { let segment = null; const requiredMediaTime = timelineConverter.calcMediaTimeFromPresentationTime(requestedTime, representation); - iterateSegments(representation, function (time, scaledTime, base, list, frag, fTimescale, availabilityIdx, i) { + iterateSegments(representation, function (time, base, list, frag, fTimescale, relativeIdx, i) { // In some cases when requiredMediaTime = actual end time of the last segment // it is possible that this time a bit exceeds the declared end time of the last segment. // in this case we still need to include the last segment in the segment list. - if (requiredMediaTime < (scaledTime + (frag.d / fTimescale))) { + const scaledMediaTime = precisionRound(requiredMediaTime * fTimescale); + if (scaledMediaTime < (time + frag.d) && scaledMediaTime >= time) { let media = base.media; let mediaRange = frag.mediaRange; @@ -206,7 +269,7 @@ function TimelineSegmentsGetter(config, isDynamic) { fTimescale, media, mediaRange, - availabilityIdx, + relativeIdx, frag.tManifest); return true; @@ -218,10 +281,14 @@ function TimelineSegmentsGetter(config, isDynamic) { return segment; } + function precisionRound(number) { + return parseFloat(number.toPrecision(15)); + } instance = { - getSegmentByIndex: getSegmentByIndex, - getSegmentByTime: getSegmentByTime + getSegmentByIndex, + getSegmentByTime, + getMediaFinishedInformation }; return instance; diff --git a/src/dash/vo/ContentSteering.js b/src/dash/vo/ContentSteering.js new file mode 100644 index 0000000000..d06442a87f --- /dev/null +++ b/src/dash/vo/ContentSteering.js @@ -0,0 +1,44 @@ +/** + * The copyright in this software is being made available under the BSD License, + * included below. This software may be subject to other third party and contributor + * rights, including patent rights, and no such rights are granted under this license. + * + * Copyright (c) 2013, Dash Industry Forum. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * * Neither the name of Dash Industry Forum nor the names of its + * contributors may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +/** + * @class + * @ignore + */ +class ContentSteering { + constructor() { + this.defaultServiceLocation = null; + this.queryBeforeStart = false; + this.proxyServerUrl = null; + this.serverUrl = null; + } +} + +export default ContentSteering; diff --git a/src/streaming/utils/LiveEdgeFinder.js b/src/dash/vo/ContentSteeringRequest.js similarity index 65% rename from src/streaming/utils/LiveEdgeFinder.js rename to src/dash/vo/ContentSteeringRequest.js index 5994d0370a..29a8b5f3ec 100644 --- a/src/streaming/utils/LiveEdgeFinder.js +++ b/src/dash/vo/ContentSteeringRequest.js @@ -28,43 +28,19 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ -import FactoryMaker from '../../core/FactoryMaker'; -import Constants from '../constants/Constants'; - /** - * @param {Object} config - * @returns {{initialize: initialize, getLiveEdge: getLiveEdge, reset: reset}|*} - * @constructor + * @class * @ignore */ -function LiveEdgeFinder(config) { - - config = config || {}; - let instance; - let timelineConverter = config.timelineConverter; - function checkConfig() { - if (!timelineConverter || !timelineConverter.hasOwnProperty('getExpectedLiveEdge')) { - throw new Error(Constants.MISSING_CONFIG_ERROR); - } - } +import {HTTPRequest} from '../../streaming/vo/metrics/HTTPRequest'; - function getLiveEdge(representationInfo) { - checkConfig(); - return representationInfo.DVRWindow ? representationInfo.DVRWindow.end : 0; +class ContentSteeringRequest { + constructor(url) { + this.url = url || null; + this.type = HTTPRequest.CONTENT_STEERING_TYPE; + this.responseType = 'json'; } - - function reset() { - timelineConverter = null; - } - - instance = { - getLiveEdge: getLiveEdge, - reset: reset - }; - - return instance; } -LiveEdgeFinder.__dashjs_factory_name = 'LiveEdgeFinder'; -export default FactoryMaker.getClassFactory(LiveEdgeFinder); +export default ContentSteeringRequest; diff --git a/src/dash/vo/ContentSteeringResponse.js b/src/dash/vo/ContentSteeringResponse.js new file mode 100644 index 0000000000..e20c79db99 --- /dev/null +++ b/src/dash/vo/ContentSteeringResponse.js @@ -0,0 +1,44 @@ +/** + * The copyright in this software is being made available under the BSD License, + * included below. This software may be subject to other third party and contributor + * rights, including patent rights, and no such rights are granted under this license. + * + * Copyright (c) 2013, Dash Industry Forum. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * * Neither the name of Dash Industry Forum nor the names of its + * contributors may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +/** + * @class + * @ignore + */ +class ContentSteeringResponse { + constructor() { + this.version = null; + this.ttl = 300; + this.reloadUri = null; + this.serviceLocationPriority = []; + } +} + +export default ContentSteeringResponse; diff --git a/src/dash/vo/ManifestInfo.js b/src/dash/vo/ManifestInfo.js index cb02fc7480..79a7a778dc 100644 --- a/src/dash/vo/ManifestInfo.js +++ b/src/dash/vo/ManifestInfo.js @@ -34,7 +34,7 @@ */ class ManifestInfo { constructor() { - this.DVRWindowSize = NaN; + this.dvrWindowSize = NaN; this.loadedTime = null; this.availableFrom = null; this.minBufferTime = NaN; @@ -44,4 +44,4 @@ class ManifestInfo { } } -export default ManifestInfo; \ No newline at end of file +export default ManifestInfo; diff --git a/src/dash/vo/MediaInfo.js b/src/dash/vo/MediaInfo.js index 1a543e9376..59cae50fda 100644 --- a/src/dash/vo/MediaInfo.js +++ b/src/dash/vo/MediaInfo.js @@ -39,6 +39,7 @@ class MediaInfo { this.type = null; this.streamInfo = null; this.representationCount = 0; + this.labels = null; this.lang = null; this.viewpoint = null; this.accessibility = null; @@ -47,9 +48,11 @@ class MediaInfo { this.codec = null; this.mimeType = null; this.contentProtection = null; - this.isText = false; this.KID = null; this.bitrateList = null; + this.isFragmented = null; + this.isEmbedded = null; + this.selectionPriority = 1; } } diff --git a/src/dash/vo/Period.js b/src/dash/vo/Period.js index 10094b7b02..f5c114c3ff 100644 --- a/src/dash/vo/Period.js +++ b/src/dash/vo/Period.js @@ -39,9 +39,10 @@ class Period { this.duration = NaN; this.start = NaN; this.mpd = null; + this.nextPeriodId = null; } } Period.DEFAULT_ID = 'defaultId'; -export default Period; \ No newline at end of file +export default Period; diff --git a/src/dash/vo/ProducerReferenceTime.js b/src/dash/vo/ProducerReferenceTime.js new file mode 100644 index 0000000000..5460a157ff --- /dev/null +++ b/src/dash/vo/ProducerReferenceTime.js @@ -0,0 +1,47 @@ +/** + * The copyright in this software is being made available under the BSD License, + * included below. This software may be subject to other third party and contributor + * rights, including patent rights, and no such rights are granted under this license. + * + * Copyright (c) 2013, Dash Industry Forum. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * * Neither the name of Dash Industry Forum nor the names of its + * contributors may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +/** + * @class + * @ignore + */ +class ProducerReferenceTime { + constructor() { + this.id = null; + this.inband = false; + this.type = 'encoder'; + this.applicationScheme = null; + this.wallClockTime = null; + this.presentationTime = NaN; + this.UTCTiming = null; + } +} + +export default ProducerReferenceTime; diff --git a/src/dash/vo/Representation.js b/src/dash/vo/Representation.js index d56210ad4c..6c820edd5c 100644 --- a/src/dash/vo/Representation.js +++ b/src/dash/vo/Representation.js @@ -43,6 +43,7 @@ class Representation { this.segmentInfoType = null; this.initialization = null; this.codecs = null; + this.mimeType = null; this.codecPrivateData = null; this.segmentDuration = NaN; this.timescale = 1; @@ -52,8 +53,8 @@ class Representation { this.presentationTimeOffset = 0; // Set the source buffer timeOffset to this this.MSETimeOffset = NaN; - this.segmentAvailabilityRange = null; - this.availableSegmentsNumber = 0; + // The information we need in the DashHandler to determine whether the last segment has been loaded + this.mediaFinishedInformation = { numberOfSegments: 0, mediaTimeOfLastSignaledSegment: NaN }; this.bandwidth = NaN; this.width = NaN; this.height = NaN; diff --git a/src/dash/vo/RepresentationInfo.js b/src/dash/vo/RepresentationInfo.js index 0711217c04..9d55e8c460 100644 --- a/src/dash/vo/RepresentationInfo.js +++ b/src/dash/vo/RepresentationInfo.js @@ -36,7 +36,6 @@ class RepresentationInfo { constructor() { this.id = null; this.quality = null; - this.DVRWindow = null; this.fragmentDuration = null; this.mediaInfo = null; this.MSETimeOffset = null; diff --git a/src/dash/vo/Segment.js b/src/dash/vo/Segment.js index 3b9b79c985..33d6559ad7 100644 --- a/src/dash/vo/Segment.js +++ b/src/dash/vo/Segment.js @@ -35,6 +35,7 @@ class Segment { constructor() { this.indexRange = null; + // The index of the segment in the list of segments. We start at 0 this.index = null; this.mediaRange = null; this.media = null; @@ -52,8 +53,6 @@ class Segment { this.availabilityStartTime = NaN; // Ignore and discard this segment after this.availabilityEndTime = NaN; - // The index of the segment inside the availability window - this.availabilityIdx = NaN; // For dynamic mpd's, this is the wall clock time that the video // element currentTime should be presentationStartTime this.wallStartTime = NaN; @@ -61,4 +60,4 @@ class Segment { } } -export default Segment; \ No newline at end of file +export default Segment; diff --git a/src/mss/MssFragmentMoofProcessor.js b/src/mss/MssFragmentMoofProcessor.js index 20c83d1e8f..61dfd20942 100644 --- a/src/mss/MssFragmentMoofProcessor.js +++ b/src/mss/MssFragmentMoofProcessor.js @@ -186,7 +186,6 @@ function MssFragmentMoofProcessor(config) { updateDVR(type, range, streamProcessor.getStreamInfo().manifestInfo); } - representationController.updateRepresentation(representation, true); } function updateDVR(type, range, manifestInfo) { diff --git a/src/mss/MssFragmentMoovProcessor.js b/src/mss/MssFragmentMoovProcessor.js index 130f18f3bb..429c5341a2 100644 --- a/src/mss/MssFragmentMoovProcessor.js +++ b/src/mss/MssFragmentMoovProcessor.js @@ -28,7 +28,7 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ - import MssErrors from './errors/MssErrors'; +import MssErrors from './errors/MssErrors'; /** * @module MssFragmentMoovProcessor diff --git a/src/mss/MssHandler.js b/src/mss/MssHandler.js index e0db20ae46..8ff21f9e0e 100644 --- a/src/mss/MssHandler.js +++ b/src/mss/MssHandler.js @@ -36,7 +36,6 @@ import MssFragmentProcessor from './MssFragmentProcessor'; import MssParser from './parser/MssParser'; import MssErrors from './errors/MssErrors'; import DashJSError from '../streaming/vo/DashJSError'; -import InitCache from '../streaming/utils/InitCache'; import {HTTPRequest} from '../streaming/vo/metrics/HTTPRequest'; function MssHandler(config) { @@ -64,12 +63,10 @@ function MssHandler(config) { }); let mssParser, fragmentInfoControllers, - initCache, instance; function setup() { fragmentInfoControllers = []; - initCache = InitCache(context).getInstance(); } function getStreamProcessor(type) { @@ -103,12 +100,12 @@ function MssHandler(config) { function startFragmentInfoControllers() { - // Create MssFragmentInfoControllers for each StreamProcessor of active stream (only for audio, video or fragmentedText) + // Create MssFragmentInfoControllers for each StreamProcessor of active stream (only for audio, video or text) let processors = streamController.getActiveStreamProcessors(); processors.forEach(function (processor) { if (processor.getType() === constants.VIDEO || processor.getType() === constants.AUDIO || - processor.getType() === constants.FRAGMENTED_TEXT) { + processor.getType() === constants.TEXT) { let fragmentInfoController = getFragmentInfoController(processor.getType()); if (!fragmentInfoController) { @@ -187,7 +184,7 @@ function MssHandler(config) { // Start MssFragmentInfoControllers in case of start-over streams let manifestInfo = e.request.mediaInfo.streamInfo.manifestInfo; - if (!manifestInfo.isDynamic && manifestInfo.DVRWindowSize !== Infinity) { + if (!manifestInfo.isDynamic && manifestInfo.dvrWindowSize !== Infinity) { startFragmentInfoControllers(); } } @@ -198,7 +195,7 @@ function MssHandler(config) { } } - function onPlaybackSeekAsked() { + function onPlaybackSeeking() { if (playbackController.getIsDynamic() && playbackController.getTime() !== 0) { startFragmentInfoControllers(); } @@ -215,7 +212,7 @@ function MssHandler(config) { function registerEvents() { eventBus.on(events.INIT_FRAGMENT_NEEDED, onInitFragmentNeeded, instance, { priority: dashjs.FactoryMaker.getSingletonFactoryByName(eventBus.getClassName()).EVENT_PRIORITY_HIGH }); /* jshint ignore:line */ eventBus.on(events.PLAYBACK_PAUSED, onPlaybackPaused, instance, { priority: dashjs.FactoryMaker.getSingletonFactoryByName(eventBus.getClassName()).EVENT_PRIORITY_HIGH }); /* jshint ignore:line */ - eventBus.on(events.PLAYBACK_SEEK_ASKED, onPlaybackSeekAsked, instance, { priority: dashjs.FactoryMaker.getSingletonFactoryByName(eventBus.getClassName()).EVENT_PRIORITY_HIGH }); /* jshint ignore:line */ + eventBus.on(events.PLAYBACK_SEEKING, onPlaybackSeeking, instance, { priority: dashjs.FactoryMaker.getSingletonFactoryByName(eventBus.getClassName()).EVENT_PRIORITY_HIGH }); /* jshint ignore:line */ eventBus.on(events.FRAGMENT_LOADING_COMPLETED, onSegmentMediaLoaded, instance, { priority: dashjs.FactoryMaker.getSingletonFactoryByName(eventBus.getClassName()).EVENT_PRIORITY_HIGH }); /* jshint ignore:line */ eventBus.on(events.TTML_TO_PARSE, onTTMLPreProcess, instance); } @@ -228,7 +225,7 @@ function MssHandler(config) { eventBus.off(events.INIT_FRAGMENT_NEEDED, onInitFragmentNeeded, this); eventBus.off(events.PLAYBACK_PAUSED, onPlaybackPaused, this); - eventBus.off(events.PLAYBACK_SEEK_ASKED, onPlaybackSeekAsked, this); + eventBus.off(events.PLAYBACK_SEEKING, onPlaybackSeeking, this); eventBus.off(events.FRAGMENT_LOADING_COMPLETED, onSegmentMediaLoaded, this); eventBus.off(events.TTML_TO_PARSE, onTTMLPreProcess, this); diff --git a/src/mss/errors/MssErrors.js b/src/mss/errors/MssErrors.js index 830c9d790d..b619125b4a 100644 --- a/src/mss/errors/MssErrors.js +++ b/src/mss/errors/MssErrors.js @@ -29,12 +29,13 @@ * POSSIBILITY OF SUCH DAMAGE. */ import ErrorsBase from '../../core/errors/ErrorsBase'; + /** * @class * */ class MssErrors extends ErrorsBase { - constructor () { + constructor() { super(); /** * Error code returned when no tfrf box is detected in MSS live stream @@ -52,4 +53,4 @@ class MssErrors extends ErrorsBase { } let mssErrors = new MssErrors(); -export default mssErrors; \ No newline at end of file +export default mssErrors; diff --git a/src/mss/parser/MssParser.js b/src/mss/parser/MssParser.js index 72a5be2ef5..c6f34e5c8d 100644 --- a/src/mss/parser/MssParser.js +++ b/src/mss/parser/MssParser.js @@ -43,11 +43,10 @@ function MssParser(config) { const debug = config.debug; const constants = config.constants; const manifestModel = config.manifestModel; - const mediaPlayerModel = config.mediaPlayerModel; const settings = config.settings; const DEFAULT_TIME_SCALE = 10000000.0; - const SUPPORTED_CODECS = ['AAC', 'AACL', 'AVC1', 'H264', 'TTML', 'DFXP']; + const SUPPORTED_CODECS = ['AAC', 'AACL', 'AACH', 'AACP', 'AVC1', 'H264', 'TTML', 'DFXP']; // MPEG-DASH Role and accessibility mapping for text tracks according to ETSI TS 103 285 v1.1.1 (section 7.1.2) const ROLE = { 'CAPT': 'main', @@ -121,7 +120,6 @@ function MssParser(config) { let segmentTemplate; let qualityLevels, representation, - segments, i, index; @@ -193,8 +191,6 @@ function MssParser(config) { // Set SegmentTemplate adaptationSet.SegmentTemplate = segmentTemplate; - segments = segmentTemplate.SegmentTimeline.S_asArray; - return adaptationSet; } @@ -353,6 +349,9 @@ function MssParser(config) { segmentTemplate.SegmentTimeline = mapSegmentTimeline(streamIndex, segmentTemplate.timescale); + // Patch: set availabilityTimeOffset to Infinity since segments are available as long as they are present in timeline + segmentTemplate.availabilityTimeOffset = 'INF'; + return segmentTemplate; } @@ -363,7 +362,7 @@ function MssParser(config) { let segment, prevSegment, tManifest, - i,j,r; + i, j, r; let duration = 0; for (i = 0; i < chunks.length; i++) { @@ -426,7 +425,7 @@ function MssParser(config) { segment.t = prevSegment.t + prevSegment.d; segment.d = prevSegment.d; if (prevSegment.tManifest) { - segment.tManifest = BigInt(prevSegment.tManifest).add(BigInt(prevSegment.d)).toString(); + segment.tManifest = BigInt(prevSegment.tManifest).add(BigInt(prevSegment.d)).toString(); } duration += segment.d; segments.push(segment); @@ -485,11 +484,11 @@ function MssParser(config) { // Parse PlayReady header // Length - 32 bits (LE format) - length = (prHeader[i + 3] << 24) + (prHeader[i + 2] << 16) + (prHeader[i + 1] << 8) + prHeader[i]; + length = (prHeader[i + 3] << 24) + (prHeader[i + 2] << 16) + (prHeader[i + 1] << 8) + prHeader[i]; // eslint-disable-line i += 4; // Record count - 16 bits (LE format) - recordCount = (prHeader[i + 1] << 8) + prHeader[i]; + recordCount = (prHeader[i + 1] << 8) + prHeader[i]; // eslint-disable-line i += 2; // Parse records @@ -571,7 +570,7 @@ function MssParser(config) { i += 8; // Set SystemID ('edef8ba9-79d6-4ace-a3c8-27dcd51d21ed') - pssh.set([0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed], i); + pssh.set([0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed], i); i += 16; // Set data length value @@ -613,7 +612,7 @@ function MssParser(config) { manifest.protocol = 'MSS'; manifest.profiles = 'urn:mpeg:dash:profile:isoff-live:2011'; manifest.type = getAttributeAsBoolean(smoothStreamingMedia, 'IsLive') ? 'dynamic' : 'static'; - timescale = smoothStreamingMedia.getAttribute('TimeScale'); + timescale = smoothStreamingMedia.getAttribute('TimeScale'); manifest.timescale = timescale ? parseFloat(timescale) : DEFAULT_TIME_SCALE; let dvrWindowLength = parseFloat(smoothStreamingMedia.getAttribute('DVRWindowLength')); // If the DVRWindowLength field is omitted for a live presentation or set to 0, the DVR window is effectively infinite @@ -647,6 +646,7 @@ function MssParser(config) { manifest.refreshManifestOnSwitchTrack = true; // Refresh manifest when switching tracks manifest.doNotUpdateDVRWindowOnBufferUpdated = true; // DVRWindow is update by MssFragmentMoofPocessor based on tfrf boxes manifest.ignorePostponeTimePeriod = true; // Never update manifest + manifest.availabilityStartTime = new Date(null); // Returns 1970 } // Map period node to manifest root node @@ -705,7 +705,7 @@ function MssParser(config) { // Set minBufferTime to one segment duration manifest.minBufferTime = segmentDuration; - if (manifest.type === 'dynamic' ) { + if (manifest.type === 'dynamic') { // Match timeShiftBufferDepth to video segment timeline duration if (manifest.timeShiftBufferDepth > 0 && manifest.timeShiftBufferDepth !== Infinity && @@ -724,9 +724,9 @@ function MssParser(config) { // 2- adapt live delay and then buffers length in case timeShiftBufferDepth is too small compared to target live delay (see PlaybackController.computeLiveDelay()) // 3- Set retry attempts and intervals for FragmentInfo requests if (manifest.type === 'dynamic') { - let targetLiveDelay = mediaPlayerModel.getLiveDelay(); + let targetLiveDelay = settings.get().streaming.delay.liveDelay; if (!targetLiveDelay) { - const liveDelayFragmentCount = settings.get().streaming.liveDelayFragmentCount !== null && !isNaN(settings.get().streaming.liveDelayFragmentCount) ? settings.get().streaming.liveDelayFragmentCount : 4; + const liveDelayFragmentCount = settings.get().streaming.delay.liveDelayFragmentCount !== null && !isNaN(settings.get().streaming.delay.liveDelayFragmentCount) ? settings.get().streaming.delay.liveDelayFragmentCount : 4; targetLiveDelay = segmentDuration * liveDelayFragmentCount; } let targetDelayCapping = Math.max(manifest.timeShiftBufferDepth - 10/*END_OF_PLAYLIST_PADDING*/, manifest.timeShiftBufferDepth / 2); @@ -737,21 +737,33 @@ function MssParser(config) { // Store initial buffer settings initialBufferSettings = { 'streaming': { - 'calcSegmentAvailabilityRangeFromTimeline': settings.get().streaming.calcSegmentAvailabilityRangeFromTimeline, - 'liveDelay': settings.get().streaming.liveDelay, - 'stableBufferTime': settings.get().streaming.stableBufferTime, - 'bufferTimeAtTopQuality': settings.get().streaming.bufferTimeAtTopQuality, - 'bufferTimeAtTopQualityLongForm': settings.get().streaming.bufferTimeAtTopQualityLongForm + 'buffer': { + 'stableBufferTime': settings.get().streaming.buffer.stableBufferTime, + 'bufferTimeAtTopQuality': settings.get().streaming.buffer.bufferTimeAtTopQuality, + 'bufferTimeAtTopQualityLongForm': settings.get().streaming.buffer.bufferTimeAtTopQualityLongForm + }, + 'timeShiftBuffer': { + calcFromSegmentTimeline: settings.get().streaming.timeShiftBuffer.calcFromSegmentTimeline + }, + 'delay': { + 'liveDelay': settings.get().streaming.delay.liveDelay + } } }; settings.update({ 'streaming': { - 'calcSegmentAvailabilityRangeFromTimeline': true, - 'liveDelay': liveDelay, - 'stableBufferTime': bufferTime, - 'bufferTimeAtTopQuality': bufferTime, - 'bufferTimeAtTopQualityLongForm': bufferTime + 'buffer': { + 'stableBufferTime': bufferTime, + 'bufferTimeAtTopQuality': bufferTime, + 'bufferTimeAtTopQualityLongForm': bufferTime + }, + 'timeShiftBuffer': { + calcFromSegmentTimeline: true + }, + 'delay': { + 'liveDelay': liveDelay + } } }); } diff --git a/src/offline/OfflineDownload.js b/src/offline/OfflineDownload.js index b9ea24103b..99defbb441 100644 --- a/src/offline/OfflineDownload.js +++ b/src/offline/OfflineDownload.js @@ -55,6 +55,7 @@ function OfflineDownload(config) { const debug = config.debug; const manifestUpdater = config.manifestUpdater; const baseURLController = config.baseURLController; + const segmentBaseController = config.segmentBaseController; const constants = config.constants; const dashConstants = config.dashConstants; const urlUtils = config.urlUtils; @@ -90,15 +91,15 @@ function OfflineDownload(config) { return manifestId; } - function getOfflineUrl () { + function getOfflineUrl() { return _offlineURL; } - function getManifestUrl () { + function getManifestUrl() { return _manifestURL; } - function getStatus () { + function getStatus() { return _status; } @@ -176,7 +177,10 @@ function OfflineDownload(config) { if (!e.error && manifestId !== null) { _status = OfflineConstants.OFFLINE_STATUS_STARTED; offlineStoreController.setDownloadingStatus(manifestId, _status).then(function () { - eventBus.trigger(events.OFFLINE_RECORD_STARTED, { id: manifestId, message: 'Downloading started for this stream !' }); + eventBus.trigger(events.OFFLINE_RECORD_STARTED, { + id: manifestId, + message: 'Downloading started for this stream !' + }); }); } else { _status = OfflineConstants.OFFLINE_STATUS_ERROR; @@ -233,10 +237,13 @@ function OfflineDownload(config) { if (!e.error && manifestId !== null) { _status = OfflineConstants.OFFLINE_STATUS_FINISHED; offlineStoreController.setDownloadingStatus(manifestId, _status) - .then(function () { - eventBus.trigger(events.OFFLINE_RECORD_FINISHED, { id: manifestId, message: 'Downloading has been successfully completed for this stream !' }); - resetDownload(); - }); + .then(function () { + eventBus.trigger(events.OFFLINE_RECORD_FINISHED, { + id: manifestId, + message: 'Downloading has been successfully completed for this stream !' + }); + resetDownload(); + }); } else { _status = OfflineConstants.OFFLINE_STATUS_ERROR; errHandler.error({ @@ -262,15 +269,15 @@ function OfflineDownload(config) { _indexDBManifestParser.parse(_xmlManifest, _representationsToUpdate).then(function (parsedManifest) { if (parsedManifest !== null && manifestId !== null) { offlineStoreController.getManifestById(manifestId) - .then((item) => { - item.manifest = parsedManifest; - return updateOfflineManifest(item); - }) - .then( function () { - for (let i = 0, ln = _streams.length; i < ln; i++) { - _streams[i].startOfflineStreamProcessors(); - } - }); + .then((item) => { + item.manifest = parsedManifest; + return updateOfflineManifest(item); + }) + .then(function () { + for (let i = 0, ln = _streams.length; i < ln; i++) { + _streams[i].startOfflineStreamProcessors(); + } + }); } else { throw 'falling parsing offline manifest'; } @@ -321,6 +328,7 @@ function OfflineDownload(config) { baseURLController: baseURLController, timelineConverter: timelineConverter, adapter: adapter, + segmentBaseController: segmentBaseController, offlineStoreController: offlineStoreController }); _streams.push(stream); @@ -440,7 +448,6 @@ function OfflineDownload(config) { rep[constants.VIDEO] = []; rep[constants.AUDIO] = []; rep[constants.TEXT] = []; - rep[constants.FRAGMENTED_TEXT] = []; // selectedRepresentations.video.forEach(item => { // ret[constants.VIDEO].push(item.id); @@ -465,15 +472,15 @@ function OfflineDownload(config) { let rep = getSelectedRepresentations(mediaInfos); offlineStoreController.saveSelectedRepresentations(manifestId, rep) - .then(() => { - return createFragmentStore(manifestId); - }) - .then(() => { - return generateOfflineManifest(rep); - }) - .then(function () { - initializeAllMediasInfoList(rep); - }); + .then(() => { + return createFragmentStore(manifestId); + }) + .then(() => { + return generateOfflineManifest(rep); + }) + .then(function () { + initializeAllMediasInfoList(rep); + }); } catch (err) { _status = OfflineConstants.OFFLINE_STATUS_ERROR; errHandler.error({ @@ -506,12 +513,12 @@ function OfflineDownload(config) { return _indexDBManifestParser.parse(_xmlManifest).then(function (parsedManifest) { if (parsedManifest !== null) { return offlineStoreController.getManifestById(manifestId) - .then((item) => { - item.originalURL = _manifest.url; - item.originalManifest = _xmlManifest; - item.manifest = parsedManifest; - return updateOfflineManifest(item); - }); + .then((item) => { + item.originalURL = _manifest.url; + item.originalManifest = _xmlManifest; + item.manifest = parsedManifest; + return updateOfflineManifest(item); + }); } else { return Promise.reject('falling parsing offline manifest'); } @@ -571,20 +578,21 @@ function OfflineDownload(config) { let selectedRepresentations; offlineStoreController.getManifestById(manifestId) - .then((item) => { - let parser = DashParser(context).create({debug: debug}); - _manifest = parser.parse(item.originalManifest); + .then((item) => { + let parser = DashParser(context).create({ debug: debug }); + _manifest = parser.parse(item.originalManifest); - composeStreams(_manifest); + composeStreams(_manifest); - selectedRepresentations = item.selected; + selectedRepresentations = item.selected; - eventBus.trigger(events.STREAMS_COMPOSED); + eventBus.trigger(events.STREAMS_COMPOSED); - return createFragmentStore(manifestId); - }). then(() => { - initializeAllMediasInfoList(selectedRepresentations); - }); + return createFragmentStore(manifestId); + }) + .then(() => { + initializeAllMediasInfoList(selectedRepresentations); + }); } /** @@ -612,8 +620,8 @@ function OfflineDownload(config) { } function onError(e) { - if ( e.error.code === OfflineErrors.INDEXEDDB_QUOTA_EXCEED_ERROR || - e.error.code === OfflineErrors.INDEXEDDB_INVALID_STATE_ERROR ) { + if (e.error.code === OfflineErrors.INDEXEDDB_QUOTA_EXCEED_ERROR || + e.error.code === OfflineErrors.INDEXEDDB_INVALID_STATE_ERROR) { stopDownload(); } } diff --git a/src/offline/OfflineStream.js b/src/offline/OfflineStream.js index e86b684310..4c857baa2a 100644 --- a/src/offline/OfflineStream.js +++ b/src/offline/OfflineStream.js @@ -58,6 +58,7 @@ function OfflineStream(config) { const dashMetrics = config.dashMetrics; const baseURLController = config.baseURLController; const timelineConverter = config.timelineConverter; + const segmentBaseController = config.segmentBaseController; const offlineStoreController = config.offlineStoreController; const manifestId = config.id; const startedCb = config.callbacks && config.callbacks.started; @@ -110,7 +111,6 @@ function OfflineStream(config) { function getMediaInfos() { let mediaInfos = adapter.getAllMediaInfoForType(streamInfo, constants.VIDEO); mediaInfos = mediaInfos.concat(adapter.getAllMediaInfoForType(streamInfo, constants.AUDIO)); - mediaInfos = mediaInfos.concat(adapter.getAllMediaInfoForType(streamInfo, constants.FRAGMENTED_TEXT)); mediaInfos = mediaInfos.concat(adapter.getAllMediaInfoForType(streamInfo, constants.TEXT)); // mediaInfos = mediaInfos.concat(adapter.getAllMediaInfoForType(streamInfo, constants.MUXED)); @@ -138,9 +138,7 @@ function OfflineStream(config) { function initializeMedia(streamInfo) { createOfflineStreamProcessorFor(constants.VIDEO,streamInfo); createOfflineStreamProcessorFor(constants.AUDIO,streamInfo); - createOfflineStreamProcessorFor(constants.FRAGMENTED_TEXT,streamInfo); createOfflineStreamProcessorFor(constants.TEXT,streamInfo); - createOfflineStreamProcessorFor(constants.MUXED,streamInfo); createOfflineStreamProcessorFor(constants.IMAGE,streamInfo); } @@ -196,6 +194,7 @@ function OfflineStream(config) { baseURLController: baseURLController, timelineConverter: timelineConverter, offlineStoreController: offlineStoreController, + segmentBaseController: segmentBaseController, callbacks: { completed: onStreamCompleted, progression: onStreamProgression diff --git a/src/offline/OfflineStreamProcessor.js b/src/offline/OfflineStreamProcessor.js index b70187c006..afe0ba63e9 100644 --- a/src/offline/OfflineStreamProcessor.js +++ b/src/offline/OfflineStreamProcessor.js @@ -34,7 +34,7 @@ import FragmentModel from '../streaming/models/FragmentModel'; import FragmentLoader from '../streaming/FragmentLoader'; import URLUtils from '../streaming/utils/URLUtils'; import RequestModifier from '../streaming/utils/RequestModifier'; - +import SegmentsController from '../dash/controllers/SegmentsController'; function OfflineStreamProcessor(config) { @@ -72,12 +72,23 @@ function OfflineStreamProcessor(config) { updating, downloadedSegments, isInitialized, + segmentsController, isStopped; function setup() { resetInitialSettings(); logger = debug.getLogger(instance); + segmentsController = SegmentsController(context).create({ + events, + eventBus, + streamInfo, + timelineConverter, + dashConstants, + segmentBaseController: config.segmentBaseController, + type + }); + indexHandler = DashHandler(context).create({ streamInfo: streamInfo, type: type, @@ -94,6 +105,7 @@ function OfflineStreamProcessor(config) { requestModifier: RequestModifier(context).getInstance(), dashConstants: dashConstants, constants: constants, + segmentsController: segmentsController, urlUtils: URLUtils(context).getInstance() }); @@ -107,7 +119,8 @@ function OfflineStreamProcessor(config) { dashConstants: dashConstants, events: events, eventBus: eventBus, - errors: errors + errors: errors, + segmentsController: segmentsController }); fragmentModel = FragmentModel(context).create({ @@ -131,7 +144,7 @@ function OfflineStreamProcessor(config) { events: events }); - eventBus.on(events.STREAM_COMPLETED, onStreamCompleted, instance); + eventBus.on(events.STREAM_REQUESTING_COMPLETED, onStreamRequestingCompleted, instance); eventBus.on(events.FRAGMENT_LOADING_COMPLETED, onFragmentLoadingCompleted, instance); } @@ -155,15 +168,15 @@ function OfflineStreamProcessor(config) { let suffix = isInit ? 'init' : e.request.index; let fragmentName = e.request.representationId + '_' + suffix; offlineStoreController.storeFragment(manifestId, fragmentName, e.response) - .then(() => { - if (!isInit) { - // store current index and downloadedSegments number - offlineStoreController.setRepresentationCurrentState(manifestId, e.request.representationId, { - index: e.request.index, - downloaded: downloadedSegments - } ); - } - }); + .then(() => { + if (!isInit) { + // store current index and downloadedSegments number + offlineStoreController.setRepresentationCurrentState(manifestId, e.request.representationId, { + index: e.request.index, + downloaded: downloadedSegments + }); + } + }); } if (e.error && e.request.serviceLocation && !isStopped) { @@ -174,7 +187,7 @@ function OfflineStreamProcessor(config) { } } - function onStreamCompleted(e) { + function onStreamRequestingCompleted(e) { if (e.fragmentModel !== fragmentModel) { return; } @@ -183,7 +196,7 @@ function OfflineStreamProcessor(config) { completedCb(); } - function getRepresentationController () { + function getRepresentationController() { return representationController; } @@ -212,7 +225,7 @@ function OfflineStreamProcessor(config) { /** * Execute init request for the represenation * @memberof OfflineStreamProcessor# - */ + */ function getInitRequest() { if (!representationController.getCurrentRepresentation()) { return null; @@ -224,7 +237,7 @@ function OfflineStreamProcessor(config) { /** * Get next request * @memberof OfflineStreamProcessor# - */ + */ function getNextRequest() { return indexHandler.getNextSegmentRequest(getMediaInfo(), representationController.getCurrentRepresentation()); } @@ -232,7 +245,7 @@ function OfflineStreamProcessor(config) { /** * Start download * @memberof OfflineStreamProcessor# - */ + */ function start() { if (representationController) { if (!representationController.getCurrentRepresentation()) { @@ -241,23 +254,24 @@ function OfflineStreamProcessor(config) { isStopped = false; offlineStoreController.getRepresentationCurrentState(manifestId, representationController.getCurrentRepresentation().id) - .then((state) => { - if (state) { - indexHandler.setCurrentIndex(state.index); - downloadedSegments = state.downloaded; - } - download(); - }).catch(() => { - // start from beginining - download(); - }); + .then((state) => { + if (state) { + indexHandler.setCurrentIndex(state.index); + downloadedSegments = state.downloaded; + } + download(); + }) + .catch(() => { + // start from beginining + download(); + }); } } /** * Performs download of fragment according to type * @memberof OfflineStreamProcessor# - */ + */ function download() { if (isStopped) { return; @@ -299,12 +313,12 @@ function OfflineStreamProcessor(config) { return representation.id === bitrate.id; }); - if (type !== constants.VIDEO && type !== constants.AUDIO && type !== constants.TEXT && type !== constants.FRAGMENTED_TEXT) { + if (type !== constants.VIDEO && type !== constants.AUDIO && type !== constants.TEXT) { updating = false; return; } - representationController.updateData(null, voRepresentations, type, quality); + representationController.updateData(null, voRepresentations, type, mediaInfo.isFragmented, quality); } function isUpdating() { @@ -320,10 +334,10 @@ function OfflineStreamProcessor(config) { } function getAvailableSegmentsNumber() { - return representationController.getCurrentRepresentation().availableSegmentsNumber + 1; // do not forget init segment + return representationController.getCurrentRepresentation().numberOfSegments + 1; // do not forget init segment } - function updateProgression () { + function updateProgression() { if (progressCb) { progressCb(instance, downloadedSegments, getAvailableSegmentsNumber()); } @@ -338,12 +352,12 @@ function OfflineStreamProcessor(config) { /** * Reset * @memberof OfflineStreamProcessor# - */ + */ function reset() { resetInitialSettings(); indexHandler.reset(); - eventBus.off(events.STREAM_COMPLETED, onStreamCompleted, instance); + eventBus.off(events.STREAM_REQUESTING_COMPLETED, onStreamRequestingCompleted, instance); eventBus.off(events.FRAGMENT_LOADING_COMPLETED, onFragmentLoadingCompleted, instance); } @@ -365,6 +379,7 @@ function OfflineStreamProcessor(config) { return instance; } + OfflineStreamProcessor.__dashjs_factory_name = 'OfflineStreamProcessor'; const factory = dashjs.FactoryMaker.getClassFactory(OfflineStreamProcessor); /* jshint ignore:line */ export default factory; diff --git a/src/offline/constants/OfflineConstants.js b/src/offline/constants/OfflineConstants.js index b51f4fe376..c03f55b09e 100644 --- a/src/offline/constants/OfflineConstants.js +++ b/src/offline/constants/OfflineConstants.js @@ -36,19 +36,19 @@ */ class OfflineConstants { - init() { - this.OFFLINE_SCHEME = 'offline_indexeddb'; - this.OFFLINE_URL_REGEX = /^offline_indexeddb:\/\//i; - this.OFFLINE_STATUS_CREATED = 'created'; - this.OFFLINE_STATUS_STARTED = 'started'; - this.OFFLINE_STATUS_STOPPED = 'stopped'; - this.OFFLINE_STATUS_FINISHED = 'finished'; - this.OFFLINE_STATUS_ERROR = 'error'; - } + init() { + this.OFFLINE_SCHEME = 'offline_indexeddb'; + this.OFFLINE_URL_REGEX = /^offline_indexeddb:\/\//i; + this.OFFLINE_STATUS_CREATED = 'created'; + this.OFFLINE_STATUS_STARTED = 'started'; + this.OFFLINE_STATUS_STOPPED = 'stopped'; + this.OFFLINE_STATUS_FINISHED = 'finished'; + this.OFFLINE_STATUS_ERROR = 'error'; + } - constructor () { - this.init(); - } + constructor() { + this.init(); + } } let constants = new OfflineConstants(); diff --git a/src/offline/controllers/OfflineController.js b/src/offline/controllers/OfflineController.js index 4755342816..5681cb5ab7 100644 --- a/src/offline/controllers/OfflineController.js +++ b/src/offline/controllers/OfflineController.js @@ -41,7 +41,7 @@ import OfflineRecord from '../vo/OfflineDownloadVo'; /** * @module OfflineController * @param {Object} config - dependencies - * @description Provides access to offline stream recording and playback functionality. + * @description Provides access to offline stream recording and playback functionality. This module can be accessed using the MediaPlayer API getOfflineController() */ function OfflineController(config) { @@ -59,6 +59,7 @@ function OfflineController(config) { const playbackController = config.playbackController; const dashMetrics = config.dashMetrics; const timelineConverter = config.timelineConverter; + const segmentBaseController = config.segmentBaseController; const adapter = config.adapter; const manifestUpdater = config.manifestUpdater; const baseURLController = config.baseURLController; @@ -121,6 +122,7 @@ function OfflineController(config) { dashMetrics: dashMetrics, timelineConverter: timelineConverter, errHandler: errHandler, + segmentBaseController: segmentBaseController, offlineStoreController: offlineStoreController, debug: debug, constants: constants, @@ -260,12 +262,12 @@ function OfflineController(config) { download.initDownload(); resolve(id); }) - .catch((e) => { - logger.error('Failed to download ' + e); - removeDownloadFromId(id).then(function () { - reject(e); + .catch((e) => { + logger.error('Failed to download ' + e); + removeDownloadFromId(id).then(function () { + reject(e); + }); }); - }); }); } diff --git a/src/offline/net/IndexDBOfflineLoader.js b/src/offline/net/IndexDBOfflineLoader.js index 94b8aa0390..6c8446989d 100644 --- a/src/offline/net/IndexDBOfflineLoader.js +++ b/src/offline/net/IndexDBOfflineLoader.js @@ -62,9 +62,7 @@ function IndexDBOfflineLoader(config) { config.request.mediaType === constants.VIDEO || config.request.mediaType === constants.TEXT || config.request.mediaType === constants.MUXED || - config.request.mediaType === constants.IMAGE || - config.request.mediaType === constants.FRAGMENTED_TEXT || - config.request.mediaType === constants.EMBEDDED_TEXT + config.request.mediaType === constants.IMAGE ) { let suffix = config.request.type === 'InitializationSegment' ? 'init' : config.request.index; let key = config.request.representationId + '_' + suffix; diff --git a/src/offline/storage/IndexDBStore.js b/src/offline/storage/IndexDBStore.js index 9967a10a85..58467b743b 100644 --- a/src/offline/storage/IndexDBStore.js +++ b/src/offline/storage/IndexDBStore.js @@ -28,12 +28,13 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ -const localforage = require('localforage'); -const entities = require('html-entities').XmlEntities; /** * @ignore */ +const localforage = require('localforage'); +const entities = require('html-entities').XmlEntities; + function IndexDBStore() { let instance, diff --git a/src/offline/utils/OfflineIndexDBManifestParser.js b/src/offline/utils/OfflineIndexDBManifestParser.js index d07f8f5824..27551363c6 100644 --- a/src/offline/utils/OfflineIndexDBManifestParser.js +++ b/src/offline/utils/OfflineIndexDBManifestParser.js @@ -29,6 +29,9 @@ * POSSIBILITY OF SUCH DAMAGE. */ +/** + * @ignore + */ const Entities = require('html-entities').XmlEntities; const OFFLINE_BASE_URL = 'offline_indexeddb://'; @@ -227,13 +230,13 @@ function OfflineIndexDBManifestParser(config) { return constants.AUDIO; } else if (getIsVideo(currentAdaptationSet)) { return constants.VIDEO; - } else if (getIsFragmentedText(currentAdaptationSet)) { - return constants.FRAGMENTED_TEXT; + } else if (getIsText(currentAdaptationSet)) { + return constants.TEXT; } else if (getIsImage(currentAdaptationSet)) { return constants.IMAGE; } - return constants.TEXT; + return null; } function getIsAudio(adaptation) { @@ -244,8 +247,8 @@ function OfflineIndexDBManifestParser(config) { return getIsTypeOf(adaptation, constants.VIDEO); } - function getIsFragmentedText(adaptation) { - return getIsTypeOf(adaptation, constants.FRAGMENTED_TEXT); + function getIsText(adaptation) { + return getIsTypeOf(adaptation, constants.TEXT); } function getIsMuxed(adaptation) { @@ -267,13 +270,6 @@ function OfflineIndexDBManifestParser(config) { throw new Error('type is not defined'); } - // 1. check codecs for fragmented text - if (isFragmentedTextCodecFound(adaptation)) { - // fragmented text codec has been found for adaptation, let's check if tested type is fragmented text - return type === constants.FRAGMENTED_TEXT; - } - - // 2. test mime type return testMimeType(adaptation, type); } @@ -299,37 +295,6 @@ function OfflineIndexDBManifestParser(config) { return false; } - /** - * Search for fragmented text codec in adaptation (STPP or WVTT) - * @param {Object} adaptation - */ - function isFragmentedTextCodecFound (adaptation) { - let isFragmentedTextCodecFoundInTag = function (tag) { - let codecs = tag.getAttribute(dashConstants.CODECS); - if (codecs) { - if (codecs.search(constants.STPP) === 0 || - codecs.search(constants.WVTT) === 0 ) { - return true; - } - } - return false; - }; - - if (isFragmentedTextCodecFoundInTag(adaptation)) { - return true; - } - - // check in representations - let representations = findRepresentations(adaptation); - if (representations && representations.length > 0) { - - if (isFragmentedTextCodecFoundInTag(representations[0])) { - return true; - } - } - return false; - } - /** * Returns mime-type of xml tag * @param {Object} tag diff --git a/src/streaming/FragmentLoader.js b/src/streaming/FragmentLoader.js index 9f82097231..e55760c3a5 100644 --- a/src/streaming/FragmentLoader.js +++ b/src/streaming/FragmentLoader.js @@ -42,6 +42,7 @@ function FragmentLoader(config) { const events = config.events; const urlUtils = config.urlUtils; const errors = config.errors; + const streamId = config.streamId; let instance, urlLoader; @@ -53,7 +54,6 @@ function FragmentLoader(config) { dashMetrics: config.dashMetrics, mediaPlayerModel: config.mediaPlayerModel, requestModifier: config.requestModifier, - useFetch: config.settings.get().streaming.lowLatencyEnabled, urlUtils: urlUtils, constants: Constants, boxParser: config.boxParser, @@ -64,10 +64,7 @@ function FragmentLoader(config) { function checkForExistence(request) { const report = function (success) { - eventBus.trigger(events.CHECK_FOR_EXISTENCE_COMPLETED, { - request: request, - exists: success - } + eventBus.trigger(events.CHECK_FOR_EXISTENCE_COMPLETED, { request: request, exists: success } ); }; @@ -103,7 +100,8 @@ function FragmentLoader(config) { progress: function (event) { eventBus.trigger(events.LOADING_PROGRESS, { request: request, - stream: event.stream + stream: event.stream, + streamId }); if (event.data) { eventBus.trigger(events.LOADING_DATA_PROGRESS, { diff --git a/src/streaming/ManifestLoader.js b/src/streaming/ManifestLoader.js index 7733fb0dbf..2117c105a6 100644 --- a/src/streaming/ManifestLoader.js +++ b/src/streaming/ManifestLoader.js @@ -47,6 +47,7 @@ function ManifestLoader(config) { config = config || {}; const context = this.context; const debug = config.debug; + const settings = config.settings; const eventBus = EventBus(context).getInstance(); const urlUtils = URLUtils(context).getInstance(); @@ -68,7 +69,6 @@ function ManifestLoader(config) { dashMetrics: config.dashMetrics, mediaPlayerModel: config.mediaPlayerModel, requestModifier: config.requestModifier, - useFetch: config.settings.get().streaming.lowLatencyEnabled, urlUtils: urlUtils, constants: Constants, dashConstants: DashConstants, @@ -195,6 +195,20 @@ function ManifestLoader(config) { logger.debug('BaseURI set by Location to: ' + baseUri); } + // If there is a mismatch between the manifest's specified duration and the total duration of all periods, + // and the specified duration is greater than the total duration of all periods, + // overwrite the manifest's duration attribute. This is a patch for if a manifest is generated incorrectly. + if (settings && + settings.get().streaming.enableManifestDurationMismatchFix && + manifest.mediaPresentationDuration && + manifest.Period_asArray.length > 1) { + const sumPeriodDurations = manifest.Period_asArray.reduce((totalDuration, period) => totalDuration + period.duration, 0); + if (!isNaN(sumPeriodDurations) && manifest.mediaPresentationDuration > sumPeriodDurations) { + logger.warn('Media presentation duration greater than duration of all periods. Setting duration to total period duration'); + manifest.mediaPresentationDuration = sumPeriodDurations; + } + } + manifest.baseUri = baseUri; manifest.loadedTime = new Date(); xlinkController.resolveManifestOnLoad(manifest); diff --git a/src/streaming/ManifestUpdater.js b/src/streaming/ManifestUpdater.js index 2afb5aee9a..85a7558f92 100644 --- a/src/streaming/ManifestUpdater.js +++ b/src/streaming/ManifestUpdater.js @@ -30,6 +30,7 @@ */ import EventBus from '../core/EventBus'; import Events from '../core/events/Events'; +import MediaPlayerEvents from '../streaming/MediaPlayerEvents'; import FactoryMaker from '../core/FactoryMaker'; import Debug from '../core/Debug'; import Errors from '../core/errors/Errors'; @@ -83,8 +84,8 @@ function ManifestUpdater() { resetInitialSettings(); eventBus.on(Events.STREAMS_COMPOSED, onStreamsComposed, this); - eventBus.on(Events.PLAYBACK_STARTED, onPlaybackStarted, this); - eventBus.on(Events.PLAYBACK_PAUSED, onPlaybackPaused, this); + eventBus.on(MediaPlayerEvents.PLAYBACK_STARTED, onPlaybackStarted, this); + eventBus.on(MediaPlayerEvents.PLAYBACK_PAUSED, onPlaybackPaused, this); eventBus.on(Events.INTERNAL_MANIFEST_LOADED, onManifestLoaded, this); } @@ -102,8 +103,8 @@ function ManifestUpdater() { function reset() { - eventBus.off(Events.PLAYBACK_STARTED, onPlaybackStarted, this); - eventBus.off(Events.PLAYBACK_PAUSED, onPlaybackPaused, this); + eventBus.off(MediaPlayerEvents.PLAYBACK_STARTED, onPlaybackStarted, this); + eventBus.off(MediaPlayerEvents.PLAYBACK_PAUSED, onPlaybackPaused, this); eventBus.off(Events.STREAMS_COMPOSED, onStreamsComposed, this); eventBus.off(Events.INTERNAL_MANIFEST_LOADED, onManifestLoaded, this); @@ -251,7 +252,7 @@ function ManifestUpdater() { } function onPlaybackPaused(/*e*/) { - isPaused = !settings.get().streaming.scheduleWhilePaused; + isPaused = !settings.get().streaming.scheduling.scheduleWhilePaused; if (isPaused) { stopManifestRefreshTimer(); @@ -263,10 +264,15 @@ function ManifestUpdater() { isUpdating = false; } + function getIsUpdating() { + return isUpdating; + } + instance = { initialize: initialize, setManifest: setManifest, refreshManifest: refreshManifest, + getIsUpdating: getIsUpdating, setConfig: setConfig, reset: reset }; diff --git a/src/streaming/MediaPlayer.js b/src/streaming/MediaPlayer.js index 093285f5a9..07f3f3b149 100644 --- a/src/streaming/MediaPlayer.js +++ b/src/streaming/MediaPlayer.js @@ -35,15 +35,16 @@ import MetricsConstants from './constants/MetricsConstants'; import PlaybackController from './controllers/PlaybackController'; import StreamController from './controllers/StreamController'; import GapController from './controllers/GapController'; +import CatchupController from './controllers/CatchupController'; +import ServiceDescriptionController from '../dash/controllers/ServiceDescriptionController'; +import ContentSteeringController from '../dash/controllers/ContentSteeringController'; import MediaController from './controllers/MediaController'; import BaseURLController from './controllers/BaseURLController'; import ManifestLoader from './ManifestLoader'; import ErrorHandler from './utils/ErrorHandler'; import Capabilities from './utils/Capabilities'; import CapabilitiesFilter from './utils/CapabilitiesFilter'; -import TextTracks from './text/TextTracks'; import RequestModifier from './utils/RequestModifier'; -import TextController from './text/TextController'; import URIFragmentModel from './models/URIFragmentModel'; import ManifestModel from './models/ManifestModel'; import MediaPlayerModel from './models/MediaPlayerModel'; @@ -62,7 +63,7 @@ import Settings from '../core/Settings'; import { getVersionString } - from './../core/Version'; + from '../core/Version'; //Dash import SegmentBaseController from '../dash/controllers/SegmentBaseController'; @@ -79,15 +80,14 @@ import {checkParameterType} from './utils/SupervisorTools'; import ManifestUpdater from './ManifestUpdater'; import URLUtils from '../streaming/utils/URLUtils'; import BoxParser from './utils/BoxParser'; +import TextController from './text/TextController'; +import CustomParametersModel from './models/CustomParametersModel'; -/* jscs:disable */ /** * The media types - * @typedef {("video" | "audio" | "text" | "fragmentedText" | "embeddedText" | "image")} MediaType + * @typedef {("video" | "audio" | "text" | "image")} MediaType */ -/* jscs:enable */ - /** * @module MediaPlayer * @description The MediaPlayer is the primary dash.js Module and a Facade to build your player around. @@ -144,24 +144,25 @@ function MediaPlayer() { offlineController, adapter, mediaPlayerModel, + customParametersModel, errHandler, baseURLController, capabilities, capabilitiesFilter, streamController, + textController, gapController, playbackController, + serviceDescriptionController, + contentSteeringController, + catchupController, dashMetrics, manifestModel, cmcdModel, videoModel, - textController, uriFragmentModel, domStorage, - segmentBaseController, - licenseRequestFilters, - licenseResponseFilters, - customCapabilitiesFilters; + segmentBaseController; /* --------------------------------------------------------------------------- @@ -183,11 +184,9 @@ function MediaPlayer() { segmentBaseController = null; Events.extend(MediaPlayerEvents); mediaPlayerModel = MediaPlayerModel(context).getInstance(); + customParametersModel = CustomParametersModel(context).getInstance(); videoModel = VideoModel(context).getInstance(); uriFragmentModel = URIFragmentModel(context).getInstance(); - licenseRequestFilters = []; - licenseResponseFilters = []; - customCapabilitiesFilters = []; } /** @@ -210,15 +209,30 @@ function MediaPlayer() { if (config.streamController) { streamController = config.streamController; } + if (config.textController) { + textController = config.textController; + } if (config.gapController) { gapController = config.gapController; } if (config.playbackController) { playbackController = config.playbackController; } + if (config.serviceDescriptionController) { + serviceDescriptionController = config.serviceDescriptionController + } + if (config.contentSteeringController) { + contentSteeringController = config.contentSteeringController; + } + if (config.catchupController) { + catchupController = config.catchupController; + } if (config.mediaPlayerModel) { mediaPlayerModel = config.mediaPlayerModel; } + if (config.customParametersModel) { + customParametersModel = config.customParametersModel; + } if (config.abrController) { abrController = config.abrController; } @@ -243,124 +257,154 @@ function MediaPlayer() { * * @param {HTML5MediaElement=} view - Optional arg to set the video element. {@link module:MediaPlayer#attachView attachView()} * @param {string=} source - Optional arg to set the media source. {@link module:MediaPlayer#attachSource attachSource()} - * @param {boolean=} AutoPlay - Optional arg to set auto play. {@link module:MediaPlayer#setAutoPlay setAutoPlay()} - * @see {@link module:MediaPlayer#attachView attachView()} + * @param {boolean=} autoPlay - Optional arg to set auto play. {@link module:MediaPlayer#setAutoPlay setAutoPlay()} + * @param {number|string} startTime - For VoD content the start time is relative to the start time of the first period. + * For live content + * If the parameter starts from prefix posix: it signifies the absolute time range defined in seconds of Coordinated Universal Time (ITU-R TF.460-6). This is the number of seconds since 01-01-1970 00:00:00 UTC. Fractions of seconds may be optionally specified down to the millisecond level. + * If no posix prefix is used the starttime is relative to MPD@availabilityStartTime * @see {@link module:MediaPlayer#attachSource attachSource()} * @see {@link module:MediaPlayer#setAutoPlay setAutoPlay()} * @memberof module:MediaPlayer * @instance */ - function initialize(view, source, AutoPlay) { + function initialize(view, source, autoPlay, startTime = NaN) { if (!capabilities) { capabilities = Capabilities(context).getInstance(); + capabilities.setConfig({ + settings + }) } - errHandler = ErrorHandler(context).getInstance(); + if (!errHandler) { + errHandler = ErrorHandler(context).getInstance(); + } if (!capabilities.supportsMediaSource()) { errHandler.error(new DashJSError(Errors.CAPABILITY_MEDIASOURCE_ERROR_CODE, Errors.CAPABILITY_MEDIASOURCE_ERROR_MESSAGE)); return; } - if (mediaPlayerInitialized) return; - mediaPlayerInitialized = true; + if (!mediaPlayerInitialized) { + mediaPlayerInitialized = true; + + // init some controllers and models + timelineConverter = TimelineConverter(context).getInstance(); + if (!abrController) { + abrController = AbrController(context).getInstance(); + abrController.setConfig({ + settings: settings + }); + } - // init some controllers and models - timelineConverter = TimelineConverter(context).getInstance(); - if (!abrController) { - abrController = AbrController(context).getInstance(); - abrController.setConfig({ - settings: settings - }); - } + if (!schemeLoaderFactory) { + schemeLoaderFactory = SchemeLoaderFactory(context).getInstance(); + } - if (!schemeLoaderFactory) { - schemeLoaderFactory = SchemeLoaderFactory(context).getInstance(); - } + if (!playbackController) { + playbackController = PlaybackController(context).getInstance(); + } - if (!playbackController) { - playbackController = PlaybackController(context).getInstance(); - } + if (!mediaController) { + mediaController = MediaController(context).getInstance(); + } - if (!mediaController) { - mediaController = MediaController(context).getInstance(); - } + if (!streamController) { + streamController = StreamController(context).getInstance(); + } - if (!streamController) { - streamController = StreamController(context).getInstance(); - } + if (!gapController) { + gapController = GapController(context).getInstance(); + } - if (!gapController) { - gapController = GapController(context).getInstance(); - } + if (!catchupController) { + catchupController = CatchupController(context).getInstance(); + } - if (!capabilitiesFilter) { - capabilitiesFilter = CapabilitiesFilter(context).getInstance(); - } + if (!serviceDescriptionController) { + serviceDescriptionController = ServiceDescriptionController(context).getInstance(); + } - adapter = DashAdapter(context).getInstance(); + if (!contentSteeringController) { + contentSteeringController = ContentSteeringController(context).getInstance(); + } - manifestModel = ManifestModel(context).getInstance(); + if (!capabilitiesFilter) { + capabilitiesFilter = CapabilitiesFilter(context).getInstance(); + } - cmcdModel = CmcdModel(context).getInstance(); + adapter = DashAdapter(context).getInstance(); - dashMetrics = DashMetrics(context).getInstance({ - settings: settings - }); - textController = TextController(context).getInstance(); - domStorage = DOMStorage(context).getInstance({ - settings: settings - }); + manifestModel = ManifestModel(context).getInstance(); - adapter.setConfig({ - constants: Constants, - cea608parser: cea608parser, - errHandler: errHandler, - BASE64: BASE64 - }); + cmcdModel = CmcdModel(context).getInstance(); - if (!baseURLController) { - baseURLController = BaseURLController(context).create(); - } + dashMetrics = DashMetrics(context).getInstance({ + settings: settings + }); - baseURLController.setConfig({ - adapter: adapter - }); + domStorage = DOMStorage(context).getInstance({ + settings: settings + }); + adapter.setConfig({ + constants: Constants, + cea608parser: cea608parser, + errHandler: errHandler, + BASE64: BASE64 + }); - segmentBaseController = SegmentBaseController(context).getInstance({ - dashMetrics: dashMetrics, - mediaPlayerModel: mediaPlayerModel, - errHandler: errHandler, - baseURLController: baseURLController, - events: Events, - eventBus: eventBus, - debug: debug, - boxParser: BoxParser(context).getInstance(), - requestModifier: RequestModifier(context).getInstance(), - errors: Errors - }); + if (!baseURLController) { + baseURLController = BaseURLController(context).create(); + } - segmentBaseController.initialize(); + baseURLController.setConfig({ + adapter + }); - // configure controllers - mediaController.setConfig({ - domStorage: domStorage, - settings: settings - }); + serviceDescriptionController.setConfig({ + adapter + }); + + if (!segmentBaseController) { + segmentBaseController = SegmentBaseController(context).getInstance({ + dashMetrics: dashMetrics, + mediaPlayerModel: mediaPlayerModel, + errHandler: errHandler, + baseURLController: baseURLController, + events: Events, + eventBus: eventBus, + debug: debug, + boxParser: BoxParser(context).getInstance(), + requestModifier: RequestModifier(context).getInstance(), + errors: Errors + }); + } + + // configure controllers + mediaController.setConfig({ + domStorage, + settings, + customParametersModel + }); - restoreDefaultUTCTimingSources(); - setAutoPlay(AutoPlay !== undefined ? AutoPlay : true); + mediaPlayerModel.setConfig({ + playbackController, + serviceDescriptionController + }); + + restoreDefaultUTCTimingSources(); + setAutoPlay(autoPlay !== undefined ? autoPlay : true); - // Detect and initialize offline module to support offline contents playback - detectOffline(); + // Detect and initialize offline module to support offline contents playback + _detectOffline(); + } if (view) { attachView(view); } if (source) { - attachSource(source); + attachSource(source, startTime); } logger.info('[dash.js ' + getVersion() + '] ' + 'MediaPlayer has been initialized'); @@ -388,8 +432,9 @@ function MediaPlayer() { metricsReportingController.reset(); metricsReportingController = null; } - - segmentBaseController.reset(); + if (customParametersModel) { + customParametersModel.reset(); + } settings.reset(); @@ -407,9 +452,6 @@ function MediaPlayer() { */ function destroy() { reset(); - licenseRequestFilters = []; - licenseResponseFilters = []; - customCapabilitiesFilters = []; FactoryMaker.deleteSingletonInstances(context); } @@ -482,28 +524,6 @@ function MediaPlayer() { --------------------------------------------------------------------------- */ - /** - * Causes the player to begin streaming the media as set by the {@link module:MediaPlayer#attachSource attachSource()} - * method in preparation for playing. It specifically does not require a view to be attached with {@link module:MediaPlayer#attachSource attachView()} to begin preloading. - * When a view is attached after preloading, the buffered data is transferred to the attached mediaSource buffers. - * - * @see {@link module:MediaPlayer#attachSource attachSource()} - * @see {@link module:MediaPlayer#attachView attachView()} - * @memberof module:MediaPlayer - * @throws {@link module:MediaPlayer~SOURCE_NOT_ATTACHED_ERROR SOURCE_NOT_ATTACHED_ERROR} if called before attachSource function - * @instance - */ - function preload() { - if (videoModel.getElement() || streamingInitialized) { - return false; - } - if (source) { - initializePlayback(); - } else { - throw SOURCE_NOT_ATTACHED_ERROR; - } - } - /** * The play method initiates playback of the media defined by the {@link module:MediaPlayer#attachSource attachSource()} method. * This method will call play on the native Video Element. @@ -518,7 +538,7 @@ function MediaPlayer() { throw PLAYBACK_NOT_INITIALIZED_ERROR; } if (!autoPlay || (isPaused() && playbackInitialized)) { - playbackController.play(); + playbackController.play(true); } } @@ -554,7 +574,8 @@ function MediaPlayer() { * Sets the currentTime property of the attached video element. If it is a live stream with a * timeShiftBufferLength, then the DVR window offset will be automatically calculated. * - * @param {number} value - A relative time, in seconds, based on the return value of the {@link module:MediaPlayer#duration duration()} method is expected + * @param {number} value - A relative time, in seconds, based on the return value of the {@link module:MediaPlayer#duration duration()} method is expected. + * For dynamic streams duration() returns DVRWindow.end - DVRWindow.start. Consequently, the value provided to this function should be relative to DVRWindow.start. * @see {@link module:MediaPlayer#getDVRSeekOffset getDVRSeekOffset()} * @throws {@link module:MediaPlayer~PLAYBACK_NOT_INITIALIZED_ERROR PLAYBACK_NOT_INITIALIZED_ERROR} if called before initializePlayback function * @throws {@link Constants#BAD_ARGUMENT_ERROR BAD_ARGUMENT_ERROR} if called with an invalid argument, not number type or is NaN. @@ -573,7 +594,18 @@ function MediaPlayer() { } let s = playbackController.getIsDynamic() ? getDVRSeekOffset(value) : value; - playbackController.seek(s); + playbackController.seek(s, false, false, true); + } + + /** + * Seeks back to the original live edge (live edge as calculated at playback start). Only applies to live streams, for VoD streams this call will be ignored. + */ + function seekToOriginalLive() { + if (!playbackInitialized || !isDynamic()) { + return; + } + + playbackController.seekToOriginalLive(); } /** @@ -604,6 +636,19 @@ function MediaPlayer() { return playbackController.getIsDynamic(); } + /** + * Returns a boolean that indicates whether the player is operating in low latency mode. + * @return {boolean} + * @memberof module:MediaPlayer + * @instance + */ + function getLowLatencyModeEnabled() { + if (!playbackInitialized) { + throw PLAYBACK_NOT_INITIALIZED_ERROR; + } + return playbackController.getLowLatencyModeEnabled(); + } + /** * Use this method to set the native Video Element's playback rate. * @param {number} value @@ -672,21 +717,21 @@ function MediaPlayer() { /** * The length of the buffer for a given media type, in seconds. Valid media - * types are "video", "audio" and "fragmentedText". If no type is passed - * in, then the minimum of video, audio and fragmentedText buffer length is + * types are "video", "audio" and "text". If no type is passed + * in, then the minimum of video, audio and text buffer length is * returned. NaN is returned if an invalid type is requested, the * presentation does not contain that type, or if no arguments are passed * and the presentation does not include any adaption sets of valid media * type. * - * @param {MediaType} type - 'video', 'audio' or 'fragmentedText' + * @param {MediaType} type - 'video', 'audio' or 'text' * @returns {number} The length of the buffer for the given media type, in * seconds, or NaN * @memberof module:MediaPlayer * @instance */ function getBufferLength(type) { - const types = [Constants.VIDEO, Constants.AUDIO, Constants.FRAGMENTED_TEXT]; + const types = [Constants.VIDEO, Constants.AUDIO, Constants.TEXT]; if (!type) { const buffer = types.map( t => getTracksFor(t).length > 0 ? getDashMetrics().getCurrentBufferLevel(t) : Number.MAX_VALUE @@ -708,16 +753,17 @@ function MediaPlayer() { /** * The timeShiftBufferLength (DVR Window), in seconds. * - * @returns {number} The window of allowable play time behind the live point of a live stream. + * @returns {number} The window of allowable play time behind the live point of a live stream as defined in the manifest. * @memberof module:MediaPlayer * @instance */ function getDVRWindowSize() { - let metric = dashMetrics.getCurrentDVRInfo(); + const type = streamController && streamController.hasVideoTrack() ? Constants.VIDEO : Constants.AUDIO; + let metric = dashMetrics.getCurrentDVRInfo(type); if (!metric) { return 0; } - return metric.manifestInfo.DVRWindowSize; + return metric.manifestInfo.dvrWindowSize; } /** @@ -732,12 +778,13 @@ function MediaPlayer() { * @instance */ function getDVRSeekOffset(value) { - let metric = dashMetrics.getCurrentDVRInfo(); + const type = streamController && streamController.hasVideoTrack() ? Constants.VIDEO : Constants.AUDIO; + let metric = dashMetrics.getCurrentDVRInfo(type); if (!metric) { return 0; } - let liveDelay = playbackController.getLiveDelay(); + let liveDelay = playbackController.getOriginalLiveDelay(); let val = metric.range.start + value; @@ -748,6 +795,20 @@ function MediaPlayer() { return val; } + /** + * Returns the target live delay + * @returns {number} The target live delay + * @memberof module:MediaPlayer + * @instance + */ + function getTargetLiveDelay() { + if (!playbackInitialized) { + throw PLAYBACK_NOT_INITIALIZED_ERROR; + } + + return playbackController.getOriginalLiveDelay(); + } + /** * Current time of the playhead, in seconds. * @@ -769,7 +830,8 @@ function MediaPlayer() { if (streamId !== undefined) { t = streamController.getTimeRelativeToStreamId(t, streamId); } else if (playbackController.getIsDynamic()) { - let metric = dashMetrics.getCurrentDVRInfo(); + const type = streamController && streamController.hasVideoTrack() ? Constants.VIDEO : Constants.AUDIO; + let metric = dashMetrics.getCurrentDVRInfo(type); t = (metric === null || t === 0) ? 0 : Math.max(0, (t - metric.range.start)); } @@ -779,7 +841,7 @@ function MediaPlayer() { /** * Duration of the media's playback, in seconds. * - * @returns {number} The current duration of the media. + * @returns {number} The current duration of the media. For a dynamic stream this will return DVRWindow.end - DVRWindow.start * @memberof module:MediaPlayer * @throws {@link module:MediaPlayer~PLAYBACK_NOT_INITIALIZED_ERROR PLAYBACK_NOT_INITIALIZED_ERROR} if called before initializePlayback function * @instance @@ -791,7 +853,8 @@ function MediaPlayer() { let d = getVideoElement().duration; if (playbackController.getIsDynamic()) { - let metric = dashMetrics.getCurrentDVRInfo(); + const type = streamController && streamController.hasVideoTrack() ? Constants.VIDEO : Constants.AUDIO; + let metric = dashMetrics.getCurrentDVRInfo(type); d = metric ? (metric.range.end - metric.range.start) : 0; } return d; @@ -813,7 +876,7 @@ function MediaPlayer() { if (time() < 0) { return NaN; } - return getAsUTC(time()); + return _getAsUTC(time()); } /** @@ -829,7 +892,7 @@ function MediaPlayer() { if (!playbackInitialized) { throw PLAYBACK_NOT_INITIALIZED_ERROR; } - return getAsUTC(duration()); + return _getAsUTC(duration()); } /* @@ -841,7 +904,7 @@ function MediaPlayer() { */ /** * Gets the top quality BitrateInfo checking portal limit and max allowed. - * It calls getTopQualityIndexFor internally + * It calls getMaxAllowedIndexFor internally * * @param {MediaType} type - 'video' or 'audio' * @memberof module:MediaPlayer @@ -858,13 +921,12 @@ function MediaPlayer() { /** * Gets the current download quality for media type video, audio or images. For video and audio types the ABR - * rules update this value before every new download unless setAutoSwitchQualityFor(type, false) is called. For 'image' + * rules update this value before every new download unless autoSwitchBitrate is set to false. For 'image' * type, thumbnails, there is no ABR algorithm and quality is set manually. * * @param {MediaType} type - 'video', 'audio' or 'image' (thumbnails) * @returns {number} the quality index, 0 corresponding to the lowest bitrate * @memberof module:MediaPlayer - * @see {@link module:MediaPlayer#setAutoSwitchQualityFor setAutoSwitchQualityFor()} * @see {@link module:MediaPlayer#setQualityFor setQualityFor()} * @throws {@link module:MediaPlayer~STREAMING_NOT_INITIALIZED_ERROR STREAMING_NOT_INITIALIZED_ERROR} if called before initializePlayback function * @instance @@ -887,17 +949,17 @@ function MediaPlayer() { /** * Sets the current quality for media type instead of letting the ABR Heuristics automatically selecting it. - * This value will be overwritten by the ABR rules unless setAutoSwitchQualityFor(type, false) is called. + * This value will be overwritten by the ABR rules unless autoSwitchBitrate is set to false. * * @param {MediaType} type - 'video', 'audio' or 'image' * @param {number} value - the quality index, 0 corresponding to the lowest bitrate + * @param {boolean} forceReplace - true if segments have to be replaced by segments of the new quality * @memberof module:MediaPlayer - * @see {@link module:MediaPlayer#setAutoSwitchQualityFor setAutoSwitchQualityFor()} * @see {@link module:MediaPlayer#getQualityFor getQualityFor()} * @throws {@link module:MediaPlayer~STREAMING_NOT_INITIALIZED_ERROR STREAMING_NOT_INITIALIZED_ERROR} if called before initializePlayback function * @instance */ - function setQualityFor(type, value) { + function setQualityFor(type, value, forceReplace = false) { if (!streamingInitialized) { throw STREAMING_NOT_INITIALIZED_ERROR; } @@ -911,7 +973,7 @@ function MediaPlayer() { thumbnailController.setTrackByIndex(value); } } - abrController.setPlaybackQuality(type, streamController.getActiveStreamInfo(), value); + abrController.setPlaybackQuality(type, streamController.getActiveStreamInfo(), value, { forceReplace }); } /** @@ -963,7 +1025,7 @@ function MediaPlayer() { /** * @memberof module:MediaPlayer * @instance - * @returns {number|NaN} Current live stream latency in seconds. It is the difference between current time and time position at the playback head. + * @returns {number|NaN} Current live stream latency in seconds. It is the difference between now time and time position at the playback head. * @throws {@link module:MediaPlayer~MEDIA_PLAYER_NOT_INITIALIZED_ERROR MEDIA_PLAYER_NOT_INITIALIZED_ERROR} if called before initialize function */ function getCurrentLiveLatency() { @@ -990,7 +1052,7 @@ function MediaPlayer() { * @instance */ function addABRCustomRule(type, rulename, rule) { - mediaPlayerModel.addABRCustomRule(type, rulename, rule); + customParametersModel.addAbrCustomRule(type, rulename, rule); } /** @@ -1001,16 +1063,24 @@ function MediaPlayer() { * @instance */ function removeABRCustomRule(rulename) { - mediaPlayerModel.removeABRCustomRule(rulename); + customParametersModel.removeAbrCustomRule(rulename); } /** - * Remove all custom rules + * Remove all ABR custom rules * @memberof module:MediaPlayer * @instance */ function removeAllABRCustomRule() { - mediaPlayerModel.removeABRCustomRule(); + customParametersModel.removeAllAbrCustomRule(); + } + + /** + * Returns all ABR custom rules + * @return {Array} + */ + function getABRCustomRules() { + return customParametersModel.getAbrCustomRules(); } /** @@ -1043,7 +1113,7 @@ function MediaPlayer() { * @instance */ function addUTCTimingSource(schemeIdUri, value) { - mediaPlayerModel.addUTCTimingSource(schemeIdUri, value); + customParametersModel.addUTCTimingSource(schemeIdUri, value); } /** @@ -1057,7 +1127,7 @@ function MediaPlayer() { * @instance */ function removeUTCTimingSource(schemeIdUri, value) { - mediaPlayerModel.removeUTCTimingSource(schemeIdUri, value); + customParametersModel.removeUTCTimingSource(schemeIdUri, value); } /** @@ -1072,7 +1142,7 @@ function MediaPlayer() { * @instance */ function clearDefaultUTCTimingSources() { - mediaPlayerModel.clearDefaultUTCTimingSources(); + customParametersModel.clearDefaultUTCTimingSources(); } /** @@ -1089,7 +1159,7 @@ function MediaPlayer() { * @instance */ function restoreDefaultUTCTimingSources() { - mediaPlayerModel.restoreDefaultUTCTimingSources(); + customParametersModel.restoreDefaultUTCTimingSources(); } /** @@ -1102,7 +1172,9 @@ function MediaPlayer() { */ function getAverageThroughput(type) { const throughputHistory = abrController.getThroughputHistory(); - return throughputHistory ? throughputHistory.getAverageThroughput(type) : 0; + const isDynamic = playbackController.getIsDynamic(); + + return throughputHistory ? throughputHistory.getAverageThroughput(type, isDynamic) : 0; } /** @@ -1116,7 +1188,7 @@ function MediaPlayer() { * @instance */ function setXHRWithCredentialsForType(type, value) { - mediaPlayerModel.setXHRWithCredentialsForType(type, value); + customParametersModel.setXHRWithCredentialsForType(type, value); } /** @@ -1129,7 +1201,7 @@ function MediaPlayer() { * @instance */ function getXHRWithCredentialsForType(type) { - return mediaPlayerModel.getXHRWithCredentialsForType(type); + return customParametersModel.getXHRWithCredentialsForType(type); } /* @@ -1146,7 +1218,7 @@ function MediaPlayer() { * @instance */ function getOfflineController() { - return detectOffline(); + return _detectOffline(); } /* @@ -1176,70 +1248,6 @@ function MediaPlayer() { --------------------------------------------------------------------------- */ - /** - * Set default language for text. If default language is not one of text tracks, dash will choose the first one. - * - * @param {string} lang - default language - * @memberof module:MediaPlayer - * @instance - * @deprecated will be removed in version 3.2.0. Please use setInitialMediaSettingsFor("fragmentedText", { lang: lang }) instead - */ - function setTextDefaultLanguage(lang) { - logger.warn('setTextDefaultLanguage is deprecated and will be removed in version 3.2.0. Please use setInitialMediaSettingsFor("fragmentedText", { lang: lang }) instead'); - if (textController === undefined) { - textController = TextController(context).getInstance(); - } - textController.setTextDefaultLanguage(lang); - } - - /** - * Get default language for text. - * - * @return {string} the default language if it has been set using setTextDefaultLanguage - * @memberof module:MediaPlayer - * @instance - * @deprecated will be removed in version 3.2.0. Please use getInitialMediaSettingsFor("fragmentedText").lang instead - */ - function getTextDefaultLanguage() { - logger.warn('getTextDefaultLanguage is deprecated and will be removed in version 3.2.0. Please use getInitialMediaSettingsFor("fragmentedText").lang instead'); - if (textController === undefined) { - textController = TextController(context).getInstance(); - } - - return textController.getTextDefaultLanguage(); - } - - /** - * Set enabled default state. - * This is used to enable/disable text when a file is loaded. - * During playback, use enableText to enable text for the file - * - * @param {boolean} enable - true to enable text, false otherwise - * @memberof module:MediaPlayer - * @instance - */ - function setTextDefaultEnabled(enable) { - if (textController === undefined) { - textController = TextController(context).getInstance(); - } - - textController.setTextDefaultEnabled(enable); - } - - /** - * Get enabled default state. - * - * @return {boolean} default enable state - * @memberof module:MediaPlayer - * @instance - */ - function getTextDefaultEnabled() { - if (textController === undefined) { - textController = TextController(context).getInstance(); - } - - return textController.getTextDefaultEnabled(); - } /** * Enable/disable text @@ -1250,11 +1258,13 @@ function MediaPlayer() { * @instance */ function enableText(enable) { - if (textController === undefined) { - textController = TextController(context).getInstance(); + const activeStreamInfo = streamController.getActiveStreamInfo(); + + if (!activeStreamInfo || !textController) { + return false; } - textController.enableText(enable); + return textController.enableText(activeStreamInfo.id, enable); } /** @@ -1266,11 +1276,11 @@ function MediaPlayer() { * @instance */ function enableForcedTextStreaming(enable) { - if (textController === undefined) { - textController = TextController(context).getInstance(); + if (!textController) { + return false; } - textController.enableForcedTextStreaming(enable); + return textController.enableForcedTextStreaming(enable); } /** @@ -1281,11 +1291,13 @@ function MediaPlayer() { * @instance */ function isTextEnabled() { - if (textController === undefined) { - textController = TextController(context).getInstance(); + const activeStreamInfo = streamController.getActiveStreamInfo(); + + if (!activeStreamInfo || !textController) { + return false; } - return textController.isTextEnabled(); + return textController.isTextEnabled(activeStreamInfo); } /** @@ -1302,35 +1314,27 @@ function MediaPlayer() { throw PLAYBACK_NOT_INITIALIZED_ERROR; } - if (textController === undefined) { - textController = TextController(context).getInstance(); + const activeStreamInfo = streamController.getActiveStreamInfo(); + + if (!activeStreamInfo || !textController) { + return; } - textController.setTextTrack(idx); + textController.setTextTrack(activeStreamInfo.id, idx); } function getCurrentTextTrackIndex() { let idx = NaN; - if (textController) { - idx = textController.getCurrentTrackIdx(); + + const activeStreamInfo = streamController.getActiveStreamInfo(); + + if (!activeStreamInfo || !textController) { + return; } - return idx; - } - /** - * This method serves to control captions z-index value. If 'true' is passed, the captions will have the highest z-index and be - * displayed on top of other html elements. Default value is 'false' (z-index is not set). - * @param {boolean} value - * @memberof module:MediaPlayer - * @instance - */ - function displayCaptionsOnTop(value) { - let textTracks = TextTracks(context).getInstance(); - textTracks.setConfig({ - videoModel: videoModel - }); - textTracks.initialize(); - textTracks.setDisplayCConTop(value); + idx = textController.getCurrentTrackIdx(activeStreamInfo.id); + + return idx; } /* @@ -1371,9 +1375,9 @@ function MediaPlayer() { videoModel.setElement(element); if (element) { - detectProtection(); - detectMetricsReporting(); - detectMss(); + _detectProtection(); + _detectMetricsReporting(); + _detectMss(); if (streamController) { streamController.switchToVideoElement(); @@ -1381,10 +1385,10 @@ function MediaPlayer() { } if (playbackInitialized) { //Reset if we have been playing before, so this is a new element. - resetPlaybackControllers(); + _resetPlaybackControllers(); } - initializePlayback(); + _initializePlayback(); } /** @@ -1462,7 +1466,12 @@ function MediaPlayer() { throw STREAMING_NOT_INITIALIZED_ERROR; } let streamInfo = streamController.getActiveStreamInfo(); - return mediaController.getTracksFor(type, streamInfo); + + if (!streamInfo) { + return []; + } + + return mediaController.getTracksFor(type, streamInfo.id); } /** @@ -1498,13 +1507,13 @@ function MediaPlayer() { throw STREAMING_NOT_INITIALIZED_ERROR; } let streamInfo = streamController.getActiveStreamInfo(); - return mediaController.getCurrentTrackFor(type, streamInfo); + return mediaController.getCurrentTrackFor(type, streamInfo.id); } /** * This method allows to set media settings that will be used to pick the initial track. Format of the settings * is following:
- * {lang: langValue (can be either a string or a regex to match), + * {lang: langValue (can be either a string primitive, a string object, or a RegExp object to match), * index: indexValue, * viewpoint: viewpointValue, * audioChannelConfiguration: audioChannelConfigurationValue, @@ -1522,9 +1531,6 @@ function MediaPlayer() { throw MEDIA_PLAYER_NOT_INITIALIZED_ERROR; } mediaController.setInitialSettings(type, value); - if (type === Constants.FRAGMENTED_TEXT) { - textController.setInitialSettings(value); - } } /** @@ -1562,133 +1568,49 @@ function MediaPlayer() { mediaController.setTrack(track); } - /** - * This method returns the current track switch mode. - * - * @param {MediaType} type - * @returns {string} mode - * @memberof module:MediaPlayer - * @throws {@link module:MediaPlayer~MEDIA_PLAYER_NOT_INITIALIZED_ERROR MEDIA_PLAYER_NOT_INITIALIZED_ERROR} if called before initialize function - * @instance - */ - function getTrackSwitchModeFor(type) { - if (!mediaPlayerInitialized) { - throw MEDIA_PLAYER_NOT_INITIALIZED_ERROR; - } - return mediaController.getSwitchMode(type); - } - - /** - * This method sets the current track switch mode. Available options are: - * - * Constants.TRACK_SWITCH_MODE_NEVER_REPLACE - * (used to forbid clearing the buffered data (prior to current playback position) after track switch. - * Defers to fastSwitchEnabled for placement of new data. Default for video) - * - * Constants.TRACK_SWITCH_MODE_ALWAYS_REPLACE - * (used to clear the buffered data (prior to current playback position) after track switch. Default for audio) - * - * @param {MediaType} type - * @param {string} mode - * @memberof module:MediaPlayer - * @throws {@link module:MediaPlayer~MEDIA_PLAYER_NOT_INITIALIZED_ERROR MEDIA_PLAYER_NOT_INITIALIZED_ERROR} if called before initialize function - * @instance - */ - function setTrackSwitchModeFor(type, mode) { - if (!mediaPlayerInitialized) { - throw MEDIA_PLAYER_NOT_INITIALIZED_ERROR; - } - mediaController.setSwitchMode(type, mode); - } - - /** - * This method sets the selection mode for the initial track. This mode defines how the initial track will be selected - * if no initial media settings are set. If initial media settings are set this parameter will be ignored. Available options are: - * - * Constants.TRACK_SELECTION_MODE_HIGHEST_BITRATE - * This mode makes the player select the track with a highest bitrate. This mode is a default mode. - * - * Constants.TRACK_SELECTION_MODE_FIRST_TRACK - * This mode makes the player select the select the first track found in the manifest. - * - * Constants.TRACK_SELECTION_MODE_HIGHEST_EFFICIENCY - * This mode makes the player select the track with the lowest bitrate per pixel average. - * - * Constants.TRACK_SELECTION_MODE_WIDEST_RANGE - * This mode makes the player select the track with a widest range of bitrates. - * - * @param {string} mode - * @memberof module:MediaPlayer - * @throws {@link module:MediaPlayer~MEDIA_PLAYER_NOT_INITIALIZED_ERROR MEDIA_PLAYER_NOT_INITIALIZED_ERROR} if called before initialize function - * @instance - */ - function setSelectionModeForInitialTrack(mode) { - if (!mediaPlayerInitialized) { - throw MEDIA_PLAYER_NOT_INITIALIZED_ERROR; - } - mediaController.setSelectionModeForInitialTrack(mode); - } - - /** - * This method returns the track selection mode. - * - * @returns {string} mode - * @memberof module:MediaPlayer - * @throws {@link module:MediaPlayer~MEDIA_PLAYER_NOT_INITIALIZED_ERROR MEDIA_PLAYER_NOT_INITIALIZED_ERROR} if called before initialize function - * @instance - */ - function getSelectionModeForInitialTrack() { - if (!mediaPlayerInitialized) { - throw MEDIA_PLAYER_NOT_INITIALIZED_ERROR; - } - return mediaController.getSelectionModeForInitialTrack(); - } - /* --------------------------------------------------------------------------- - PROTECTION MANAGEMENT + Custom filter and callback functions --------------------------------------------------------------------------- */ - /** - * Detects if Protection is included and returns an instance of ProtectionController.js + * Registers a custom capabilities filter. This enables application to filter representations to use. + * The provided callback function shall return a boolean based on whether or not to use the representation. + * The filters are applied in the order they are registered. + * @param {function} filter - the custom capabilities filter callback * @memberof module:MediaPlayer * @instance */ - function getProtectionController() { - return detectProtection(); + function registerCustomCapabilitiesFilter(filter) { + customParametersModel.registerCustomCapabilitiesFilter(filter); } /** - * Will override dash.js protection controller. - * @param {ProtectionController} value - valid protection controller instance. + * Unregisters a custom capabilities filter. + * @param {function} filter - the custom capabilities filter callback * @memberof module:MediaPlayer * @instance */ - function attachProtectionController(value) { - protectionController = value; + function unregisterCustomCapabilitiesFilter(filter) { + customParametersModel.unregisterCustomCapabilitiesFilter(filter); } /** - * Sets Protection Data required to setup the Protection Module (DRM). Protection Data must - * be set before initializing MediaPlayer or, once initialized, before PROTECTION_CREATED event is fired. - * @see {@link module:MediaPlayer#initialize initialize()} - * @see {@link ProtectionEvents#event:PROTECTION_CREATED dashjs.Protection.events.PROTECTION_CREATED} - * @param {ProtectionDataSet} value - object containing - * property names corresponding to key system name strings and associated - * values being instances of. - * @memberof module:MediaPlayer - * @instance + * Registers a custom initial track selection function. Only one function is allowed. Calling this method will overwrite a potentially existing function. + * @param {function} customFunc - the custom function that returns the initial track */ - function setProtectionData(value) { - protectionData = value; + function setCustomInitialTrackSelectionFunction(customFunc) { + customParametersModel.setCustomInitialTrackSelectionFunction(customFunc); + } + + /** + * Resets the custom initial track selection + */ + function resetCustomInitialTrackSelectionFunction() { + customParametersModel.resetCustomInitialTrackSelectionFunction(null); - // Propagate changes in case StreamController is already created - if (streamController) { - streamController.setProtectionData(protectionData); - } } /** @@ -1700,10 +1622,7 @@ function MediaPlayer() { * @instance */ function registerLicenseRequestFilter(filter) { - licenseRequestFilters.push(filter); - if (protectionController) { - protectionController.setLicenseRequestFilters(licenseRequestFilters); - } + customParametersModel.registerLicenseRequestFilter(filter); } /** @@ -1715,10 +1634,7 @@ function MediaPlayer() { * @instance */ function registerLicenseResponseFilter(filter) { - licenseResponseFilters.push(filter); - if (protectionController) { - protectionController.setLicenseResponseFilters(licenseResponseFilters); - } + customParametersModel.registerLicenseResponseFilter(filter); } /** @@ -1728,10 +1644,7 @@ function MediaPlayer() { * @instance */ function unregisterLicenseRequestFilter(filter) { - unregisterFilter(licenseRequestFilters, filter); - if (protectionController) { - protectionController.setLicenseRequestFilters(licenseRequestFilters); - } + customParametersModel.unregisterLicenseRequestFilter(filter); } /** @@ -1741,50 +1654,54 @@ function MediaPlayer() { * @instance */ function unregisterLicenseResponseFilter(filter) { - unregisterFilter(licenseResponseFilters, filter); - if (protectionController) { - protectionController.setLicenseResponseFilters(licenseResponseFilters); - } + customParametersModel.unregisterLicenseResponseFilter(filter); } + /* + --------------------------------------------------------------------------- + + PROTECTION MANAGEMENT + + --------------------------------------------------------------------------- + */ + /** - * Registers a custom capabilities filter. This enables application to filter representations to use. - * The provided callback function shall return a boolean based on whether or not to use the representation. - * The filters are applied in the order they are registered. - * @param {function} filter - the custom capabilities filter callback + * Detects if Protection is included and returns an instance of ProtectionController.js * @memberof module:MediaPlayer * @instance */ - function registerCustomCapabilitiesFilter(filter) { - customCapabilitiesFilters.push(filter); - if (capabilitiesFilter) { - capabilitiesFilter.setCustomCapabilitiesFilters(customCapabilitiesFilters); - } + function getProtectionController() { + return _detectProtection(); } /** - * Unregisters a custom capabilities filter. - * @param {function} filter - the custom capabilities filter callback + * Will override dash.js protection controller. + * @param {ProtectionController} value - valid protection controller instance. * @memberof module:MediaPlayer * @instance */ - function unregisterCustomCapabilitiesFilter(filter) { - unregisterFilter(customCapabilitiesFilters, filter); - if (capabilitiesFilter) { - capabilitiesFilter.setCustomCapabilitiesFilters(customCapabilitiesFilters); - } + function attachProtectionController(value) { + protectionController = value; } - function unregisterFilter(filters, filter) { - let index = -1; - filters.some((item, i) => { - if (item === filter) { - index = i; - return true; - } - }); - if (index < 0) return; - filters.splice(index, 1); + /** + * Sets Protection Data required to setup the Protection Module (DRM). Protection Data must + * be set before initializing MediaPlayer or, once initialized, before PROTECTION_CREATED event is fired. + * @see {@link module:MediaPlayer#initialize initialize()} + * @see {@link ProtectionEvents#event:PROTECTION_CREATED dashjs.Protection.events.PROTECTION_CREATED} + * @param {ProtectionDataSet} value - object containing + * property names corresponding to key system name strings and associated + * values being instances of. + * @memberof module:MediaPlayer + * @instance + */ + function setProtectionData(value) { + protectionData = value; + + // Propagate changes in case StreamController is already created + if (streamController) { + streamController.setProtectionData(protectionData); + } } /* @@ -1824,8 +1741,7 @@ function MediaPlayer() { return; } - const timeInPeriod = streamController.getTimeRelativeToStreamId(s, stream.getId()); - return thumbnailController.provide(timeInPeriod, callback); + return thumbnailController.provide(s, callback); } /* @@ -1846,7 +1762,7 @@ function MediaPlayer() { * @instance */ function retrieveManifest(url, callback) { - let manifestLoader = createManifestLoader(); + let manifestLoader = _createManifestLoader(); let self = this; const handler = function (e) { @@ -1879,6 +1795,16 @@ function MediaPlayer() { return source; } + /** + * Sets the source to a new manifest URL or object without reloading + * Useful for updating CDN tokens + * @param urlOrManifest + */ + function updateSource(urlOrManifest) { + source = urlOrManifest + streamController.load(source); + } + /** * Use this method to set a source URL to a valid MPD manifest file OR * a previously downloaded and parsed manifest object. Optionally, can @@ -1886,14 +1812,17 @@ function MediaPlayer() { * * @param {string|Object} urlOrManifest - A URL to a valid MPD manifest file, or a * parsed manifest object. - * + * @param {number|string} startTime - For VoD content the start time is relative to the start time of the first period. + * For live content + * If the parameter starts from prefix posix: it signifies the absolute time range defined in seconds of Coordinated Universal Time (ITU-R TF.460-6). This is the number of seconds since 01-01-1970 00:00:00 UTC. Fractions of seconds may be optionally specified down to the millisecond level. + * If no posix prefix is used the starttime is relative to MPD@availabilityStartTime * * @throws {@link module:MediaPlayer~MEDIA_PLAYER_NOT_INITIALIZED_ERROR MEDIA_PLAYER_NOT_INITIALIZED_ERROR} if called before initialize function * * @memberof module:MediaPlayer * @instance */ - function attachSource(urlOrManifest) { + function attachSource(urlOrManifest, startTime = NaN) { if (!mediaPlayerInitialized) { throw MEDIA_PLAYER_NOT_INITIALIZED_ERROR; } @@ -1905,11 +1834,11 @@ function MediaPlayer() { source = urlOrManifest; if (streamingInitialized || playbackInitialized) { - resetPlaybackControllers(); + _resetPlaybackControllers(); } if (isReady()) { - initializePlayback(); + _initializePlayback(startTime); } } @@ -1934,7 +1863,7 @@ function MediaPlayer() { * @example * player.updateSettings({ * streaming: { - * liveDelayFragmentCount: 8 + * lowLatencyEnabled: false, * abr: { * maxBitrate: { audio: 100, video: 1000 } * } @@ -2038,65 +1967,119 @@ function MediaPlayer() { return streamInfo ? streamController.getStreamById(streamInfo.id) : null; } + /** + * Returns the DashAdapter.js Module. + * + * @see {@link module:DashAdapter} + * @returns {Object} + * @memberof module:MediaPlayer + * @instance + */ + function getDashAdapter() { + return adapter; + } + + /** + * Triggers a request to the content steering server to update the steering information. + * @return {Promise} + */ + function triggerSteeringRequest() { + if (contentSteeringController) { + return contentSteeringController.loadSteeringData(); + } + } + + /** + * Returns the current response data of the content steering server + * @return {object} + */ + function getCurrentSteeringResponseData() { + if(contentSteeringController) { + return contentSteeringController.getCurrentSteeringResponseData(); + } + } + //*********************************** // PRIVATE METHODS //*********************************** - function resetPlaybackControllers() { + function _resetPlaybackControllers() { playbackInitialized = false; streamingInitialized = false; adapter.reset(); streamController.reset(); gapController.reset(); + catchupController.reset(); playbackController.reset(); + serviceDescriptionController.reset(); + contentSteeringController.reset(); abrController.reset(); mediaController.reset(); - textController.reset(); + segmentBaseController.reset(); if (protectionController) { - if (settings.get().streaming.keepProtectionMediaKeys) { + if (settings.get().streaming.protection.keepProtectionMediaKeys) { protectionController.stop(); } else { protectionController.reset(); protectionController = null; - detectProtection(); + _detectProtection(); } } + textController.reset(); cmcdModel.reset(); } - function createPlaybackControllers() { + function _createPlaybackControllers() { // creates or get objects instances - const manifestLoader = createManifestLoader(); + const manifestLoader = _createManifestLoader(); if (!streamController) { streamController = StreamController(context).getInstance(); } + if (!textController) { + textController = TextController(context).create({ + errHandler, + manifestModel, + adapter, + mediaController, + videoModel, + settings + }); + } + capabilitiesFilter.setConfig({ capabilities, + customParametersModel, adapter, - settings + settings, + manifestModel, + errHandler }); - capabilitiesFilter.setCustomCapabilitiesFilters(customCapabilitiesFilters); streamController.setConfig({ - capabilities: capabilities, + capabilities, capabilitiesFilter, - manifestLoader: manifestLoader, - manifestModel: manifestModel, - mediaPlayerModel: mediaPlayerModel, - protectionController: protectionController, - adapter: adapter, - dashMetrics: dashMetrics, - errHandler: errHandler, - timelineConverter: timelineConverter, - videoModel: videoModel, - playbackController: playbackController, - abrController: abrController, - mediaController: mediaController, - textController: textController, - settings: settings, - baseURLController: baseURLController + manifestLoader, + manifestModel, + mediaPlayerModel, + customParametersModel, + protectionController, + textController, + adapter, + dashMetrics, + errHandler, + timelineConverter, + videoModel, + playbackController, + serviceDescriptionController, + contentSteeringController, + abrController, + mediaController, + settings, + baseURLController, + uriFragmentModel, + segmentBaseController }); gapController.setConfig({ @@ -2109,33 +2092,32 @@ function MediaPlayer() { }); playbackController.setConfig({ - streamController: streamController, - dashMetrics: dashMetrics, - mediaPlayerModel: mediaPlayerModel, - adapter: adapter, - videoModel: videoModel, - timelineConverter: timelineConverter, - uriFragmentModel: uriFragmentModel, - settings: settings + streamController, + serviceDescriptionController, + dashMetrics, + adapter, + videoModel, + timelineConverter, + settings }); - abrController.setConfig({ - streamController: streamController, - domStorage: domStorage, - mediaPlayerModel: mediaPlayerModel, - dashMetrics: dashMetrics, - adapter: adapter, - videoModel: videoModel, - settings: settings - }); + catchupController.setConfig({ + streamController, + playbackController, + mediaPlayerModel, + videoModel, + settings + }) - textController.setConfig({ - errHandler: errHandler, - manifestModel: manifestModel, - adapter: adapter, - mediaController: mediaController, - streamController: streamController, - videoModel: videoModel + abrController.setConfig({ + streamController, + domStorage, + mediaPlayerModel, + customParametersModel, + dashMetrics, + adapter, + videoModel, + settings }); cmcdModel.setConfig({ @@ -2144,13 +2126,29 @@ function MediaPlayer() { playbackController }); + contentSteeringController.setConfig({ + adapter, + errHandler, + dashMetrics, + mediaPlayerModel, + manifestModel, + abrController, + eventBus, + requestModifier: RequestModifier(context).getInstance() + }) + // initialises controller + abrController.initialize(); streamController.initialize(autoPlay, protectionData); + textController.initialize(); gapController.initialize(); + catchupController.initialize(); cmcdModel.initialize(); + contentSteeringController.initialize(); + segmentBaseController.initialize(); } - function createManifestLoader() { + function _createManifestLoader() { return ManifestLoader(context).create({ debug: debug, errHandler: errHandler, @@ -2162,7 +2160,7 @@ function MediaPlayer() { }); } - function detectProtection() { + function _detectProtection() { if (protectionController) { return protectionController; } @@ -2179,28 +2177,26 @@ function MediaPlayer() { capabilities = Capabilities(context).getInstance(); } protectionController = protection.createProtectionSystem({ - debug: debug, - errHandler: errHandler, - videoModel: videoModel, - capabilities: capabilities, - eventBus: eventBus, + debug, + errHandler, + videoModel, + customParametersModel, + capabilities, + eventBus, events: Events, - BASE64: BASE64, + BASE64, constants: Constants, - cmcdModel: cmcdModel, - settings: settings + cmcdModel, + settings }); - if (protectionController) { - protectionController.setLicenseRequestFilters(licenseRequestFilters); - protectionController.setLicenseResponseFilters(licenseResponseFilters); - } + return protectionController; } return null; } - function detectMetricsReporting() { + function _detectMetricsReporting() { if (metricsReportingController) { return; } @@ -2215,6 +2211,7 @@ function MediaPlayer() { mediaElement: getVideoElement(), adapter: adapter, dashMetrics: dashMetrics, + mediaPlayerModel: mediaPlayerModel, events: Events, constants: Constants, metricsConstants: MetricsConstants @@ -2222,7 +2219,7 @@ function MediaPlayer() { } } - function detectMss() { + function _detectMss() { if (mssHandler) { return; } @@ -2251,7 +2248,7 @@ function MediaPlayer() { } } - function detectOffline() { + function _detectOffline() { if (!mediaPlayerInitialized) { throw MEDIA_PLAYER_NOT_INITIALIZED_ERROR; } @@ -2270,7 +2267,7 @@ function MediaPlayer() { }); Errors.extend(OfflineController.errors); - const manifestLoader = createManifestLoader(); + const manifestLoader = _createManifestLoader(); const manifestUpdater = ManifestUpdater(context).create(); manifestUpdater.setConfig({ @@ -2293,6 +2290,7 @@ function MediaPlayer() { errHandler: errHandler, dashMetrics: dashMetrics, timelineConverter: timelineConverter, + segmentBaseController: segmentBaseController, schemeLoaderFactory: schemeLoaderFactory, eventBus: eventBus, events: Events, @@ -2308,8 +2306,9 @@ function MediaPlayer() { return null; } - function getAsUTC(valToConvert) { - let metric = dashMetrics.getCurrentDVRInfo(); + function _getAsUTC(valToConvert) { + const type = streamController && streamController.hasVideoTrack() ? Constants.VIDEO : Constants.AUDIO; + let metric = dashMetrics.getCurrentDVRInfo(type); let availableFrom, utcValue; @@ -2321,7 +2320,11 @@ function MediaPlayer() { return utcValue; } - function initializePlayback() { + /** + * + * @private + */ + function _initializePlayback(startTime = NaN) { if (offlineController) { offlineController.resetRecords(); @@ -2330,12 +2333,12 @@ function MediaPlayer() { if (!streamingInitialized && source) { streamingInitialized = true; logger.info('Streaming Initialized'); - createPlaybackControllers(); + _createPlaybackControllers(); if (typeof source === 'string') { - streamController.load(source); + streamController.load(source, startTime); } else { - streamController.loadWithManifest(source); + streamController.loadWithManifest(source, startTime); } } @@ -2345,114 +2348,101 @@ function MediaPlayer() { } } - /** - * Returns the DashAdapter.js Module. - * - * @see {@link module:DashAdapter} - * @returns {Object} - * @memberof module:MediaPlayer - * @instance - */ - function getDashAdapter() { - return adapter; - } - instance = { - initialize: initialize, - setConfig: setConfig, - on: on, - off: off, - extend: extend, - attachView: attachView, - attachSource: attachSource, - isReady: isReady, - preload: preload, - play: play, - isPaused: isPaused, - pause: pause, - isSeeking: isSeeking, - isDynamic: isDynamic, - seek: seek, - setPlaybackRate: setPlaybackRate, - getPlaybackRate: getPlaybackRate, - setMute: setMute, - isMuted: isMuted, - setVolume: setVolume, - getVolume: getVolume, - time: time, - duration: duration, - timeAsUTC: timeAsUTC, - durationAsUTC: durationAsUTC, - getActiveStream: getActiveStream, - getDVRWindowSize: getDVRWindowSize, - getDVRSeekOffset: getDVRSeekOffset, - convertToTimeCode: convertToTimeCode, - formatUTC: formatUTC, - getVersion: getVersion, - getDebug: getDebug, - getBufferLength: getBufferLength, - getTTMLRenderingDiv: getTTMLRenderingDiv, - getVideoElement: getVideoElement, - getSource: getSource, - getCurrentLiveLatency: getCurrentLiveLatency, - getTopBitrateInfoFor: getTopBitrateInfoFor, - setAutoPlay: setAutoPlay, - getAutoPlay: getAutoPlay, - getDashMetrics: getDashMetrics, - getQualityFor: getQualityFor, - setQualityFor: setQualityFor, - updatePortalSize: updatePortalSize, - setTextDefaultLanguage: setTextDefaultLanguage, - getTextDefaultLanguage: getTextDefaultLanguage, - setTextDefaultEnabled: setTextDefaultEnabled, - getTextDefaultEnabled: getTextDefaultEnabled, - enableText: enableText, - enableForcedTextStreaming: enableForcedTextStreaming, - isTextEnabled: isTextEnabled, - setTextTrack: setTextTrack, - getBitrateInfoListFor: getBitrateInfoListFor, - getStreamsFromManifest: getStreamsFromManifest, - getTracksFor: getTracksFor, - getTracksForTypeFromManifest: getTracksForTypeFromManifest, - getCurrentTrackFor: getCurrentTrackFor, - setInitialMediaSettingsFor: setInitialMediaSettingsFor, - getInitialMediaSettingsFor: getInitialMediaSettingsFor, - setCurrentTrack: setCurrentTrack, - getTrackSwitchModeFor: getTrackSwitchModeFor, - setTrackSwitchModeFor: setTrackSwitchModeFor, - setSelectionModeForInitialTrack: setSelectionModeForInitialTrack, - getSelectionModeForInitialTrack: getSelectionModeForInitialTrack, - addABRCustomRule: addABRCustomRule, - removeABRCustomRule: removeABRCustomRule, - removeAllABRCustomRule: removeAllABRCustomRule, - getAverageThroughput: getAverageThroughput, - retrieveManifest: retrieveManifest, - addUTCTimingSource: addUTCTimingSource, - removeUTCTimingSource: removeUTCTimingSource, - clearDefaultUTCTimingSources: clearDefaultUTCTimingSources, - restoreDefaultUTCTimingSources: restoreDefaultUTCTimingSources, - setXHRWithCredentialsForType: setXHRWithCredentialsForType, - getXHRWithCredentialsForType: getXHRWithCredentialsForType, - getProtectionController: getProtectionController, - attachProtectionController: attachProtectionController, - setProtectionData: setProtectionData, - registerLicenseRequestFilter: registerLicenseRequestFilter, - registerLicenseResponseFilter: registerLicenseResponseFilter, - unregisterLicenseRequestFilter: unregisterLicenseRequestFilter, - unregisterLicenseResponseFilter: unregisterLicenseResponseFilter, + initialize, + setConfig, + on, + off, + extend, + attachView, + attachSource, + isReady, + play, + isPaused, + pause, + isSeeking, + isDynamic, + getLowLatencyModeEnabled, + seek, + seekToOriginalLive, + setPlaybackRate, + getPlaybackRate, + setMute, + isMuted, + setVolume, + getVolume, + time, + duration, + timeAsUTC, + durationAsUTC, + getActiveStream, + getDVRWindowSize, + getDVRSeekOffset, + getTargetLiveDelay, + convertToTimeCode, + formatUTC, + getVersion, + getDebug, + getBufferLength, + getTTMLRenderingDiv, + getVideoElement, + getSource, + updateSource, + getCurrentLiveLatency, + getTopBitrateInfoFor, + setAutoPlay, + getAutoPlay, + getDashMetrics, + getQualityFor, + setQualityFor, + updatePortalSize, + enableText, + enableForcedTextStreaming, + isTextEnabled, + setTextTrack, + getBitrateInfoListFor, + getStreamsFromManifest, + getTracksFor, + getTracksForTypeFromManifest, + getCurrentTrackFor, + setInitialMediaSettingsFor, + getInitialMediaSettingsFor, + setCurrentTrack, + addABRCustomRule, + removeABRCustomRule, + removeAllABRCustomRule, + getABRCustomRules, + getAverageThroughput, + retrieveManifest, + addUTCTimingSource, + removeUTCTimingSource, + clearDefaultUTCTimingSources, + restoreDefaultUTCTimingSources, + setXHRWithCredentialsForType, + getXHRWithCredentialsForType, + getProtectionController, + attachProtectionController, + setProtectionData, + registerLicenseRequestFilter, + registerLicenseResponseFilter, + unregisterLicenseRequestFilter, + unregisterLicenseResponseFilter, registerCustomCapabilitiesFilter, unregisterCustomCapabilitiesFilter, - displayCaptionsOnTop: displayCaptionsOnTop, - attachTTMLRenderingDiv: attachTTMLRenderingDiv, - getCurrentTextTrackIndex: getCurrentTextTrackIndex, - provideThumbnail: provideThumbnail, - getDashAdapter: getDashAdapter, - getOfflineController: getOfflineController, - getSettings: getSettings, - updateSettings: updateSettings, - resetSettings: resetSettings, - reset: reset, - destroy: destroy + setCustomInitialTrackSelectionFunction, + resetCustomInitialTrackSelectionFunction, + attachTTMLRenderingDiv, + getCurrentTextTrackIndex, + provideThumbnail, + getDashAdapter, + getOfflineController, + triggerSteeringRequest, + getCurrentSteeringResponseData, + getSettings, + updateSettings, + resetSettings, + reset, + destroy }; setup(); diff --git a/src/streaming/MediaPlayerEvents.js b/src/streaming/MediaPlayerEvents.js index 74867f6d0b..99897d3192 100644 --- a/src/streaming/MediaPlayerEvents.js +++ b/src/streaming/MediaPlayerEvents.js @@ -69,6 +69,12 @@ class MediaPlayerEvents extends EventsBase { */ this.BUFFER_LEVEL_STATE_CHANGED = 'bufferStateChanged'; + /** + * Triggered when the buffer level of a media type has been updated + * @event MediaPlayerEvents#BUFFER_LEVEL_UPDATED + */ + this.BUFFER_LEVEL_UPDATED = 'bufferLevelUpdated'; + /** * Triggered when a dynamic stream changed to static (transition phase between Live and On-Demand). * @event MediaPlayerEvents#DYNAMIC_TO_STATIC @@ -109,7 +115,6 @@ class MediaPlayerEvents extends EventsBase { */ this.LOG = 'log'; - //TODO refactor with internal event /** * Triggered when the manifest load is complete * @event MediaPlayerEvents#MANIFEST_LOADED @@ -141,16 +146,16 @@ class MediaPlayerEvents extends EventsBase { this.METRIC_UPDATED = 'metricUpdated'; /** - * Triggered at the stream end of a period. - * @event MediaPlayerEvents#PERIOD_SWITCH_COMPLETED + * Triggered when a new stream (period) starts. + * @event MediaPlayerEvents#PERIOD_SWITCH_STARTED */ - this.PERIOD_SWITCH_COMPLETED = 'periodSwitchCompleted'; + this.PERIOD_SWITCH_STARTED = 'periodSwitchStarted'; /** - * Triggered when a new period starts. - * @event MediaPlayerEvents#PERIOD_SWITCH_STARTED + * Triggered at the stream end of a period. + * @event MediaPlayerEvents#PERIOD_SWITCH_COMPLETED */ - this.PERIOD_SWITCH_STARTED = 'periodSwitchStarted'; + this.PERIOD_SWITCH_COMPLETED = 'periodSwitchCompleted'; /** * Triggered when an ABR up /down switch is initiated; either by user in manual mode or auto mode via ABR rules. @@ -170,12 +175,6 @@ class MediaPlayerEvents extends EventsBase { */ this.TRACK_CHANGE_RENDERED = 'trackChangeRendered'; - /** - * Triggered when the source is setup and ready. - * @event MediaPlayerEvents#SOURCE_INITIALIZED - */ - this.SOURCE_INITIALIZED = 'sourceInitialized'; - /** * Triggered when a stream (period) is being loaded * @event MediaPlayerEvents#STREAM_INITIALIZING @@ -189,7 +188,19 @@ class MediaPlayerEvents extends EventsBase { this.STREAM_UPDATED = 'streamUpdated'; /** - * Triggered when a stream (period) is updated + * Triggered when a stream (period) is activated + * @event MediaPlayerEvents#STREAM_ACTIVATED + */ + this.STREAM_ACTIVATED = 'streamActivated'; + + /** + * Triggered when a stream (period) is deactivated + * @event MediaPlayerEvents#STREAM_DEACTIVATED + */ + this.STREAM_DEACTIVATED = 'streamDeactivated'; + + /** + * Triggered when a stream (period) is activated * @event MediaPlayerEvents#STREAM_INITIALIZED */ this.STREAM_INITIALIZED = 'streamInitialized'; @@ -244,6 +255,12 @@ class MediaPlayerEvents extends EventsBase { */ this.CAN_PLAY = 'canPlay'; + /** + * This corresponds to the CAN_PLAY_THROUGH readyState. + * @event MediaPlayerEvents#CAN_PLAY_THROUGH + */ + this.CAN_PLAY_THROUGH = 'canPlayThrough'; + /** * Sent when playback completes. * @event MediaPlayerEvents#PLAYBACK_ENDED @@ -270,6 +287,13 @@ class MediaPlayerEvents extends EventsBase { */ this.PLAYBACK_METADATA_LOADED = 'playbackMetaDataLoaded'; + /** + * The event is fired when the frame at the current playback position of the media has finished loading; + * often the first frame + * @event MediaPlayerEvents#PLAYBACK_LOADED_DATA + */ + this.PLAYBACK_LOADED_DATA = 'playbackLoadedData'; + /** * Sent when playback is paused. * @event MediaPlayerEvents#PLAYBACK_PAUSED @@ -310,12 +334,6 @@ class MediaPlayerEvents extends EventsBase { */ this.PLAYBACK_SEEKING = 'playbackSeeking'; - /** - * Sent when a seek operation has been asked. - * @event MediaPlayerEvents#PLAYBACK_SEEK_ASKED - */ - this.PLAYBACK_SEEK_ASKED = 'playbackSeekAsked'; - /** * Sent when the video element reports stalled * @event MediaPlayerEvents#PLAYBACK_STALLED @@ -336,6 +354,12 @@ class MediaPlayerEvents extends EventsBase { */ this.PLAYBACK_TIME_UPDATED = 'playbackTimeUpdated'; + /** + * Sent when the video element reports that the volume has changed + * @event MediaPlayerEvents#PLAYBACK_VOLUME_CHANGED + */ + this.PLAYBACK_VOLUME_CHANGED = 'playbackVolumeChanged'; + /** * Sent when the media playback has stopped because of a temporary lack of data. * @@ -349,18 +373,6 @@ class MediaPlayerEvents extends EventsBase { */ this.MANIFEST_VALIDITY_CHANGED = 'manifestValidityChanged'; - /** - * A gap occured in the timeline which requires a seek to the next period - * @event MediaPlayerEvents#GAP_CAUSED_SEEK_TO_PERIOD_END - */ - this.GAP_CAUSED_SEEK_TO_PERIOD_END = 'gapCausedSeekToPeriodEnd'; - - /** - * A gap occured in the timeline which requires an internal seek - * @event MediaPlayerEvents#GAP_CAUSED_INTERNAL_SEEK - */ - this.GAP_CAUSED_INTERNAL_SEEK = 'gapCausedInternalSeek'; - /** * Dash events are triggered at their respective start points on the timeline. * @event MediaPlayerEvents#EVENT_MODE_ON_START @@ -378,6 +390,23 @@ class MediaPlayerEvents extends EventsBase { * @event MediaPlayerEvents#CONFORMANCE_VIOLATION */ this.CONFORMANCE_VIOLATION = 'conformanceViolation'; + + /** + * Event that is dispatched whenever the player switches to a different representation + * @event MediaPlayerEvents#REPRESENTATION_SWITCH + */ + this.REPRESENTATION_SWITCH = 'representationSwitch'; + + /** + * Event that is dispatched whenever an adaptation set is removed due to all representations not being supported. + * @event MediaPlayerEvents#ADAPTATION_SET_REMOVED_NO_CAPABILITIES + */ + this.ADAPTATION_SET_REMOVED_NO_CAPABILITIES = 'adaptationSetRemovedNoCapabilities'; + /** + * Triggered when a content steering request has completed. + * @event MediaPlayerEvents#CONTENT_STEERING_REQUEST_COMPLETED + */ + this.CONTENT_STEERING_REQUEST_COMPLETED = 'contentSteeringRequestCompleted'; } } diff --git a/src/streaming/MediaPlayerFactory.js b/src/streaming/MediaPlayerFactory.js index 289f7be050..471552aa63 100644 --- a/src/streaming/MediaPlayerFactory.js +++ b/src/streaming/MediaPlayerFactory.js @@ -28,8 +28,8 @@ function MediaPlayerFactory() { let videoID = (video.id || video.name || 'video element'); source = source || [].slice.call(video.querySelectorAll('source')).filter(function (s) { - return s.type == SUPPORTED_MIME_TYPE; - })[0]; + return s.type == SUPPORTED_MIME_TYPE; + })[0]; if (!source && video.src) { source = document.createElement('source'); source.src = video.src; diff --git a/src/streaming/PreBufferSink.js b/src/streaming/PreBufferSink.js index a6960223c6..57ca04af14 100644 --- a/src/streaming/PreBufferSink.js +++ b/src/streaming/PreBufferSink.js @@ -61,7 +61,9 @@ function PreBufferSink(onAppendedCallback) { function append(chunk) { if (chunk.segmentType !== 'InitializationSegment') { //Init segments are stored in the initCache. chunks.push(chunk); - chunks.sort(function (a, b) { return a.start - b.start; }); + chunks.sort(function (a, b) { + return a.start - b.start; + }); outstandingInit = null; } else {//We need to hold an init chunk for when a corresponding media segment is being downloaded when the discharge happens. outstandingInit = chunk; @@ -76,7 +78,7 @@ function PreBufferSink(onAppendedCallback) { } function remove(start, end) { - chunks = chunks.filter( a => !((isNaN(end) || a.start < end) && (isNaN(start) || a.end > start))); //The opposite of the getChunks predicate. + chunks = chunks.filter(a => !((isNaN(end) || a.start < end) && (isNaN(start) || a.end > start))); //The opposite of the getChunks predicate. } //Nothing async, nothing to abort. @@ -89,7 +91,7 @@ function PreBufferSink(onAppendedCallback) { for (let i = 0; i < chunks.length; i++) { let chunk = chunks[i]; if (ranges.length === 0 || chunk.start > ranges[ranges.length - 1].end) { - ranges.push({ start: chunk.start, end: chunk.end }); + ranges.push({start: chunk.start, end: chunk.end}); } else { ranges[ranges.length - 1].end = chunk.end; } @@ -114,10 +116,6 @@ function PreBufferSink(onAppendedCallback) { return timeranges; } - function hasDiscontinuitiesAfter() { - return false; - } - function updateTimestampOffset() { // Nothing to do } @@ -148,7 +146,7 @@ function PreBufferSink(onAppendedCallback) { } function getChunksAt(start, end) { - return chunks.filter( a => ((isNaN(end) || a.start < end) && (isNaN(start) || a.end > start)) ); + return chunks.filter(a => ((isNaN(end) || a.start < end) && (isNaN(start) || a.end > start))); } function waitForUpdateEnd(callback) { @@ -163,7 +161,6 @@ function PreBufferSink(onAppendedCallback) { discharge: discharge, reset: reset, updateTimestampOffset: updateTimestampOffset, - hasDiscontinuitiesAfter: hasDiscontinuitiesAfter, waitForUpdateEnd: waitForUpdateEnd, getBuffer: getBuffer }; diff --git a/src/streaming/SourceBufferSink.js b/src/streaming/SourceBufferSink.js index 2732d7dad9..4d53569fcb 100644 --- a/src/streaming/SourceBufferSink.js +++ b/src/streaming/SourceBufferSink.js @@ -30,38 +30,85 @@ */ import Debug from '../core/Debug'; import DashJSError from './vo/DashJSError'; -import EventBus from '../core/EventBus'; -import Events from '../core/events/Events'; import FactoryMaker from '../core/FactoryMaker'; -import TextController from './text/TextController'; import Errors from '../core/errors/Errors'; +import Settings from '../core/Settings'; +import constants from './constants/Constants'; +import {HTTPRequest} from './vo/metrics/HTTPRequest'; +import Events from '../core/events/Events'; -const MAX_ALLOWED_DISCONTINUITY = 0.1; // 100 milliseconds +const APPEND_WINDOW_START_OFFSET = 0.1; +const APPEND_WINDOW_END_OFFSET = 0.01; /** * @class SourceBufferSink * @ignore * @implements FragmentSink */ -function SourceBufferSink(mediaSource, mediaInfo, onAppendedCallback, oldBuffer) { + +const CHECK_INTERVAL = 50; + +function SourceBufferSink(config) { const context = this.context; - const eventBus = EventBus(context).getInstance(); + const settings = Settings(context).getInstance(); + const textController = config.textController; + const eventBus = config.eventBus; let instance, type, logger, buffer, - isAppendingInProgress, + mediaInfo, intervalId; let callbacks = []; let appendQueue = []; - let onAppended = onAppendedCallback; + let isAppendingInProgress = false; + let mediaSource = config.mediaSource; + let lastRequestAppended = null; function setup() { logger = Debug(context).getInstance().getLogger(instance); - isAppendingInProgress = false; + } + + function initializeForStreamSwitch(mInfo, selectedRepresentation, oldSourceBufferSink) { + mediaInfo = mInfo; + type = mediaInfo.type; + const codec = mediaInfo.codec; + + _copyPreviousSinkData(oldSourceBufferSink); + _addEventListeners(); + + const promises = []; + + promises.push(_abortBeforeAppend()); + promises.push(updateAppendWindow(mediaInfo.streamInfo)); + promises.push(changeType(codec)); + + if (selectedRepresentation && selectedRepresentation.MSETimeOffset !== undefined) { + promises.push(updateTimestampOffset(selectedRepresentation.MSETimeOffset)); + } + + return Promise.all(promises); + } + + function changeType(codec) { + return new Promise((resolve) => { + _waitForUpdateEnd(() => { + if (buffer.changeType) { + buffer.changeType(codec); + } + resolve(); + }); + }); + } + + function _copyPreviousSinkData(oldSourceBufferSink) { + buffer = oldSourceBufferSink.getBuffer(); + } + function initializeForFirstUse(streamInfo, mInfo, selectedRepresentation) { + mediaInfo = mInfo; type = mediaInfo.type; const codec = mediaInfo.codec; try { @@ -70,40 +117,52 @@ function SourceBufferSink(mediaSource, mediaInfo, onAppendedCallback, oldBuffer) // - currently no browser does, so check for it and use our own // implementation. The same is true for codecs="wvtt". if (codec.match(/application\/mp4;\s*codecs="(stpp|wvtt).*"/i)) { - throw new Error('not really supported'); - } - buffer = oldBuffer ? oldBuffer : mediaSource.addSourceBuffer(codec); - if (buffer.changeType && oldBuffer) { - logger.debug('Doing period transition with changeType'); - buffer.changeType(codec); + return _initializeForText(streamInfo); } - updateAppendWindow(); + buffer = mediaSource.addSourceBuffer(codec); - const CHECK_INTERVAL = 50; - // use updateend event if possible - if (typeof buffer.addEventListener === 'function') { - try { - buffer.addEventListener('updateend', updateEndHandler, false); - buffer.addEventListener('error', errHandler, false); - buffer.addEventListener('abort', errHandler, false); + _addEventListeners(); - } catch (err) { - // use setInterval to periodically check if updating has been completed - intervalId = setInterval(checkIsUpdateEnded, CHECK_INTERVAL); - } - } else { - // use setInterval to periodically check if updating has been completed - intervalId = setInterval(checkIsUpdateEnded, CHECK_INTERVAL); + const promises = []; + + promises.push(updateAppendWindow(mediaInfo.streamInfo)); + + if (selectedRepresentation && selectedRepresentation.MSETimeOffset !== undefined) { + promises.push(updateTimestampOffset(selectedRepresentation.MSETimeOffset)); } - } catch (ex) { + + return Promise.all(promises); + + } catch (e) { // Note that in the following, the quotes are open to allow for extra text after stpp and wvtt - if ((mediaInfo.isText) || (codec.indexOf('codecs="stpp') !== -1) || (codec.indexOf('codecs="wvtt') !== -1)) { - const textController = TextController(context).getInstance(); - buffer = textController.getTextSourceBuffer(); - } else { - throw ex; + if ((mediaInfo.type == constants.TEXT && !mediaInfo.isFragmented) || (codec.indexOf('codecs="stpp') !== -1) || (codec.indexOf('codecs="vtt') !== -1)) { + return _initializeForText(streamInfo); + } + return Promise.reject(e); + } + } + + function _initializeForText(streamInfo) { + buffer = textController.getTextSourceBuffer(streamInfo); + return Promise.resolve(); + } + + function _addEventListeners() { + // use updateend event if possible + if (typeof buffer.addEventListener === 'function') { + try { + buffer.addEventListener('updateend', _updateEndHandler, false); + buffer.addEventListener('error', _errHandler, false); + buffer.addEventListener('abort', _errHandler, false); + + } catch (err) { + // use setInterval to periodically check if updating has been completed + intervalId = setInterval(_updateEndHandler, CHECK_INTERVAL); } + } else { + // use setInterval to periodically check if updating has been completed + intervalId = setInterval(_updateEndHandler, CHECK_INTERVAL); } } @@ -111,30 +170,98 @@ function SourceBufferSink(mediaSource, mediaInfo, onAppendedCallback, oldBuffer) return type; } - function reset(keepBuffer) { - if (buffer) { + function _removeEventListeners() { + try { if (typeof buffer.removeEventListener === 'function') { - buffer.removeEventListener('updateend', updateEndHandler, false); - buffer.removeEventListener('error', errHandler, false); - buffer.removeEventListener('abort', errHandler, false); + buffer.removeEventListener('updateend', _updateEndHandler, false); + buffer.removeEventListener('error', _errHandler, false); + buffer.removeEventListener('abort', _errHandler, false); } clearInterval(intervalId); - callbacks = []; - if (!keepBuffer) { + } catch (e) { + logger.error(e); + } + } + + function updateAppendWindow(sInfo) { + return new Promise((resolve) => { + + if (!buffer || !settings.get().streaming.buffer.useAppendWindow) { + resolve(); + return; + } + + _waitForUpdateEnd(() => { + try { + if (!buffer) { + resolve(); + return; + } + + let appendWindowEnd = mediaSource.duration; + let appendWindowStart = 0; + if (sInfo && !isNaN(sInfo.start) && !isNaN(sInfo.duration) && isFinite(sInfo.duration)) { + appendWindowEnd = sInfo.start + sInfo.duration; + } + if (sInfo && !isNaN(sInfo.start)) { + appendWindowStart = sInfo.start; + } + if (buffer.appendWindowEnd !== appendWindowEnd || buffer.appendWindowStart !== appendWindowStart) { + buffer.appendWindowStart = 0; + buffer.appendWindowEnd = appendWindowEnd + APPEND_WINDOW_END_OFFSET; + buffer.appendWindowStart = Math.max(appendWindowStart - APPEND_WINDOW_START_OFFSET, 0); + logger.debug(`Updated append window for ${mediaInfo.type}. Set start to ${buffer.appendWindowStart} and end to ${buffer.appendWindowEnd}`); + } + + resolve(); + } catch (e) { + logger.warn(`Failed to set append window`); + resolve(); + } + }); + }); + } + + function updateTimestampOffset(MSETimeOffset) { + return new Promise((resolve) => { + + if (!buffer) { + resolve(); + return; + } + + _waitForUpdateEnd(() => { try { - if (!buffer.getClassName || buffer.getClassName() !== 'TextSourceBuffer') { - logger.debug(`Removing sourcebuffer from media source`); - mediaSource.removeSourceBuffer(buffer); + if (buffer.timestampOffset !== MSETimeOffset && !isNaN(MSETimeOffset)) { + buffer.timestampOffset = MSETimeOffset; + logger.debug(`Set MSE timestamp offset to ${MSETimeOffset}`); } + resolve(); } catch (e) { - logger.error('Failed to remove source buffer from media source.'); + resolve(); + } + }); + }); + } + + + function reset() { + if (buffer) { + try { + callbacks = []; + _removeEventListeners(); + isAppendingInProgress = false; + appendQueue = []; + if (!buffer.getClassName || buffer.getClassName() !== 'TextSourceBuffer') { + logger.debug(`Removing sourcebuffer from media source`); + mediaSource.removeSourceBuffer(buffer); } - buffer = null; + } catch (e) { + } - isAppendingInProgress = false; + buffer = null; } - appendQueue = []; - onAppended = null; + lastRequestAppended = null; } function getBuffer() { @@ -150,244 +277,205 @@ function SourceBufferSink(mediaSource, mediaInfo, onAppendedCallback, oldBuffer) } } - function hasDiscontinuitiesAfter(time) { - try { - const ranges = getAllBufferRanges(); - if (ranges && ranges.length > 1) { - for (let i = 0, len = ranges.length; i < len; i++) { - if (i > 0) { - if (time < ranges.start(i) && ranges.start(i) > ranges.end(i - 1) + MAX_ALLOWED_DISCONTINUITY) { - return true; - } - } - } + function append(chunk, request = null) { + return new Promise((resolve, reject) => { + if (!chunk) { + reject({ + chunk: chunk, + error: new DashJSError(Errors.APPEND_ERROR_CODE, Errors.APPEND_ERROR_MESSAGE) + }); + return; } - } catch (e) { - logger.error('hasDiscontinuities exception: ' + e.message); - } - return false; - } - - function append(chunk) { - if (!chunk) { - onAppended({ - chunk: chunk, - error: new DashJSError(Errors.APPEND_ERROR_CODE, Errors.APPEND_ERROR_MESSAGE) - }); - return; - } - appendQueue.push(chunk); - if (!isAppendingInProgress) { - waitForUpdateEnd(appendNextInQueue.bind(this)); - } + appendQueue.push({ data: chunk, promise: { resolve, reject }, request }); + _waitForUpdateEnd(_appendNextInQueue.bind(this)); + }); } - function updateTimestampOffset(MSETimeOffset) { - if (buffer.timestampOffset !== MSETimeOffset && !isNaN(MSETimeOffset)) { - waitForUpdateEnd(() => { - if (MSETimeOffset < 0) { - MSETimeOffset += 0.001; + function _abortBeforeAppend() { + return new Promise((resolve) => { + _waitForUpdateEnd(() => { + // Save the append window, which is reset on abort(). + const appendWindowStart = buffer.appendWindowStart; + const appendWindowEnd = buffer.appendWindowEnd; + + if (buffer) { + buffer.abort(); + buffer.appendWindowStart = appendWindowStart; + buffer.appendWindowEnd = appendWindowEnd; } - buffer.timestampOffset = MSETimeOffset; + resolve(); }); - } + }); } - function updateAppendWindow(sInfo) { - if (!buffer) { - return; - } - waitForUpdateEnd(() => { - try { - let appendWindowEnd = mediaSource.duration; - let appendWindowStart = 0; - if (sInfo && !isNaN(sInfo.start) && !isNaN(sInfo.duration) && isFinite(sInfo.duration)) { - appendWindowEnd = sInfo.start + sInfo.duration; - } - if (sInfo && !isNaN(sInfo.start)) { - appendWindowStart = sInfo.start; - } - buffer.appendWindowStart = 0; - buffer.appendWindowEnd = appendWindowEnd; - buffer.appendWindowStart = appendWindowStart; - logger.debug(`Updated append window. Set start to ${buffer.appendWindowStart} and end to ${buffer.appendWindowEnd}`); - } catch (e) { - logger.warn(`Failed to set append window`); + function remove(range) { + return new Promise((resolve, reject) => { + const start = range.start; + const end = range.end; + + // make sure that the given time range is correct. Otherwise we will get InvalidAccessError + if (!((start >= 0) && (end > start))) { + resolve(); + return; } - }); - } - function remove(start, end, forceRemoval) { - const sourceBufferSink = this; - // make sure that the given time range is correct. Otherwise we will get InvalidAccessError - waitForUpdateEnd(function () { - try { - if ((start >= 0) && (end > start) && (forceRemoval || mediaSource.readyState !== 'ended')) { + _waitForUpdateEnd(function () { + try { buffer.remove(start, end); - } - // updating is in progress, we should wait for it to complete before signaling that this operation is done - waitForUpdateEnd(function () { - eventBus.trigger(Events.SOURCEBUFFER_REMOVE_COMPLETED, { - buffer: sourceBufferSink, + // updating is in progress, we should wait for it to complete before signaling that this operation is done + _waitForUpdateEnd(function () { + resolve({ + from: start, + to: end, + unintended: false + }); + if (range.resolve) { + range.resolve(); + } + }); + } catch (err) { + reject({ from: start, to: end, - unintended: false + unintended: false, + error: new DashJSError(Errors.REMOVE_ERROR_CODE, Errors.REMOVE_ERROR_MESSAGE) }); - }); - } catch (err) { - eventBus.trigger(Events.SOURCEBUFFER_REMOVE_COMPLETED, { - buffer: sourceBufferSink, - from: start, - to: end, - unintended: false, - error: new DashJSError(err.code, err.message) - }); - } + if (range.reject) { + range.reject(err); + } + } + }); }); } - function appendNextInQueue() { - const sourceBufferSink = this; + function _appendNextInQueue() { + if (isAppendingInProgress) { + return; + } if (appendQueue.length > 0) { isAppendingInProgress = true; const nextChunk = appendQueue[0]; appendQueue.splice(0, 1); - let oldRanges = []; + const afterSuccess = function () { - // Safari sometimes drops a portion of a buffer after appending. Handle these situations here - const newRanges = getAllBufferRanges(); - checkBufferGapsAfterAppend(sourceBufferSink, oldRanges, newRanges, nextChunk); + isAppendingInProgress = false; if (appendQueue.length > 0) { - appendNextInQueue.call(this); - } else { - isAppendingInProgress = false; - if (onAppended) { - onAppended({ - chunk: nextChunk - }); - } + _appendNextInQueue.call(this); + } + // Init segments are cached. In any other case we dont need the chunk bytes anymore and can free the memory + if (nextChunk && nextChunk.data && nextChunk.data.segmentType && nextChunk.data.segmentType !== HTTPRequest.INIT_SEGMENT_TYPE) { + delete nextChunk.data.bytes; } + nextChunk.promise.resolve({ chunk: nextChunk.data }); }; try { - if (nextChunk.bytes.length === 0) { + lastRequestAppended = nextChunk.request; + if (nextChunk.data.bytes.byteLength === 0) { afterSuccess.call(this); } else { - oldRanges = getAllBufferRanges(); if (buffer.appendBuffer) { - buffer.appendBuffer(nextChunk.bytes); + buffer.appendBuffer(nextChunk.data.bytes); } else { - buffer.append(nextChunk.bytes, nextChunk); + buffer.append(nextChunk.data.bytes, nextChunk.data); } // updating is in progress, we should wait for it to complete before signaling that this operation is done - waitForUpdateEnd(afterSuccess.bind(this)); + _waitForUpdateEnd(afterSuccess.bind(this)); } } catch (err) { logger.fatal('SourceBuffer append failed "' + err + '"'); if (appendQueue.length > 0) { - appendNextInQueue(); + _appendNextInQueue(); } else { isAppendingInProgress = false; } - if (onAppended) { - onAppended({ - chunk: nextChunk, - error: new DashJSError(err.code, err.message) - }); - } - } - } - } - - function checkBufferGapsAfterAppend(buffer, oldRanges, newRanges, chunk) { - if (oldRanges && oldRanges.length > 0 && oldRanges.length < newRanges.length && - isChunkAlignedWithRange(oldRanges, chunk)) { - // A split in the range was created while appending - eventBus.trigger(Events.SOURCEBUFFER_REMOVE_COMPLETED, { - buffer: buffer, - from: newRanges.end(newRanges.length - 2), - to: newRanges.start(newRanges.length - 1), - unintended: true - }); - } - } - - function isChunkAlignedWithRange(oldRanges, chunk) { - for (let i = 0; i < oldRanges.length; i++) { - const start = Math.round(oldRanges.start(i)); - const end = Math.round(oldRanges.end(i)); - if (end === chunk.start || start === chunk.end || (chunk.start >= start && chunk.end <= end)) { - return true; + delete nextChunk.data.bytes; + nextChunk.promise.reject({ chunk: nextChunk.data, error: new DashJSError(err.code, err.message) }); } } - return false; } function abort() { - try { - if (mediaSource.readyState === 'open') { - buffer.abort(); - } else if (buffer.setTextTrack && mediaSource.readyState === 'ended') { - buffer.abort(); //The cues need to be removed from the TextSourceBuffer via a call to abort() + return new Promise((resolve) => { + try { + appendQueue = []; + if (mediaSource.readyState === 'open') { + _waitForUpdateEnd(() => { + try { + if (buffer) { + buffer.abort(); + } + resolve(); + } catch (e) { + resolve(); + } + }); + } else if (buffer && buffer.setTextTrack && mediaSource.readyState === 'ended') { + buffer.abort(); //The cues need to be removed from the TextSourceBuffer via a call to abort() + resolve(); + } else { + resolve(); + } + } catch (e) { + resolve(); } - } catch (ex) { - logger.error('SourceBuffer append abort failed: "' + ex + '"'); - } - appendQueue = []; + }); } - function executeCallback() { + function _executeCallback() { if (callbacks.length > 0) { - const cb = callbacks.shift(); - if (buffer.updating) { - waitForUpdateEnd(cb); - } else { + if (!buffer.updating) { + const cb = callbacks.shift(); cb(); // Try to execute next callback if still not updating - executeCallback(); + _executeCallback(); } } } - function checkIsUpdateEnded() { + function _updateEndHandler() { // if updating is still in progress do nothing and wait for the next check again. - if (buffer.updating) return; + if (buffer.updating) { + return; + } + // updating is completed, now we can stop checking and resolve the promise - executeCallback(); + _executeCallback(); } - function updateEndHandler() { - if (buffer.updating) return; - - executeCallback(); + function _errHandler(e) { + const error = e.target || {}; + _triggerEvent(Events.SOURCE_BUFFER_ERROR, { error, lastRequestAppended }) } - function errHandler() { - logger.error('SourceBufferSink error'); + function _triggerEvent(eventType, data) { + let payload = data || {}; + eventBus.trigger(eventType, payload, { streamId: mediaInfo.streamInfo.id, mediaType: type }); } - function waitForUpdateEnd(callback) { + function _waitForUpdateEnd(callback) { callbacks.push(callback); if (!buffer.updating) { - executeCallback(); + _executeCallback(); } } instance = { - getType: getType, - getAllBufferRanges: getAllBufferRanges, - getBuffer: getBuffer, - append: append, - remove: remove, - abort: abort, - reset: reset, - updateTimestampOffset: updateTimestampOffset, - hasDiscontinuitiesAfter: hasDiscontinuitiesAfter, - waitForUpdateEnd: waitForUpdateEnd, - updateAppendWindow + getType, + getAllBufferRanges, + getBuffer, + append, + remove, + abort, + reset, + updateTimestampOffset, + initializeForStreamSwitch, + initializeForFirstUse, + updateAppendWindow, + changeType }; setup(); diff --git a/src/streaming/Stream.js b/src/streaming/Stream.js index c07e21de03..5e6d5425ca 100644 --- a/src/streaming/Stream.js +++ b/src/streaming/Stream.js @@ -41,6 +41,11 @@ import FactoryMaker from '../core/FactoryMaker'; import DashJSError from './vo/DashJSError'; import BoxParser from './utils/BoxParser'; import URLUtils from './utils/URLUtils'; +import BlacklistController from './controllers/BlacklistController'; + + +const MEDIA_TYPES = [Constants.VIDEO, Constants.AUDIO, Constants.TEXT, Constants.MUXED, Constants.IMAGE]; + function Stream(config) { @@ -51,79 +56,105 @@ function Stream(config) { const manifestModel = config.manifestModel; const mediaPlayerModel = config.mediaPlayerModel; + const dashMetrics = config.dashMetrics; const manifestUpdater = config.manifestUpdater; const adapter = config.adapter; + const timelineConverter = config.timelineConverter; const capabilities = config.capabilities; - const capabilitiesFilter = config.capabilitiesFilter; const errHandler = config.errHandler; - const timelineConverter = config.timelineConverter; - const dashMetrics = config.dashMetrics; const abrController = config.abrController; const playbackController = config.playbackController; const eventController = config.eventController; const mediaController = config.mediaController; - const textController = config.textController; const protectionController = config.protectionController; + const textController = config.textController; const videoModel = config.videoModel; - const settings = config.settings; let streamInfo = config.streamInfo; + const settings = config.settings; + let instance, logger, streamProcessors, - isStreamInitialized, - isStreamActivated, - isMediaInitialized, + isInitialized, + isActive, + hasFinishedBuffering, hasVideoTrack, hasAudioTrack, updateError, isUpdating, fragmentController, thumbnailController, + segmentBlacklistController, preloaded, boxParser, - preloadingScheduled, debug, isEndedEventSignaled, - trackChangedEvent; + trackChangedEvents; + /** + * Setup the stream + */ function setup() { - debug = Debug(context).getInstance(); - logger = debug.getLogger(instance); - resetInitialSettings(); + try { + debug = Debug(context).getInstance(); + logger = debug.getLogger(instance); + resetInitialSettings(); - boxParser = BoxParser(context).getInstance(); + boxParser = BoxParser(context).getInstance(); - fragmentController = FragmentController(context).create({ - streamInfo: streamInfo, - mediaPlayerModel: mediaPlayerModel, - dashMetrics: dashMetrics, - errHandler: errHandler, - settings: settings, - boxParser: boxParser, - dashConstants: DashConstants, - urlUtils: urlUtils - }); + segmentBlacklistController = BlacklistController(context).create({ + updateEventName: Events.SEGMENT_LOCATION_BLACKLIST_CHANGED, + addBlacklistEventName: Events.SEGMENT_LOCATION_BLACKLIST_ADD + }); + + fragmentController = FragmentController(context).create({ + streamInfo: streamInfo, + mediaPlayerModel: mediaPlayerModel, + dashMetrics: dashMetrics, + errHandler: errHandler, + settings: settings, + boxParser: boxParser, + dashConstants: DashConstants, + urlUtils: urlUtils + }); + + } catch (e) { + throw e; + } } + /** + * Initialize the events + */ function initialize() { registerEvents(); registerProtectionEvents(); + textController.initializeForStream(streamInfo); eventBus.trigger(Events.STREAM_UPDATED, { streamInfo: streamInfo }); } + /** + * Register the streaming events + */ function registerEvents() { eventBus.on(Events.BUFFERING_COMPLETED, onBufferingCompleted, instance); eventBus.on(Events.DATA_UPDATE_COMPLETED, onDataUpdateCompleted, instance); eventBus.on(Events.INBAND_EVENTS, onInbandEvents, instance); } + /** + * Unregister the streaming events + */ function unRegisterEvents() { eventBus.off(Events.DATA_UPDATE_COMPLETED, onDataUpdateCompleted, instance); eventBus.off(Events.BUFFERING_COMPLETED, onBufferingCompleted, instance); eventBus.off(Events.INBAND_EVENTS, onInbandEvents, instance); } + /** + * Register the protection events + */ function registerProtectionEvents() { if (protectionController) { eventBus.on(Events.KEY_ERROR, onProtectionError, instance); @@ -135,6 +166,9 @@ function Stream(config) { } } + /** + * Unregister the protection events + */ function unRegisterProtectionEvents() { if (protectionController) { eventBus.off(Events.KEY_ERROR, onProtectionError, instance); @@ -146,6 +180,10 @@ function Stream(config) { } } + /** + * Returns the stream id + * @return {*|null} + */ function getStreamId() { return streamInfo ? streamInfo.id : null; } @@ -153,27 +191,317 @@ function Stream(config) { /** * Activates Stream by re-initializing some of its components * @param {MediaSource} mediaSource + * @param {array} previousBufferSinks * @memberof Stream# - * @param {SourceBuffer} previousBuffers */ - function activate(mediaSource, previousBuffers) { - if (!isStreamActivated) { - let result; - eventBus.on(Events.CURRENT_TRACK_CHANGED, onCurrentTrackChanged, instance); - if (!getPreloaded()) { - result = initializeMedia(mediaSource, previousBuffers); - } else { - initializeAfterPreload(); - result = previousBuffers; + function activate(mediaSource, previousBufferSinks) { + return new Promise((resolve, reject) => { + if (isActive) { + resolve(previousBufferSinks); + return; + } + + if (getPreloaded()) { + isActive = true; + eventBus.trigger(Events.STREAM_ACTIVATED, { + streamInfo + }); + resolve(previousBufferSinks); + return; + } + + + _initializeMedia(mediaSource, previousBufferSinks) + .then((bufferSinks) => { + isActive = true; + eventBus.trigger(Events.STREAM_ACTIVATED, { + streamInfo + }); + resolve(bufferSinks); + }) + .catch((e) => { + reject(e); + }); + }); + } + + /** + * + * @param {object} mediaSource + * @param {array} previousBufferSinks + * @return {Promise} + * @private + */ + function _initializeMedia(mediaSource, previousBufferSinks) { + return _commonMediaInitialization(mediaSource, previousBufferSinks); + } + + function startPreloading(mediaSource, previousBuffers) { + return new Promise((resolve, reject) => { + + if (getPreloaded()) { + reject(); + return; } - isStreamActivated = true; - return result; + + logger.info(`[startPreloading] Preloading next stream with id ${getId()}`); + setPreloaded(true); + + _commonMediaInitialization(mediaSource, previousBuffers) + .then(() => { + for (let i = 0; i < streamProcessors.length && streamProcessors[i]; i++) { + streamProcessors[i].setExplicitBufferingTime(getStartTime()); + streamProcessors[i].getScheduleController().startScheduleTimer(); + } + resolve(); + }) + .catch(() => { + setPreloaded(false); + reject(); + }); + }); + } + + /** + * + * @param {object} mediaSource + * @param {array} previousBufferSinks + * @return {Promise} + * @private + */ + function _commonMediaInitialization(mediaSource, previousBufferSinks) { + return new Promise((resolve, reject) => { + checkConfig(); + + isUpdating = true; + _addInlineEvents(); + + + let element = videoModel.getElement(); + + MEDIA_TYPES.forEach((mediaType) => { + if (mediaType !== Constants.VIDEO || (!element || (element && (/^VIDEO$/i).test(element.nodeName)))) { + _initializeMediaForType(mediaType, mediaSource); + } + }); + + _createBufferSinks(previousBufferSinks) + .then((bufferSinks) => { + isUpdating = false; + + if (streamProcessors.length === 0) { + const msg = 'No streams to play.'; + errHandler.error(new DashJSError(Errors.MANIFEST_ERROR_ID_NOSTREAMS_CODE, msg, manifestModel.getValue())); + logger.fatal(msg); + } else { + _checkIfInitializationCompleted(); + } + + // All mediaInfos for texttracks are added to the TextSourceBuffer by now. We can start creating the tracks + textController.createTracks(streamInfo); + + resolve(bufferSinks); + }) + .catch((e) => { + reject(e); + }); + }); + + } + + + /** + * Initialize for a given media type. Creates a corresponding StreamProcessor + * @param {string} type + * @param {object} mediaSource + * @private + */ + function _initializeMediaForType(type, mediaSource) { + let allMediaForType = adapter.getAllMediaInfoForType(streamInfo, type); + let embeddedMediaInfos = []; + + let mediaInfo = null; + let initialMediaInfo; + + if (!allMediaForType || allMediaForType.length === 0) { + logger.info('No ' + type + ' data.'); + return; + } + + if (type === Constants.VIDEO) { + hasVideoTrack = true; + } + + if (type === Constants.AUDIO) { + hasAudioTrack = true; } - return previousBuffers; + + for (let i = 0, ln = allMediaForType.length; i < ln; i++) { + mediaInfo = allMediaForType[i]; + + if (type === Constants.TEXT && !!mediaInfo.isEmbedded) { + textController.addEmbeddedTrack(streamInfo, mediaInfo); + embeddedMediaInfos.push(mediaInfo); + } + if (_isMediaSupported(mediaInfo)) { + mediaController.addTrack(mediaInfo); + } + } + + if (embeddedMediaInfos.length > 0) { + mediaController.setInitialMediaSettingsForType(type, streamInfo); + textController.setInitialSettings(mediaController.getInitialSettings(type)); + textController.addMediaInfosToBuffer(streamInfo, type, embeddedMediaInfos); + } + + // Filter out embedded text track before creating StreamProcessor + allMediaForType = allMediaForType.filter(mediaInfo => { + return !mediaInfo.isEmbedded; + }); + if (allMediaForType.length === 0) { + return; + } + + if (type === Constants.IMAGE) { + thumbnailController = ThumbnailController(context).create({ + streamInfo: streamInfo, + adapter: adapter, + baseURLController: config.baseURLController, + timelineConverter: config.timelineConverter, + debug: debug, + eventBus: eventBus, + events: Events, + dashConstants: DashConstants, + dashMetrics: config.dashMetrics, + segmentBaseController: config.segmentBaseController + }); + thumbnailController.initialize(); + return; + } + + eventBus.trigger(Events.STREAM_INITIALIZING, { + streamInfo: streamInfo, + mediaInfo: mediaInfo + }); + + mediaController.setInitialMediaSettingsForType(type, streamInfo); + + let streamProcessor = _createStreamProcessor(allMediaForType, mediaSource); + + initialMediaInfo = mediaController.getCurrentTrackFor(type, streamInfo.id); + + if (initialMediaInfo) { + abrController.updateTopQualityIndex(initialMediaInfo); + // In case of mixed fragmented and embedded text tracks, check if initial selected text track is not an embedded track + streamProcessor.selectMediaInfo((type !== Constants.TEXT || !initialMediaInfo.isEmbedded) ? initialMediaInfo : allMediaForType[0]); + } + + } + + function _isMediaSupported(mediaInfo) { + const type = mediaInfo ? mediaInfo.type : null; + let msg; + + if (type === Constants.MUXED) { + msg = 'Multiplexed representations are intentionally not supported, as they are not compliant with the DASH-AVC/264 guidelines'; + logger.fatal(msg); + errHandler.error(new DashJSError(Errors.MANIFEST_ERROR_ID_MULTIPLEXED_CODE, msg, manifestModel.getValue())); + return false; + } + + if (type === Constants.TEXT || type === Constants.IMAGE) { + return true; + } + + if (!!mediaInfo.contentProtection && !capabilities.supportsEncryptedMedia()) { + errHandler.error(new DashJSError(Errors.CAPABILITY_MEDIAKEYS_ERROR_CODE, Errors.CAPABILITY_MEDIAKEYS_ERROR_MESSAGE)); + return false; + } + + return true; + } + + /** + * Creates the StreamProcessor for a given media type. + * @param {object} initialMediaInfo + * @param {array} allMediaForType + * @param {object} mediaSource + * @private + */ + function _createStreamProcessor(allMediaForType, mediaSource) { + + const mediaInfo = (allMediaForType && allMediaForType.length > 0) ? allMediaForType[0] : null; + let fragmentModel = fragmentController.getModel(mediaInfo ? mediaInfo.type : null); + const type = mediaInfo ? mediaInfo.type : null; + const mimeType = mediaInfo ? mediaInfo.mimeType : null; + const isFragmented = mediaInfo ? mediaInfo.isFragmented : null; + + let streamProcessor = StreamProcessor(context).create({ + streamInfo, + type, + mimeType, + timelineConverter, + adapter, + manifestModel, + mediaPlayerModel, + fragmentModel, + dashMetrics: config.dashMetrics, + baseURLController: config.baseURLController, + segmentBaseController: config.segmentBaseController, + abrController, + playbackController, + mediaController, + textController, + errHandler, + settings, + boxParser, + segmentBlacklistController + }); + + streamProcessor.initialize(mediaSource, hasVideoTrack, isFragmented); + streamProcessors.push(streamProcessor); + + for (let i = 0; i < allMediaForType.length; i++) { + streamProcessor.addMediaInfo(allMediaForType[i]); + } + + if (type === Constants.TEXT) { + textController.addMediaInfosToBuffer(streamInfo, type, allMediaForType, fragmentModel); + } + + return streamProcessor; } /** - * Partially resets some of the Stream elements + * Creates the SourceBufferSink objects for all StreamProcessors + * @param {array} previousBuffersSinks + * @return {Promise} + * @private + */ + function _createBufferSinks(previousBuffersSinks) { + return new Promise((resolve) => { + const buffers = {}; + const promises = streamProcessors.map((sp) => { + return sp.createBufferSinks(previousBuffersSinks); + }); + + Promise.all(promises) + .then((bufferSinks) => { + bufferSinks.forEach((sink) => { + if (sink) { + buffers[sink.getType()] = sink; + } + }); + resolve(buffers); + }) + .catch(() => { + resolve(buffers); + }); + }); + } + + /** + * Partially resets some of the Stream elements. This function is called when preloading of streams is canceled or a stream switch occurs. * @memberof Stream# * @param {boolean} keepBuffers */ @@ -182,23 +510,28 @@ function Stream(config) { const errored = false; for (let i = 0; i < ln; i++) { let fragmentModel = streamProcessors[i].getFragmentModel(); - fragmentModel.removeExecutedRequestsBeforeTime(getStartTime() + getDuration()); + fragmentModel.abortRequests(); + fragmentModel.resetInitialSettings(); streamProcessors[i].reset(errored, keepBuffers); } + if (textController) { + textController.deactivateStream(streamInfo); + } streamProcessors = []; - isStreamActivated = false; - isMediaInitialized = false; + isActive = false; + hasFinishedBuffering = false; setPreloaded(false); - eventBus.off(Events.CURRENT_TRACK_CHANGED, onCurrentTrackChanged, instance); + setIsEndedEventSignaled(false); + eventBus.trigger(Events.STREAM_DEACTIVATED, { streamInfo }); } - function isActive() { - return isStreamActivated; + function getIsActive() { + return isActive; } function setMediaSource(mediaSource) { for (let i = 0; i < streamProcessors.length;) { - if (isMediaSupported(streamProcessors[i].getMediaInfo())) { + if (_isMediaSupported(streamProcessors[i].getMediaInfo())) { streamProcessors[i].setMediaSource(mediaSource); i++; } else { @@ -207,12 +540,6 @@ function Stream(config) { } } - for (let i = 0; i < streamProcessors.length; i++) { - //Adding of new tracks to a stream processor isn't guaranteed by the spec after the METADATA_LOADED state - //so do this after the buffers are created above. - streamProcessors[i].dischargePreBuffer(); - } - if (streamProcessors.length === 0) { const msg = 'No streams to play.'; errHandler.error(new DashJSError(Errors.MANIFEST_ERROR_ID_NOSTREAMS_CODE, msg + 'nostreams', manifestModel.getValue())); @@ -220,37 +547,41 @@ function Stream(config) { } } - function resetInitialSettings() { - deactivate(); - isStreamInitialized = false; + function resetInitialSettings(keepBuffers) { + deactivate(keepBuffers); + isInitialized = false; hasVideoTrack = false; hasAudioTrack = false; updateError = {}; isUpdating = false; - preloadingScheduled = false; isEndedEventSignaled = false; + trackChangedEvents = []; } - function reset() { - - if (playbackController) { - playbackController.pause(); - } + function reset(keepBuffers) { if (fragmentController) { fragmentController.reset(); fragmentController = null; } - streamInfo = null; + if (abrController && streamInfo) { + abrController.clearDataForStream(streamInfo.id); + } + + if (segmentBlacklistController) { + segmentBlacklistController.reset(); + segmentBlacklistController = null; + } - resetInitialSettings(); + resetInitialSettings(keepBuffers); + + streamInfo = null; unRegisterEvents(); unRegisterProtectionEvents(); - setPreloaded(false); } function getDuration() { @@ -269,27 +600,6 @@ function Stream(config) { return streamInfo ? streamInfo.start : NaN; } - function getPreloadingScheduled() { - return preloadingScheduled; - } - - function setPreloadingScheduled(value) { - preloadingScheduled = value; - } - - function getLiveStartTime() { - if (!streamInfo.manifestInfo.isDynamic) return NaN; - // Get live start time of the video stream (1st in array of streams) - // or audio if no video stream - for (let i = 0; i < streamProcessors.length; i++) { - if (streamProcessors[i].getType() === Constants.AUDIO || - streamProcessors[i].getType() === Constants.VIDEO) { - return streamProcessors[i].getLiveStartTime(); - } - } - return NaN; - } - function getId() { return streamInfo ? streamInfo.id : null; } @@ -311,7 +621,7 @@ function Stream(config) { } function checkConfig() { - if (!videoModel || !abrController || !abrController.hasOwnProperty('getBitrateList') || !adapter || !adapter.hasOwnProperty('getAllMediaInfoForType') || !adapter.hasOwnProperty('getEventsFor')) { + if (!videoModel || !abrController) { throw new Error(Constants.MISSING_CONFIG_ERROR); } } @@ -337,41 +647,16 @@ function Stream(config) { if (event.error) { errHandler.error(event.error); logger.fatal(event.error.message); - reset(); } } - function isMediaSupported(mediaInfo) { - const type = mediaInfo ? mediaInfo.type : null; - let codec, - msg; - - if (type === Constants.MUXED) { - msg = 'Multiplexed representations are intentionally not supported, as they are not compliant with the DASH-AVC/264 guidelines'; - logger.fatal(msg); - errHandler.error(new DashJSError(Errors.MANIFEST_ERROR_ID_MULTIPLEXED_CODE, msg, manifestModel.getValue())); - return false; - } - - if (type === Constants.TEXT || type === Constants.FRAGMENTED_TEXT || type === Constants.EMBEDDED_TEXT || type === Constants.IMAGE) { - return true; - } - codec = mediaInfo.codec; - logger.debug(type + ' codec: ' + codec); - - if (!!mediaInfo.contentProtection && !capabilities.supportsEncryptedMedia()) { - errHandler.error(new DashJSError(Errors.CAPABILITY_MEDIAKEYS_ERROR_CODE, Errors.CAPABILITY_MEDIAKEYS_ERROR_MESSAGE)); - } else if (!capabilities.supportsCodec(codec)) { - msg = type + 'Codec (' + codec + ') is not supported.'; - logger.error(msg); - return false; + function prepareTrackChange(e) { + if (!isActive || !streamInfo) { + return; } - return true; - } + hasFinishedBuffering = false; - function onCurrentTrackChanged(e) { - if (!streamInfo || e.newMediaInfo.streamInfo.id !== streamInfo.id) return; let mediaInfo = e.newMediaInfo; let manifest = manifestModel.getValue(); @@ -383,208 +668,42 @@ function Stream(config) { let currentTime = playbackController.getTime(); logger.info('Stream - Process track changed at current time ' + currentTime); - logger.debug('Stream - Update stream controller'); - if (manifest.refreshManifestOnSwitchTrack) { // Applies only for MSS streams - logger.debug('Stream - Refreshing manifest for switch track'); - trackChangedEvent = e; - manifestUpdater.refreshManifest(); - } else { - processor.selectMediaInfo(mediaInfo); - if (mediaInfo.type !== Constants.FRAGMENTED_TEXT) { - abrController.updateTopQualityIndex(mediaInfo); - processor.switchTrackAsked(); - processor.getFragmentModel().abortRequests(); - } else { - processor.getScheduleController().setSeekTarget(currentTime); - processor.setBufferingTime(currentTime); - processor.resetIndexHandler(); + // Applies only for MSS streams + if (manifest.refreshManifestOnSwitchTrack) { + trackChangedEvents.push(e); + if (!manifestUpdater.getIsUpdating()) { + logger.debug('Stream - Refreshing manifest for switch track'); + manifestUpdater.refreshManifest(); } - } - } - - function createStreamProcessor(mediaInfo, allMediaForType, mediaSource, optionalSettings) { - - let fragmentModel = fragmentController.getModel(mediaInfo ? mediaInfo.type : null); - - let streamProcessor = StreamProcessor(context).create({ - streamInfo: streamInfo, - type: mediaInfo ? mediaInfo.type : null, - mimeType: mediaInfo ? mediaInfo.mimeType : null, - timelineConverter: timelineConverter, - adapter: adapter, - manifestModel: manifestModel, - mediaPlayerModel: mediaPlayerModel, - fragmentModel: fragmentModel, - dashMetrics: config.dashMetrics, - baseURLController: config.baseURLController, - abrController: abrController, - playbackController: playbackController, - mediaController: mediaController, - textController: textController, - errHandler: errHandler, - settings: settings, - boxParser: boxParser - }); - - streamProcessor.initialize(mediaSource, hasVideoTrack); - abrController.updateTopQualityIndex(mediaInfo); - - if (optionalSettings) { - streamProcessor.setBuffer(optionalSettings.buffer); - streamProcessor.setBufferingTime(optionalSettings.currentTime); - streamProcessors[optionalSettings.replaceIdx] = streamProcessor; } else { - streamProcessors.push(streamProcessor); - } - - if (optionalSettings && optionalSettings.ignoreMediaInfo) { - return; - } - - if (mediaInfo && (mediaInfo.type === Constants.TEXT || mediaInfo.type === Constants.FRAGMENTED_TEXT)) { - let idx; - for (let i = 0; i < allMediaForType.length; i++) { - if (allMediaForType[i].index === mediaInfo.index) { - idx = i; - } - streamProcessor.addMediaInfo(allMediaForType[i]); //creates text tracks for all adaptations in one stream processor - } - streamProcessor.selectMediaInfo(allMediaForType[idx]); //sets the initial media info - } else { - streamProcessor.addMediaInfo(mediaInfo, true); + processor.selectMediaInfo(mediaInfo) + .then(() => { + if (mediaInfo.type === Constants.VIDEO || mediaInfo.type === Constants.AUDIO) { + abrController.updateTopQualityIndex(mediaInfo); + } + processor.prepareTrackSwitch(); + }); } } - function initializeMediaForType(type, mediaSource) { - const allMediaForType = adapter.getAllMediaInfoForType(streamInfo, type); - - let mediaInfo = null; - let initialMediaInfo; - - if (!allMediaForType || allMediaForType.length === 0) { - logger.info('No ' + type + ' data.'); - return; - } - - if (type === Constants.VIDEO) { - hasVideoTrack = true; - } - - if (type === Constants.AUDIO) { - hasAudioTrack = true; - } - - for (let i = 0, ln = allMediaForType.length; i < ln; i++) { - mediaInfo = allMediaForType[i]; - - if (type === Constants.EMBEDDED_TEXT) { - textController.addEmbeddedTrack(mediaInfo); - } else { - if (!isMediaSupported(mediaInfo)) continue; - mediaController.addTrack(mediaInfo); - } - } + function prepareQualityChange(e) { + const processor = _getProcessorByType(e.mediaType); - if (type === Constants.EMBEDDED_TEXT || mediaController.getTracksFor(type, streamInfo).length === 0) { - return; + if (processor) { + processor.prepareQualityChange(e); } - - if (type === Constants.IMAGE) { - thumbnailController = ThumbnailController(context).create({ - streamInfo: streamInfo, - adapter: adapter, - baseURLController: config.baseURLController, - timelineConverter: config.timelineConverter, - debug: debug, - eventBus: eventBus, - events: Events, - dashConstants: DashConstants - }); - return; - } - - - mediaController.checkInitialMediaSettingsForType(type, streamInfo); - initialMediaInfo = mediaController.getCurrentTrackFor(type, streamInfo); - - eventBus.trigger(Events.STREAM_INITIALIZING, { - streamInfo: streamInfo, - mediaInfo: mediaInfo - }); - - // TODO : How to tell index handler live/duration? - // TODO : Pass to controller and then pass to each method on handler? - - createStreamProcessor(initialMediaInfo, allMediaForType, mediaSource); } - function addInlineEvents() { + function _addInlineEvents() { if (eventController) { const events = adapter.getEventsFor(streamInfo); - eventController.addInlineEvents(events); - } - } - - function addInbandEvents(events) { - if (eventController) { - eventController.addInbandEvents(events); - } - } - - function initializeMedia(mediaSource, previousBuffers) { - checkConfig(); - let element = videoModel.getElement(); - - addInlineEvents(); - - isUpdating = true; - - capabilitiesFilter.filterUnsupportedFeaturesOfPeriod( streamInfo); - - if (!element || (element && (/^VIDEO$/i).test(element.nodeName))) { - initializeMediaForType(Constants.VIDEO, mediaSource); - } - initializeMediaForType(Constants.AUDIO, mediaSource); - initializeMediaForType(Constants.TEXT, mediaSource); - initializeMediaForType(Constants.FRAGMENTED_TEXT, mediaSource); - initializeMediaForType(Constants.EMBEDDED_TEXT, mediaSource); - initializeMediaForType(Constants.MUXED, mediaSource); - initializeMediaForType(Constants.IMAGE, mediaSource); - - //TODO. Consider initialization of TextSourceBuffer here if embeddedText, but no sideloadedText. - const buffers = createBuffers(previousBuffers); - - isMediaInitialized = true; - isUpdating = false; - - if (streamProcessors.length === 0) { - const msg = 'No streams to play.'; - errHandler.error(new DashJSError(Errors.MANIFEST_ERROR_ID_NOSTREAMS_CODE, msg, manifestModel.getValue())); - logger.fatal(msg); - } else { - checkIfInitializationCompleted(); - } - - return buffers; - } - - function initializeAfterPreload() { - isUpdating = true; - checkConfig(); - capabilitiesFilter.filterUnsupportedFeaturesOfPeriod(streamInfo); - - isMediaInitialized = true; - isUpdating = false; - if (streamProcessors.length === 0) { - const msg = 'No streams to play.'; - errHandler.error(new DashJSError(Errors.MANIFEST_ERROR_ID_NOSTREAMS_CODE, msg, manifestModel.getValue())); - logger.debug(msg); - } else { - checkIfInitializationCompleted(); + if (events && events.length > 0) { + eventController.addInlineEvents(events, streamInfo.id); + } } } - function checkIfInitializationCompleted() { + function _checkIfInitializationCompleted() { const ln = streamProcessors.length; const hasError = !!updateError.audio || !!updateError.video; let error = hasError ? new DashJSError(Errors.DATA_UPDATE_FAILED_ERROR_CODE, Errors.DATA_UPDATE_FAILED_ERROR_MESSAGE) : null; @@ -595,44 +714,36 @@ function Stream(config) { } } - if (!isMediaInitialized) { - return; - } - if (protectionController) { // Need to check if streamProcessors exists because streamProcessors // could be cleared in case an error is detected while initializing DRM keysystem - protectionController.clearMediaInfoArrayByStreamId(getId()); + protectionController.clearMediaInfoArray(); for (let i = 0; i < ln && streamProcessors[i]; i++) { const type = streamProcessors[i].getType(); + const mediaInfo = streamProcessors[i].getMediaInfo(); if (type === Constants.AUDIO || type === Constants.VIDEO || - type === Constants.FRAGMENTED_TEXT) { + (type === Constants.TEXT && mediaInfo.isFragmented)) { let mediaInfo = streamProcessors[i].getMediaInfo(); if (mediaInfo) { protectionController.initializeForMedia(mediaInfo); } } } + protectionController.handleKeySystemFromManifest(); } if (error) { errHandler.error(error); - } else if (!isStreamInitialized) { - isStreamInitialized = true; - - eventBus.trigger(Events.STREAM_INITIALIZED, { - streamInfo: streamInfo, - liveStartTime: !preloaded ? getLiveStartTime() : NaN - }); + } else if (!isInitialized) { + isInitialized = true; + videoModel.waitForReadyState(Constants.VIDEO_ELEMENT_READY_STATES.HAVE_METADATA, () => { + eventBus.trigger(Events.STREAM_INITIALIZED, { + streamInfo: streamInfo + }); + }) } - // (Re)start ScheduleController: - // - in case stream initialization has been completed after 'play' event (case for SegmentBase streams) - // - in case stream is complete but a track switch has been requested - for (let i = 0; i < ln && streamProcessors[i]; i++) { - streamProcessors[i].getScheduleController().start(); - } } function getMediaInfo(type) { @@ -649,17 +760,6 @@ function Stream(config) { return null; } - function createBuffers(previousBuffers) { - const buffers = {}; - for (let i = 0, ln = streamProcessors.length; i < ln; i++) { - const buffer = streamProcessors[i].createBuffer(previousBuffers); - if (buffer) { - buffers[streamProcessors[i].getType()] = buffer.getBuffer(); - } - } - return buffers; - } - function onBufferingCompleted() { let processors = getProcessors(); const ln = processors.length; @@ -673,33 +773,44 @@ function Stream(config) { for (let i = 0; i < ln; i++) { //if audio or video buffer is not buffering completed state, do not send STREAM_BUFFERING_COMPLETED if (!processors[i].isBufferingCompleted() && (processors[i].getType() === Constants.AUDIO || processors[i].getType() === Constants.VIDEO)) { - logger.warn('onBufferingCompleted - One streamProcessor has finished but', processors[i].getType(), 'one is not buffering completed'); + logger.debug('onBufferingCompleted - One streamProcessor has finished but', processors[i].getType(), 'one is not buffering completed'); return; } } logger.debug('onBufferingCompleted - trigger STREAM_BUFFERING_COMPLETED'); - eventBus.trigger(Events.STREAM_BUFFERING_COMPLETED, { streamInfo: streamInfo }); + hasFinishedBuffering = true; + eventBus.trigger(Events.STREAM_BUFFERING_COMPLETED, { streamInfo: streamInfo }, { streamInfo }); } function onDataUpdateCompleted(e) { updateError[e.mediaType] = e.error; - checkIfInitializationCompleted(); + _checkIfInitializationCompleted(); } function onInbandEvents(e) { - addInbandEvents(e.events); + if (eventController) { + eventController.addInbandEvents(e.events, streamInfo.id); + } } function getProcessorForMediaInfo(mediaInfo) { - if (!mediaInfo) { + if (!mediaInfo || !mediaInfo.type) { + return null; + } + + return _getProcessorByType(mediaInfo.type); + } + + function _getProcessorByType(type) { + if (!type) { return null; } let processors = getProcessors(); return processors.filter(function (processor) { - return (processor.getType() === mediaInfo.type); + return (processor.getType() === type); })[0]; } @@ -713,7 +824,7 @@ function Stream(config) { streamProcessor = streamProcessors[i]; type = streamProcessor.getType(); - if (type === Constants.AUDIO || type === Constants.VIDEO || type === Constants.FRAGMENTED_TEXT || type === Constants.TEXT) { + if (type === Constants.AUDIO || type === Constants.VIDEO || type === Constants.TEXT) { arr.push(streamProcessor); } } @@ -721,63 +832,85 @@ function Stream(config) { return arr; } - function updateData(updatedStreamInfo) { - logger.info('Manifest updated... updating data system wide.'); - - isStreamActivated = false; - isUpdating = true; - streamInfo = updatedStreamInfo; - - eventBus.trigger(Events.STREAM_UPDATED, { streamInfo: streamInfo }); - - if (eventController) { - addInlineEvents(); + function startScheduleControllers() { + const ln = streamProcessors.length; + for (let i = 0; i < ln && streamProcessors[i]; i++) { + streamProcessors[i].getScheduleController().startScheduleTimer(); } + } - capabilitiesFilter.filterUnsupportedFeaturesOfPeriod(streamInfo); + function updateData(updatedStreamInfo) { + return new Promise((resolve) => { + isUpdating = true; + streamInfo = updatedStreamInfo; - for (let i = 0, ln = streamProcessors.length; i < ln; i++) { - let streamProcessor = streamProcessors[i]; - streamProcessor.updateStreamInfo(streamInfo); - let mediaInfo = adapter.getMediaInfoForType(streamInfo, streamProcessor.getType()); - // Check if AdaptationSet has not been removed in MPD update - if (mediaInfo) { - abrController.updateTopQualityIndex(mediaInfo); - streamProcessor.addMediaInfo(mediaInfo, true); + if (eventController) { + _addInlineEvents(); } - } - if (trackChangedEvent) { - let mediaInfo = trackChangedEvent.newMediaInfo; - if (mediaInfo.type !== Constants.FRAGMENTED_TEXT) { - let processor = getProcessorForMediaInfo(trackChangedEvent.oldMediaInfo); - if (!processor) return; - processor.switchTrackAsked(); - trackChangedEvent = undefined; + let promises = []; + for (let i = 0, ln = streamProcessors.length; i < ln; i++) { + let streamProcessor = streamProcessors[i]; + const currentMediaInfo = streamProcessor.getMediaInfo(); + promises.push(streamProcessor.updateStreamInfo(streamInfo)); + let allMediaForType = adapter.getAllMediaInfoForType(streamInfo, streamProcessor.getType()); + // Check if AdaptationSet has not been removed in MPD update + if (allMediaForType) { + // Remove the current mediaInfo objects before adding the updated ones + streamProcessor.clearMediaInfoArray(); + for (let j = 0; j < allMediaForType.length; j++) { + const mInfo = allMediaForType[j]; + streamProcessor.addMediaInfo(allMediaForType[j]); + if (adapter.areMediaInfosEqual(currentMediaInfo, mInfo)) { + abrController.updateTopQualityIndex(mInfo); + promises.push(streamProcessor.selectMediaInfo(mInfo)) + } + } + } } - } - isUpdating = false; - checkIfInitializationCompleted(); + Promise.all(promises) + .then(() => { + promises = []; + + while (trackChangedEvents.length > 0) { + let trackChangedEvent = trackChangedEvents.pop(); + let mediaInfo = trackChangedEvent.newMediaInfo; + let processor = getProcessorForMediaInfo(trackChangedEvent.oldMediaInfo); + if (!processor) return; + promises.push(processor.prepareTrackSwitch()); + processor.selectMediaInfo(mediaInfo); + } + + return Promise.all(promises) + }) + .then(() => { + isUpdating = false; + _checkIfInitializationCompleted(); + eventBus.trigger(Events.STREAM_UPDATED, { streamInfo: streamInfo }); + resolve(); + }) + + }) } function isMediaCodecCompatible(newStream, previousStream = null) { return compareCodecs(newStream, Constants.VIDEO, previousStream) && compareCodecs(newStream, Constants.AUDIO, previousStream); } - function isProtectionCompatible(stream, previousStream = null) { - return compareProtectionConfig(stream, Constants.VIDEO, previousStream) && compareProtectionConfig(stream, Constants.AUDIO, previousStream); + function isProtectionCompatible(newStream) { + if (!newStream) { + return true; + } + return _compareProtectionConfig(Constants.VIDEO, newStream) && _compareProtectionConfig(Constants.AUDIO, newStream); } - function compareProtectionConfig(stream, type, previousStream = null) { - if (!stream) { - return false; - } - const newStreamInfo = stream.getStreamInfo(); - const currentStreamInfo = previousStream ? previousStream.getStreamInfo() : getStreamInfo(); + function _compareProtectionConfig(type, newStream) { + const currentStreamInfo = getStreamInfo(); + const newStreamInfo = newStream.getStreamInfo(); if (!newStreamInfo || !currentStreamInfo) { - return false; + return true; } const newAdaptation = adapter.getAdaptationForType(newStreamInfo.index, type, newStreamInfo); @@ -789,10 +922,10 @@ function Stream(config) { } // If the current period is unencrypted and the upcoming one is encrypted we need to reset sourcebuffers. - return !(!isAdaptationDrmProtected(currentAdaptation) && isAdaptationDrmProtected(newAdaptation)); + return !(!_isAdaptationDrmProtected(currentAdaptation) && _isAdaptationDrmProtected(newAdaptation)); } - function isAdaptationDrmProtected(adaptation) { + function _isAdaptationDrmProtected(adaptation) { if (!adaptation) { // If there is no adaptation for neither the old or the new stream they're compatible @@ -847,56 +980,44 @@ function Stream(config) { return preloaded; } - function preload(mediaSource, previousBuffers) { - if (!getPreloaded()) { - addInlineEvents(); - - initializeMediaForType(Constants.VIDEO, mediaSource); - initializeMediaForType(Constants.AUDIO, mediaSource); - initializeMediaForType(Constants.TEXT, mediaSource); - initializeMediaForType(Constants.FRAGMENTED_TEXT, mediaSource); - initializeMediaForType(Constants.EMBEDDED_TEXT, mediaSource); - initializeMediaForType(Constants.MUXED, mediaSource); - initializeMediaForType(Constants.IMAGE, mediaSource); - - createBuffers(previousBuffers); - - eventBus.on(Events.CURRENT_TRACK_CHANGED, onCurrentTrackChanged, instance); - for (let i = 0; i < streamProcessors.length && streamProcessors[i]; i++) { - streamProcessors[i].getScheduleController().start(); - } - - setPreloaded(true); - } + function getHasFinishedBuffering() { + return hasFinishedBuffering; } + function getAdapter() { + return adapter; + } instance = { - initialize: initialize, - getStreamId: getStreamId, - activate: activate, - deactivate: deactivate, - isActive: isActive, - getDuration: getDuration, - getStartTime: getStartTime, - getId: getId, - getStreamInfo: getStreamInfo, - getHasAudioTrack: getHasAudioTrack, - getHasVideoTrack: getHasVideoTrack, - preload: preload, - getThumbnailController: getThumbnailController, - getBitrateListFor: getBitrateListFor, - updateData: updateData, - reset: reset, - getProcessors: getProcessors, - setMediaSource: setMediaSource, - isMediaCodecCompatible: isMediaCodecCompatible, - isProtectionCompatible: isProtectionCompatible, - getPreloaded: getPreloaded, - getPreloadingScheduled, - setPreloadingScheduled, + initialize, + getStreamId, + activate, + deactivate, + getIsActive, + getDuration, + getStartTime, + getId, + getStreamInfo, + getHasAudioTrack, + getHasVideoTrack, + startPreloading, + getThumbnailController, + getBitrateListFor, + updateData, + reset, + getProcessors, + setMediaSource, + isMediaCodecCompatible, + isProtectionCompatible, + getPreloaded, getIsEndedEventSignaled, - setIsEndedEventSignaled + setIsEndedEventSignaled, + getAdapter, + getHasFinishedBuffering, + setPreloaded, + startScheduleControllers, + prepareTrackChange, + prepareQualityChange }; setup(); diff --git a/src/streaming/StreamProcessor.js b/src/streaming/StreamProcessor.js index 35bf7796b2..1771a62e88 100644 --- a/src/streaming/StreamProcessor.js +++ b/src/streaming/StreamProcessor.js @@ -33,10 +33,9 @@ import DashConstants from '../dash/constants/DashConstants'; import MetricsConstants from './constants/MetricsConstants'; import FragmentModel from './models/FragmentModel'; import BufferController from './controllers/BufferController'; -import TextBufferController from './text/TextBufferController'; +import NotFragmentedTextBufferController from './text/NotFragmentedTextBufferController'; import ScheduleController from './controllers/ScheduleController'; import RepresentationController from '../dash/controllers/RepresentationController'; -import LiveEdgeFinder from './utils/LiveEdgeFinder'; import FactoryMaker from '../core/FactoryMaker'; import {checkInteger} from './utils/SupervisorTools'; import EventBus from '../core/EventBus'; @@ -48,8 +47,9 @@ import Debug from '../core/Debug'; import RequestModifier from './utils/RequestModifier'; import URLUtils from '../streaming/utils/URLUtils'; import BoxParser from './utils/BoxParser'; -import FragmentRequest from './vo/FragmentRequest'; import {PlayListTrace} from './vo/metrics/PlayList'; +import SegmentsController from '../dash/controllers/SegmentsController'; +import {HTTPRequest} from './vo/metrics/HTTPRequest'; function StreamProcessor(config) { @@ -73,6 +73,7 @@ function StreamProcessor(config) { let dashMetrics = config.dashMetrics; let settings = config.settings; let boxParser = config.boxParser; + let segmentBlacklistController = config.segmentBlacklistController; let instance, logger, @@ -82,40 +83,56 @@ function StreamProcessor(config) { bufferController, scheduleController, representationController, - liveEdgeFinder, - indexHandler, - bufferingTime, - bufferPruned; + shouldUseExplicitTimeForRequest, + qualityChangeInProgress, + dashHandler, + segmentsController, + bufferingTime; function setup() { logger = Debug(context).getInstance().getLogger(instance); resetInitialSettings(); - eventBus.on(Events.DATA_UPDATE_COMPLETED, onDataUpdateCompleted, instance, { priority: EventBus.EVENT_PRIORITY_HIGH }); // High priority to be notified before Stream - eventBus.on(Events.QUALITY_CHANGE_REQUESTED, onQualityChanged, instance); - eventBus.on(Events.INIT_FRAGMENT_NEEDED, onInitFragmentNeeded, instance); - eventBus.on(Events.MEDIA_FRAGMENT_NEEDED, onMediaFragmentNeeded, instance); - eventBus.on(Events.MEDIA_FRAGMENT_LOADED, onMediaFragmentLoaded, instance); - eventBus.on(Events.BUFFER_LEVEL_UPDATED, onBufferLevelUpdated, instance); - eventBus.on(Events.BUFFER_LEVEL_STATE_CHANGED, onBufferLevelStateChanged, instance); - eventBus.on(Events.BUFFER_CLEARED, onBufferCleared, instance); - eventBus.on(Events.QUOTA_EXCEEDED, onQuotaExceeded, instance); - eventBus.on(Events.SEEK_TARGET, onSeekTarget, instance); - } - - function initialize(mediaSource, hasVideoTrack) { - indexHandler = DashHandler(context).create({ - streamInfo: streamInfo, - type: type, - timelineConverter: timelineConverter, - dashMetrics: dashMetrics, - mediaPlayerModel: mediaPlayerModel, + eventBus.on(Events.DATA_UPDATE_COMPLETED, _onDataUpdateCompleted, instance, { priority: EventBus.EVENT_PRIORITY_HIGH }); // High priority to be notified before Stream + eventBus.on(Events.INIT_FRAGMENT_NEEDED, _onInitFragmentNeeded, instance); + eventBus.on(Events.MEDIA_FRAGMENT_NEEDED, _onMediaFragmentNeeded, instance); + eventBus.on(Events.MEDIA_FRAGMENT_LOADED, _onMediaFragmentLoaded, instance); + eventBus.on(Events.BUFFER_LEVEL_STATE_CHANGED, _onBufferLevelStateChanged, instance); + eventBus.on(Events.BUFFER_CLEARED, _onBufferCleared, instance); + eventBus.on(Events.SEEK_TARGET, _onSeekTarget, instance); + eventBus.on(Events.FRAGMENT_LOADING_ABANDONED, _onFragmentLoadingAbandoned, instance); + eventBus.on(Events.FRAGMENT_LOADING_COMPLETED, _onFragmentLoadingCompleted, instance); + eventBus.on(Events.QUOTA_EXCEEDED, _onQuotaExceeded, instance); + eventBus.on(Events.SET_FRAGMENTED_TEXT_AFTER_DISABLED, _onSetFragmentedTextAfterDisabled, instance); + eventBus.on(Events.SET_NON_FRAGMENTED_TEXT, _onSetNonFragmentedText, instance); + eventBus.on(Events.SOURCE_BUFFER_ERROR, _onSourceBufferError, instance); + } + + function initialize(mediaSource, hasVideoTrack, isFragmented) { + + segmentsController = SegmentsController(context).create({ + events: Events, + eventBus, + streamInfo, + timelineConverter, + dashConstants: DashConstants, + segmentBaseController: config.segmentBaseController, + type + }); + + dashHandler = DashHandler(context).create({ + streamInfo, + type, + timelineConverter, + dashMetrics, + mediaPlayerModel, baseURLController: config.baseURLController, - errHandler: errHandler, - settings: settings, - boxParser: boxParser, + errHandler, + segmentsController, + settings, + boxParser, events: Events, - eventBus: eventBus, + eventBus, errors: Errors, debug: Debug(context).getInstance(), requestModifier: RequestModifier(context).getInstance(), @@ -124,56 +141,52 @@ function StreamProcessor(config) { urlUtils: URLUtils(context).getInstance() }); - // Create live edge finder for dynamic streams isDynamic = streamInfo.manifestInfo.isDynamic; - if (isDynamic) { - liveEdgeFinder = LiveEdgeFinder(context).create({ - timelineConverter: timelineConverter - }); - } // Create/initialize controllers - indexHandler.initialize(isDynamic); + dashHandler.initialize(isDynamic); abrController.registerStreamType(type, instance); representationController = RepresentationController(context).create({ - streamInfo: streamInfo, - type: type, - abrController: abrController, - dashMetrics: dashMetrics, - playbackController: playbackController, - timelineConverter: timelineConverter, + streamInfo, + type, + abrController, + dashMetrics, + playbackController, + timelineConverter, dashConstants: DashConstants, events: Events, - eventBus: eventBus, - errors: Errors + eventBus, + errors: Errors, + isDynamic, + segmentsController }); - bufferController = createBufferControllerForType(type); + bufferController = _createBufferControllerForType(type, isFragmented); if (bufferController) { bufferController.initialize(mediaSource); } scheduleController = ScheduleController(context).create({ - streamInfo: streamInfo, - type: type, - mimeType: mimeType, - adapter: adapter, - dashMetrics: dashMetrics, - mediaPlayerModel: mediaPlayerModel, - fragmentModel: fragmentModel, - abrController: abrController, - playbackController: playbackController, - textController: textController, - mediaController: mediaController, - bufferController: bufferController, - settings: settings + streamInfo, + type, + mimeType, + adapter, + dashMetrics, + mediaPlayerModel, + fragmentModel, + abrController, + playbackController, + textController, + mediaController, + bufferController, + settings }); scheduleController.initialize(hasVideoTrack); bufferingTime = 0; - bufferPruned = false; + shouldUseExplicitTimeForRequest = false; } function getStreamId() { @@ -184,15 +197,21 @@ function StreamProcessor(config) { return type; } + function getIsTextTrack() { + return adapter.getIsTextTrack(representationController.getData()); + } + function resetInitialSettings() { mediaInfoArr = []; mediaInfo = null; bufferingTime = 0; + shouldUseExplicitTimeForRequest = false; + qualityChangeInProgress = false; } function reset(errored, keepBuffers) { - if (indexHandler) { - indexHandler.reset(); + if (dashHandler) { + dashHandler.reset(); } if (bufferController) { @@ -210,24 +229,27 @@ function StreamProcessor(config) { representationController = null; } - if (liveEdgeFinder) { - liveEdgeFinder.reset(); - liveEdgeFinder = null; + if (segmentsController) { + segmentsController = null; } - if (abrController && !keepBuffers) { - abrController.unRegisterStreamType(type); + if (abrController) { + abrController.unRegisterStreamType(getStreamId(), type); } - eventBus.off(Events.DATA_UPDATE_COMPLETED, onDataUpdateCompleted, instance); - eventBus.off(Events.QUALITY_CHANGE_REQUESTED, onQualityChanged, instance); - eventBus.off(Events.INIT_FRAGMENT_NEEDED, onInitFragmentNeeded, instance); - eventBus.off(Events.MEDIA_FRAGMENT_NEEDED, onMediaFragmentNeeded, instance); - eventBus.off(Events.MEDIA_FRAGMENT_LOADED, onMediaFragmentLoaded, instance); - eventBus.off(Events.BUFFER_LEVEL_UPDATED, onBufferLevelUpdated, instance); - eventBus.off(Events.BUFFER_LEVEL_STATE_CHANGED, onBufferLevelStateChanged, instance); - eventBus.off(Events.BUFFER_CLEARED, onBufferCleared, instance); - eventBus.off(Events.SEEK_TARGET, onSeekTarget, instance); + eventBus.off(Events.DATA_UPDATE_COMPLETED, _onDataUpdateCompleted, instance); + eventBus.off(Events.INIT_FRAGMENT_NEEDED, _onInitFragmentNeeded, instance); + eventBus.off(Events.MEDIA_FRAGMENT_NEEDED, _onMediaFragmentNeeded, instance); + eventBus.off(Events.MEDIA_FRAGMENT_LOADED, _onMediaFragmentLoaded, instance); + eventBus.off(Events.BUFFER_LEVEL_STATE_CHANGED, _onBufferLevelStateChanged, instance); + eventBus.off(Events.BUFFER_CLEARED, _onBufferCleared, instance); + eventBus.off(Events.SEEK_TARGET, _onSeekTarget, instance); + eventBus.off(Events.FRAGMENT_LOADING_ABANDONED, _onFragmentLoadingAbandoned, instance); + eventBus.off(Events.FRAGMENT_LOADING_COMPLETED, _onFragmentLoadingCompleted, instance); + eventBus.off(Events.SET_FRAGMENTED_TEXT_AFTER_DISABLED, _onSetFragmentedTextAfterDisabled, instance); + eventBus.off(Events.SET_NON_FRAGMENTED_TEXT, _onSetNonFragmentedText, instance); + eventBus.off(Events.QUOTA_EXCEEDED, _onQuotaExceeded, instance); + eventBus.off(Events.SOURCE_BUFFER_ERROR, _onSourceBufferError, instance); resetInitialSettings(); type = null; @@ -238,67 +260,549 @@ function StreamProcessor(config) { return representationController ? representationController.isUpdating() : false; } + /** + * When a seek within the corresponding period occurs this function initiates the clearing of the buffer and sets the correct buffering time. + * @param {object} e + * @private + */ + function prepareInnerPeriodPlaybackSeeking(e) { + return new Promise((resolve) => { + // Stop segment requests until we have figured out for which time we need to request a segment. We don't want to replace existing segments. + scheduleController.clearScheduleTimer(); + fragmentModel.abortRequests(); + + // Abort operations to the SourceBuffer Sink and reset the BufferControllers isBufferingCompleted state. + bufferController.prepareForPlaybackSeek() + .then(() => { + // Clear the buffer. We need to prune everything which is not in the target interval. + const clearRanges = bufferController.getAllRangesWithSafetyFactor(e.seekTime); + // When everything has been pruned go on + return bufferController.clearBuffers(clearRanges); + }) + .then(() => { + // Figure out the correct segment request time. + const continuousBufferTime = bufferController.getContinuousBufferTimeForTargetTime(e.seekTime); + + // If the buffer is continuous and exceeds the duration of the period we are still done buffering. We need to trigger the buffering completed event in order to start prebuffering upcoming periods again + if (!isNaN(continuousBufferTime) && !isNaN(streamInfo.duration) && isFinite(streamInfo.duration) && continuousBufferTime >= streamInfo.start + streamInfo.duration) { + bufferController.setIsBufferingCompleted(true); + resolve(); + } else { + const targetTime = isNaN(continuousBufferTime) ? e.seekTime : continuousBufferTime; + setExplicitBufferingTime(targetTime); + bufferController.setSeekTarget(targetTime); + + const promises = []; + + // append window has been reset by abort() operation. Set the correct values again + promises.push(bufferController.updateAppendWindow()); + + // Timestamp offset couldve been changed by preloading period + const representationInfo = getRepresentationInfo(); + promises.push(bufferController.updateBufferTimestampOffset(representationInfo)); + + Promise.all(promises) + .then(() => { + // We might have aborted the append operation of an init segment. Append init segment again. + scheduleController.setInitSegmentRequired(true); + + // Right after a seek we should not immediately check the playback quality + scheduleController.setCheckPlaybackQuality(false); + scheduleController.startScheduleTimer(); + resolve(); + }); + } + }) + .catch((e) => { + logger.error(e); + }); + }); - function onDataUpdateCompleted(e) { - if (!e.error) { - // Update representation if no error - scheduleController.setCurrentRepresentation(adapter.convertDataToRepresentationInfo(e.currentRepresentation)); + } + + /** + * Seek outside of the current period. + * @return {Promise} + */ + function prepareOuterPeriodPlaybackSeeking() { + return new Promise((resolve, reject) => { + try { + // Stop scheduling + scheduleController.clearScheduleTimer(); + + // Abort all ongoing requests + fragmentModel.abortRequests(); + + // buffering not complete anymore and abort current append operation to SourceBuffer + bufferController.prepareForPlaybackSeek() + .then(() => { + // Clear the buffers completely. + return bufferController.pruneAllSafely(); + }) + .then(() => { + resolve(); + }); + + } catch (e) { + reject(e); + } + }); + } + + /** + * ScheduleController indicates that an init segment needs to be fetched. + * @param {object} e + * @param {boolean} rescheduleIfNoRequest - Defines whether we reschedule in case no valid request could be generated + * @private + */ + function _onInitFragmentNeeded(e, rescheduleIfNoRequest = true) { + // Event propagation may have been stopped (see MssHandler) + if (!e.sender) return; + + if (playbackController.getIsManifestUpdateInProgress()) { + _noValidRequest(); + return; } - if (!e.error || e.error.code === Errors.SEGMENTS_UPDATE_FAILED_ERROR_CODE) { - // Update has been postponed, update nevertheless DVR info - const activeStreamId = playbackController.getStreamController().getActiveStreamInfo().id; - if (activeStreamId === streamInfo.id) { - addDVRMetric(); + + if (getIsTextTrack() && !textController.isTextEnabled()) return; + + if (bufferController && e.representationId) { + if (!bufferController.appendInitSegmentFromCache(e.representationId)) { + const rep = representationController.getCurrentRepresentation(); + // Dummy init segment (fragmented tracks without initialization segment) + if (rep.range === 0) { + _onMediaFragmentNeeded(); + return; + } + // Init segment not in cache, send new request + const request = dashHandler ? dashHandler.getInitRequest(mediaInfo, rep) : null; + if (request) { + fragmentModel.executeRequest(request); + } else if (rescheduleIfNoRequest) { + scheduleController.setInitSegmentRequired(true); + _noValidRequest(); + } } } } - function onQualityChanged(e) { - let representationInfo = getRepresentationInfo(e.newQuality); - scheduleController.setCurrentRepresentation(representationInfo); - dashMetrics.pushPlayListTraceMetrics(new Date(), PlayListTrace.REPRESENTATION_SWITCH_STOP_REASON); - dashMetrics.createPlaylistTraceMetrics(representationInfo.id, playbackController.getTime() * 1000, playbackController.getPlaybackRate()); + /** + * ScheduleController indicates that a media segment is needed + * @param {boolean} rescheduleIfNoRequest - Defines whether we reschedule in case no valid request could be generated + * @private + */ + function _onMediaFragmentNeeded(e, rescheduleIfNoRequest = true) { + // Don't schedule next fragments while updating manifest or pruning to avoid buffer inconsistencies + if (playbackController.getIsManifestUpdateInProgress() || bufferController.getIsPruningInProgress()) { + _noValidRequest(); + return; + } + + let request = _getFragmentRequest(); + if (request) { + shouldUseExplicitTimeForRequest = false; + _mediaRequestGenerated(request); + } else { + _noMediaRequestGenerated(rescheduleIfNoRequest); + } } - function onBufferLevelUpdated(e) { - dashMetrics.addBufferLevel(type, new Date(), e.bufferLevel * 1000); - const activeStreamId = playbackController.getStreamController().getActiveStreamInfo().id; - if (!manifestModel.getValue().doNotUpdateDVRWindowOnBufferUpdated && streamInfo.id === activeStreamId) { - addDVRMetric(); + /** + * If we generated a valid media request we can execute the request. In some cases the segment might be blacklisted. + * @param {object} request + * @private + */ + function _mediaRequestGenerated(request) { + if (!isNaN(request.startTime + request.duration)) { + bufferingTime = request.startTime + request.duration; + } + request.delayLoadingTime = new Date().getTime() + scheduleController.getTimeToLoadDelay(); + scheduleController.setTimeToLoadDelay(0); + if (!_shouldIgnoreRequest(request)) { + logger.debug(`Next fragment request url for stream id ${streamInfo.id} and media type ${type} is ${request.url}`); + fragmentModel.executeRequest(request); + } else { + logger.warn(`Fragment request url ${request.url} for stream id ${streamInfo.id} and media type ${type} is on the ignore list and will be skipped`); + _noValidRequest(); } } - function onBufferLevelStateChanged(e) { + /** + * We could not generate a valid request. Check if the media is finished, we are stuck in a gap or simply need to wait for the next segment to be available. + * @param {boolean} rescheduleIfNoRequest + * @private + */ + function _noMediaRequestGenerated(rescheduleIfNoRequest) { + const representation = representationController.getCurrentRepresentation(); + + // If this statement is true we might be stuck. A static manifest does not change and we did not find a valid request for the target time + // There is no point in trying again. We need to adjust the time in order to find a valid request. This can happen if the user/app seeked into a gap. + // For dynamic manifests this can also happen especially if we jump over the gap in the previous period and are using SegmentTimeline and in case there is a positive eptDelta at the beginning of the period we are stuck. + if (settings.get().streaming.gaps.enableSeekFix && (shouldUseExplicitTimeForRequest || playbackController.getTime() === 0)) { + let adjustedTime; + if (!isDynamic) { + adjustedTime = dashHandler.getValidTimeAheadOfTargetTime(bufferingTime, mediaInfo, representation, settings.get().streaming.gaps.threshold); + } else if (isDynamic && representation.segmentInfoType === DashConstants.SEGMENT_TIMELINE) { + // If we find a valid request ahead of the current time then we are in a gap. Segments are only added at the end of the timeline + adjustedTime = dashHandler.getValidTimeAheadOfTargetTime(bufferingTime, mediaInfo, representation, settings.get().streaming.gaps.threshold,); + } + if (!isNaN(adjustedTime) && adjustedTime !== bufferingTime) { + if (playbackController.isSeeking() || playbackController.getTime() === 0) { + // If we are seeking then playback is stalled. Do a seek to get out of this situation + logger.warn(`Adjusting playback time ${adjustedTime} because of gap in the manifest. Seeking by ${adjustedTime - bufferingTime}`); + playbackController.seek(adjustedTime, false, false); + } else { + // If we are not seeking we should still be playing but we cant find anything to buffer. So we adjust the buffering time and leave the gap jump to the GapController + logger.warn(`Adjusting buffering time ${adjustedTime} because of gap in the manifest. Adjusting time by ${adjustedTime - bufferingTime}`); + setExplicitBufferingTime(adjustedTime) + + if (rescheduleIfNoRequest) { + _noValidRequest(); + } + } + return; + } + } + + // Check if the media is finished. If so, no need to schedule another request + const isLastSegmentRequested = dashHandler.isLastSegmentRequested(representation, bufferingTime); + if (isLastSegmentRequested) { + const segmentIndex = dashHandler.getCurrentIndex(); + logger.debug(`Segment requesting for stream ${streamInfo.id} has finished`); + eventBus.trigger(Events.STREAM_REQUESTING_COMPLETED, { segmentIndex }, { + streamId: streamInfo.id, + mediaType: type + }); + bufferController.segmentRequestingCompleted(segmentIndex); + scheduleController.clearScheduleTimer(); + return; + } + + if (rescheduleIfNoRequest) { + _noValidRequest(); + } + } + + /** + * In certain situations we need to ignore a request. For instance, if a segment is blacklisted because it caused an MSE error. + * @private + */ + function _shouldIgnoreRequest(request) { + let blacklistUrl = request.url; + + if (request.range) { + blacklistUrl = blacklistUrl.concat('_', request.range); + } + + return segmentBlacklistController.contains(blacklistUrl) + } + + /** + * Get the init or media segment request using the DashHandler. + * @return {null|FragmentRequest|null} + * @private + */ + function _getFragmentRequest() { + const representationInfo = getRepresentationInfo(); + let request; + + if (isNaN(bufferingTime) || (getType() === Constants.TEXT && !textController.isTextEnabled())) { + return null; + } + + // Use time just whenever is strictly needed + const useTime = shouldUseExplicitTimeForRequest; + + if (dashHandler) { + const representation = representationController && representationInfo ? representationController.getRepresentationForQuality(representationInfo.quality) : null; + + if (useTime) { + request = dashHandler.getSegmentRequestForTime(mediaInfo, representation, bufferingTime); + } else { + request = dashHandler.getNextSegmentRequest(mediaInfo, representation); + } + } + + return request; + } + + /** + * Whenever we can not generate a valid request we restart scheduling according to the timeouts defined in the settings. + * @private + */ + function _noValidRequest() { + scheduleController.startScheduleTimer(playbackController.getLowLatencyModeEnabled() ? settings.get().streaming.scheduling.lowLatencyTimeout : settings.get().streaming.scheduling.defaultTimeout); + } + + function _onDataUpdateCompleted(e) { + if (!e.error) { + // Update representation if no error + scheduleController.setCurrentRepresentation(adapter.convertRepresentationToRepresentationInfo(e.currentRepresentation)); + if (!bufferController.getIsBufferingCompleted()) { + bufferController.updateBufferTimestampOffset(e.currentRepresentation); + } + } + } + + function _onBufferLevelStateChanged(e) { dashMetrics.addBufferState(type, e.state, scheduleController.getBufferTarget()); if (e.state === MetricsConstants.BUFFER_EMPTY && !playbackController.isSeeking()) { - // logger.info('Buffer is empty! Stalling!'); + logger.info('Buffer is empty! Stalling!'); dashMetrics.pushPlayListTraceMetrics(new Date(), PlayListTrace.REBUFFERING_REASON); } } - function onBufferCleared(e) { + function _onBufferCleared(e) { // Remove executed requests not buffered anymore fragmentModel.syncExecutedRequestsWithBufferedRange( bufferController.getBuffer().getAllBufferRanges(), streamInfo.duration); // If buffer removed ahead current time (QuotaExceededError or automatic buffer pruning) then adjust current index handler time - if (e.from > playbackController.getTime()) { - bufferingTime = e.from; - bufferPruned = true; + if (e.quotaExceeded && e.from > playbackController.getTime()) { + setExplicitBufferingTime(e.from); + } + + // (Re)start schedule once buffer has been pruned after a QuotaExceededError + if (e.hasEnoughSpaceToAppend && e.quotaExceeded) { + scheduleController.startScheduleTimer(); + } + } + + /** + * This function is called when the corresponding SourceBuffer encountered an error. + * We blacklist the last segment assuming it caused the error + * @param {object} e + * @private + */ + function _onSourceBufferError(e) { + if (!e || !e.lastRequestAppended || !e.lastRequestAppended.url) { + return; + } + + let blacklistUrl = e.lastRequestAppended.url; + + if (e.lastRequestAppended.range) { + blacklistUrl = blacklistUrl.concat('_', e.lastRequestAppended.range); + } + logger.warn(`Blacklisting segment with url ${blacklistUrl}`); + segmentBlacklistController.add(blacklistUrl); + } + + /** + * The quality has changed which means we have switched to a different representation. + * If we want to aggressively replace existing parts in the buffer we need to make sure that the new quality is higher than the already buffered one. + * @param {object} e + * @private + */ + function prepareQualityChange(e) { + logger.debug(`Preparing quality switch for type ${type}`); + const newQuality = e.newQuality; + + qualityChangeInProgress = true; + + // Stop scheduling until we are done with preparing the quality switch + scheduleController.clearScheduleTimer(); + + const representationInfo = getRepresentationInfo(newQuality); + scheduleController.setCurrentRepresentation(representationInfo); + representationController.prepareQualityChange(newQuality); + + // Abort the current request to avoid inconsistencies and in case a rule such as AbandonRequestRule has forced a quality switch. A quality switch can also be triggered manually by the application. + // If we update the buffer values now, or initialize a request to the new init segment, the currently downloading media segment might "work" with wrong values. + // Everything that is already in the buffer queue is ok and will be handled by the corresponding function below depending on the switch mode. + fragmentModel.abortRequests(); + + // In any case we need to update the MSE.timeOffset + bufferController.updateBufferTimestampOffset(representationInfo) + .then(() => { + + // If the switch should occur immediately we need to replace existing stuff in the buffer + if (e.reason && e.reason.forceReplace) { + _prepareReplacementQualitySwitch(); + } + + // If fast switch is enabled we check if we are supposed to replace existing stuff in the buffer + else if (settings.get().streaming.buffer.fastSwitchEnabled) { + _prepareForFastQualitySwitch(representationInfo); + } + + // Default quality switch. We append the new quality to the already buffered stuff + else { + _prepareForDefaultQualitySwitch(); + } + + dashMetrics.pushPlayListTraceMetrics(new Date(), PlayListTrace.REPRESENTATION_SWITCH_STOP_REASON); + dashMetrics.createPlaylistTraceMetrics(representationInfo.id, playbackController.getTime() * 1000, playbackController.getPlaybackRate()); + }) + } + + function _prepareReplacementQualitySwitch() { + + // Inform other classes like the GapController that we are replacing existing stuff + eventBus.trigger(Events.BUFFER_REPLACEMENT_STARTED, { + mediaType: type, + streamId: streamInfo.id + }, { mediaType: type, streamId: streamInfo.id }); + + // Abort appending segments to the buffer. Also adjust the appendWindow as we might have been in the progress of prebuffering stuff. + bufferController.prepareForReplacementQualitySwitch() + .then(() => { + _bufferClearedForReplacement(); + qualityChangeInProgress = false; + }) + .catch(() => { + _bufferClearedForReplacement(); + qualityChangeInProgress = false; + }); + } + + function _prepareForFastQualitySwitch(representationInfo) { + // if we switch up in quality and need to replace existing parts in the buffer we need to adjust the buffer target + const time = playbackController.getTime(); + let safeBufferLevel = 1.5 * (!isNaN(representationInfo.fragmentDuration) ? representationInfo.fragmentDuration : 1); + const request = fragmentModel.getRequests({ + state: FragmentModel.FRAGMENT_MODEL_EXECUTED, + time: time + safeBufferLevel, + threshold: 0 + })[0]; + + if (request && !getIsTextTrack()) { + const bufferLevel = bufferController.getBufferLevel(); + const abandonmentState = abrController.getAbandonmentStateFor(streamInfo.id, type); + + // The quality we originally requested was lower than the new quality + if (request.quality < representationInfo.quality && bufferLevel >= safeBufferLevel && abandonmentState !== MetricsConstants.ABANDON_LOAD) { + const targetTime = time + safeBufferLevel; + setExplicitBufferingTime(targetTime); + scheduleController.setCheckPlaybackQuality(false); + scheduleController.startScheduleTimer(); + } else { + _prepareForDefaultQualitySwitch(); + } + } else { + scheduleController.startScheduleTimer(); + } + qualityChangeInProgress = false; + } + + function _prepareForDefaultQualitySwitch() { + // We might have aborted the current request. We need to set an explicit buffer time based on what we already have in the buffer. + _bufferClearedForNonReplacement() + qualityChangeInProgress = false; + } + + /** + * We have canceled the download of a fragment and need to adjust the buffer time or reload an init segment + * @param {object} e + */ + function _onFragmentLoadingAbandoned(e) { + logger.info('onFragmentLoadingAbandoned request: ' + e.request.url + ' has been aborted'); + + // we only need to handle this if we are not seeking, not switching the tracks and not switching the quality + if (!playbackController.isSeeking() && !scheduleController.getSwitchStrack() && !qualityChangeInProgress) { + logger.info('onFragmentLoadingAbandoned request: ' + e.request.url + ' has to be downloaded again, origin is not seeking process or switch track call'); + + // in case of an init segment we force the download of an init segment + if (e.request && e.request.isInitializationRequest()) { + scheduleController.setInitSegmentRequired(true); + } + + // in case of a media segment we reset the buffering time + else { + setExplicitBufferingTime(e.request.startTime + (e.request.duration / 2)); + } + + // In case of a seek the schedule controller was stopped and will be started once the buffer has been pruned. + scheduleController.startScheduleTimer(0); } } - function onQuotaExceeded(e) { - bufferingTime = e.quotaExceededTime; - bufferPruned = true; + /** + * When a fragment has been loaded we need to start the schedule timer again in case of an error. + * @param {object} e + */ + function _onFragmentLoadingCompleted(e) { + logger.info('OnFragmentLoadingCompleted for stream id ' + streamInfo.id + ' and media type ' + type + ' - Url:', e.request ? e.request.url : 'undefined', e.request.range ? ', Range:' + e.request.range : ''); + + if (getIsTextTrack()) { + scheduleController.startScheduleTimer(0); + } + + if (e.error && e.request.serviceLocation) { + _handleFragmentLoadingError(e); + } } - function addDVRMetric() { - const manifestInfo = streamInfo.manifestInfo; - const isDynamic = manifestInfo.isDynamic; - const range = timelineConverter.calcSegmentAvailabilityRange(representationController.getCurrentRepresentation(), isDynamic); - dashMetrics.addDVRInfo(getType(), playbackController.getTime(), manifestInfo, range); + /** + * If we encountered an error when loading the fragment we need to handle it according to the segment type + * @private + */ + function _handleFragmentLoadingError(e) { + logger.info(`Fragment loading completed with an error`); + + if (!e || !e.request || !e.request.type) { + return; + } + + // In case there are baseUrls that can still be tried a valid request can be generated. If no valid request can be generated we ran out of baseUrls. + // Consequently, we need to signal that we dont want to retry in case no valid request could be generated otherwise we keep trying with the same url infinitely. + + // Init segment could not be loaded. If we have multiple baseUrls we still have a chance to get a valid segment. + if (e.request.type === HTTPRequest.INIT_SEGMENT_TYPE) { + _onInitFragmentNeeded({ + representationId: e.request.representationId, + sender: {} + }, false) + } + + // Media segment could not be loaded + else if (e.request.type === HTTPRequest.MEDIA_SEGMENT_TYPE) { + setExplicitBufferingTime(e.request.startTime + (e.request.duration / 2)); + _onMediaFragmentNeeded({}, false); + } + } + + /** + * Callback function triggered by the TextController whenever a track is changed for fragmented text. Will only be triggered if textracks have previously been disabled. + * @private + */ + function _onSetFragmentedTextAfterDisabled() { + setExplicitBufferingTime(playbackController.getTime()); + getScheduleController().startScheduleTimer(); + } + + /** + * Callback function triggered by the TextController whenever a track is changed for non fragmented text + * @param {object} e + * @private + */ + function _onSetNonFragmentedText(e) { + const currentTrackInfo = e.currentTrackInfo; + + if (!currentTrackInfo) { + return; + } + + const mInfo = mediaInfoArr.find((info) => { + return info.index === currentTrackInfo.index && info.lang === currentTrackInfo.lang; + }); + + if (mInfo) { + selectMediaInfo(mInfo) + .then(() => { + bufferController.setIsBufferingCompleted(false); + setExplicitBufferingTime(playbackController.getTime()); + scheduleController.setInitSegmentRequired(true); + scheduleController.startScheduleTimer(); + }); + } + } + + function _onQuotaExceeded(e) { + // Stop scheduler (will be restarted once buffer is pruned) + setExplicitBufferingTime(e.quotaExceededTime); + scheduleController.clearScheduleTimer(); } function getRepresentationController() { @@ -309,10 +813,6 @@ function StreamProcessor(config) { return bufferController ? bufferController.getBuffer() : null; } - function setBuffer(buffer) { - bufferController.setBuffer(buffer); - } - function getBufferController() { return bufferController; } @@ -323,15 +823,20 @@ function StreamProcessor(config) { function updateStreamInfo(newStreamInfo) { streamInfo = newStreamInfo; - if (settings.get().streaming.useAppendWindow) { - bufferController.updateAppendWindow(); + if (!isBufferingCompleted()) { + return bufferController.updateAppendWindow(); } + return Promise.resolve(); } function getStreamInfo() { return streamInfo; } + /** + * Called once the StreamProcessor is initialized and when the track is switched. We only have one StreamProcessor per media type. So we need to adjust the mediaInfo once we switch/select a track. + * @param {object} newMediaInfo + */ function selectMediaInfo(newMediaInfo) { if (newMediaInfo !== mediaInfo && (!newMediaInfo || !mediaInfo || (newMediaInfo.type === mediaInfo.type))) { mediaInfo = newMediaInfo; @@ -342,19 +847,19 @@ function StreamProcessor(config) { if (representationController) { const realAdaptation = representationController.getData(); - const maxQuality = abrController.getTopQualityIndexFor(type, streamInfo.id); - const minIdx = abrController.getMinAllowedIndexFor(type); + const maxQuality = abrController.getMaxAllowedIndexFor(type, streamInfo.id); + const minIdx = abrController.getMinAllowedIndexFor(type, streamInfo.id); let quality, averageThroughput; let bitrate = null; - if ((realAdaptation === null || (realAdaptation.id != newRealAdaptation.id)) && type !== Constants.FRAGMENTED_TEXT) { - averageThroughput = abrController.getThroughputHistory().getAverageThroughput(type); - bitrate = averageThroughput || abrController.getInitialBitrateFor(type); - quality = abrController.getQualityForBitrate(mediaInfo, bitrate); + if ((realAdaptation === null || (realAdaptation.id !== newRealAdaptation.id)) && type !== Constants.TEXT) { + averageThroughput = abrController.getThroughputHistory().getAverageThroughput(type, isDynamic); + bitrate = averageThroughput || abrController.getInitialBitrateFor(type, streamInfo.id); + quality = abrController.getQualityForBitrate(mediaInfo, bitrate, streamInfo.id); } else { - quality = abrController.getQualityFor(type); + quality = abrController.getQualityFor(type, streamInfo.id); } if (minIdx !== undefined && quality < minIdx) { @@ -363,23 +868,20 @@ function StreamProcessor(config) { if (quality > maxQuality) { quality = maxQuality; } - indexHandler.setMimeType(mediaInfo ? mediaInfo.mimeType : null); - representationController.updateData(newRealAdaptation, voRepresentations, type, quality); + return representationController.updateData(newRealAdaptation, voRepresentations, type, mediaInfo.isFragmented, quality); + } else { + return Promise.resolve(); } } - function addMediaInfo(newMediaInfo, selectNewMediaInfo) { + function addMediaInfo(newMediaInfo) { if (mediaInfoArr.indexOf(newMediaInfo) === -1) { mediaInfoArr.push(newMediaInfo); } - - if (selectNewMediaInfo) { - this.selectMediaInfo(newMediaInfo); - } } - function getMediaInfoArr() { - return mediaInfoArr; + function clearMediaInfoArray() { + mediaInfoArr = []; } function getMediaInfo() { @@ -391,11 +893,7 @@ function StreamProcessor(config) { } function setMediaSource(mediaSource) { - bufferController.setMediaSource(mediaSource, getMediaInfoArr()); - } - - function dischargePreBuffer() { - bufferController.dischargePreBuffer(); + bufferController.setMediaSource(mediaSource); } function getScheduleController() { @@ -417,7 +915,7 @@ function StreamProcessor(config) { voRepresentation = representationController ? representationController.getCurrentRepresentation() : null; } - return adapter.convertDataToRepresentationInfo(voRepresentation); + return adapter.convertRepresentationToRepresentationInfo(voRepresentation); } function isBufferingCompleted() { @@ -428,42 +926,6 @@ function StreamProcessor(config) { return bufferController ? bufferController.getBufferLevel() : 0; } - function onInitFragmentNeeded(e) { - // Event propagation may have been stopped (see MssHandler) - if (!e.sender) return; - - if (adapter.getIsTextTrack(mimeType) && !textController.isTextEnabled()) return; - - if (bufferController && e.representationId) { - if (!bufferController.appendInitSegment(e.representationId)) { - // Init segment not in cache, send new request - const request = indexHandler ? indexHandler.getInitRequest(getMediaInfo(), representationController.getCurrentRepresentation()) : null; - scheduleController.processInitRequest(request); - } - } - } - - function onMediaFragmentNeeded(e) { - let request; - - // Don't schedule next fragments while pruning to avoid buffer inconsistencies - if (!bufferController.getIsPruningInProgress()) { - request = findNextRequest(e.seekTarget, e.replacement); - if (request) { - scheduleController.setSeekTarget(NaN); - if (!e.replacement) { - if (!isNaN(request.startTime + request.duration)) { - bufferingTime = request.startTime + request.duration; - } - request.delayLoadingTime = new Date().getTime() + scheduleController.getTimeToLoadDelay(); - scheduleController.setTimeToLoadDelay(0); - } - } - } - - scheduleController.processMediaRequest(request); - } - /** * Probe the next request. This is used in the CMCD model to get information about the upcoming request. Note: No actual request is performed here. * @return {FragmentRequest|null} @@ -474,94 +936,45 @@ function StreamProcessor(config) { const representation = representationController && representationInfo ? representationController.getRepresentationForQuality(representationInfo.quality) : null; - let request = indexHandler.getNextSegmentRequestIdempotent( - getMediaInfo(), + let request = dashHandler.getNextSegmentRequestIdempotent( + mediaInfo, representation ); return request; } - function findNextRequest(seekTarget, requestToReplace) { - const representationInfo = getRepresentationInfo(); - const hasSeekTarget = !isNaN(seekTarget); - const currentTime = playbackController.getNormalizedTime(); - let time = hasSeekTarget ? seekTarget : bufferingTime; - let bufferIsDivided = false; - let request; - - if (isNaN(time) || (getType() === Constants.FRAGMENTED_TEXT && !textController.isTextEnabled())) { - return null; - } - /** - * This is critical for IE/Safari/EDGE - * */ - if (bufferController) { - let range = bufferController.getRangeAt(time); - const playingRange = bufferController.getRangeAt(currentTime); - if ((range !== null || playingRange !== null) && !hasSeekTarget) { - if (!range || (playingRange && playingRange.start != range.start && playingRange.end != range.end)) { - const hasDiscontinuities = bufferController.getBuffer().hasDiscontinuitiesAfter(currentTime); - if (hasDiscontinuities && getType() !== Constants.FRAGMENTED_TEXT) { - fragmentModel.removeExecutedRequestsAfterTime(playingRange.end); - bufferIsDivided = true; - } - } - } - } - - if (requestToReplace) { - time = requestToReplace.startTime + (requestToReplace.duration / 2); - request = getFragmentRequest(representationInfo, time, { - timeThreshold: 0, - ignoreIsFinished: true - }); - } else { - // Use time just whenever is strictly needed - const useTime = hasSeekTarget || bufferPruned || bufferIsDivided; - request = getFragmentRequest(representationInfo, - useTime ? time : undefined, { - keepIdx: !useTime - }); - bufferPruned = false; - - // Then, check if this request was downloaded or not - while (request && request.action !== FragmentRequest.ACTION_COMPLETE && fragmentModel.isFragmentLoaded(request)) { - // loop until we found not loaded fragment, or no fragment - request = getFragmentRequest(representationInfo); - } - } - - return request; - } - - function onMediaFragmentLoaded(e) { + function _onMediaFragmentLoaded(e) { const chunk = e.chunk; const bytes = chunk.bytes; const quality = chunk.quality; const currentRepresentation = getRepresentationInfo(quality); - const voRepresentation = representationController && currentRepresentation ? representationController.getRepresentationForQuality(currentRepresentation.quality) : null; - const eventStreamMedia = adapter.getEventsFor(currentRepresentation.mediaInfo); - const eventStreamTrack = adapter.getEventsFor(currentRepresentation, voRepresentation); - - if (eventStreamMedia && eventStreamMedia.length > 0 || eventStreamTrack && eventStreamTrack.length > 0) { - const request = fragmentModel.getRequests({ - state: FragmentModel.FRAGMENT_MODEL_EXECUTED, - quality: quality, - index: chunk.index - })[0]; - const events = handleInbandEvents(bytes, request, eventStreamMedia, eventStreamTrack); - eventBus.trigger(Events.INBAND_EVENTS, - { events: events }, - { streamId: streamInfo.id } - ); + // If we switch tracks this event might be fired after the representations in the RepresentationController have been updated according to the new MediaInfo. + // In this case there will be no currentRepresentation and voRepresentation matching the "old" quality + if (currentRepresentation && voRepresentation) { + const eventStreamMedia = adapter.getEventsFor(currentRepresentation.mediaInfo, null, streamInfo); + const eventStreamTrack = adapter.getEventsFor(currentRepresentation, voRepresentation, streamInfo); + + if (eventStreamMedia && eventStreamMedia.length > 0 || eventStreamTrack && eventStreamTrack.length > 0) { + const request = fragmentModel.getRequests({ + state: FragmentModel.FRAGMENT_MODEL_EXECUTED, + quality: quality, + index: chunk.index + })[0]; + + const events = _handleInbandEvents(bytes, request, eventStreamMedia, eventStreamTrack); + eventBus.trigger(Events.INBAND_EVENTS, + { events: events }, + { streamId: streamInfo.id } + ); + } } } - function handleInbandEvents(data, request, mediaInbandEvents, trackInbandEvents) { + function _handleInbandEvents(data, request, mediaInbandEvents, trackInbandEvents) { try { const eventStreams = {}; const events = []; @@ -598,160 +1011,158 @@ function StreamProcessor(config) { } } - function createBuffer(previousBuffers) { - return (getBuffer() || bufferController ? bufferController.createBuffer(mediaInfoArr, previousBuffers) : null); - } - - function switchTrackAsked() { - scheduleController.switchTrackAsked(); - } - - function createBufferControllerForType(type) { - let controller = null; + function createBufferSinks(previousBufferSinks) { + const buffer = getBuffer(); - if (!type) { - errHandler.error(new DashJSError(Errors.MEDIASOURCE_TYPE_UNSUPPORTED_CODE, Errors.MEDIASOURCE_TYPE_UNSUPPORTED_MESSAGE + 'not properly defined')); - return null; - } - - if (type === Constants.VIDEO || type === Constants.AUDIO) { - controller = BufferController(context).create({ - streamInfo: streamInfo, - type: type, - mediaPlayerModel: mediaPlayerModel, - manifestModel: manifestModel, - fragmentModel: fragmentModel, - errHandler: errHandler, - mediaController: mediaController, - representationController: representationController, - adapter: adapter, - textController: textController, - abrController: abrController, - playbackController: playbackController, - settings: settings - }); - } else { - controller = TextBufferController(context).create({ - streamInfo: streamInfo, - type: type, - mimeType: mimeType, - mediaPlayerModel: mediaPlayerModel, - manifestModel: manifestModel, - fragmentModel: fragmentModel, - errHandler: errHandler, - mediaController: mediaController, - representationController: representationController, - adapter: adapter, - textController: textController, - abrController: abrController, - playbackController: playbackController, - settings: settings - }); + if (buffer) { + return Promise.resolve(buffer); } - return controller; + return bufferController ? bufferController.createBufferSink(mediaInfo, previousBufferSinks) : Promise.resolve(null); } + function prepareTrackSwitch() { + return new Promise((resolve) => { + logger.debug(`Preparing track switch for type ${type}`); + const shouldReplace = type === Constants.TEXT || (settings.get().streaming.trackSwitchMode[type] === Constants.TRACK_SWITCH_MODE_ALWAYS_REPLACE && playbackController.getTimeToStreamEnd(streamInfo) > settings.get().streaming.buffer.stallThreshold); + + // when buffering is completed and we are not supposed to replace anything do nothing. + // Still we need to trigger preloading again and call change type in case user seeks back before transitioning to next period + if (bufferController.getIsBufferingCompleted() && !shouldReplace) { + bufferController.prepareForNonReplacementTrackSwitch(mediaInfo.codec) + .then(() => { + eventBus.trigger(Events.BUFFERING_COMPLETED, {}, { streamId: streamInfo.id, mediaType: type }) + }) + .catch(() => { + eventBus.trigger(Events.BUFFERING_COMPLETED, {}, { streamId: streamInfo.id, mediaType: type }) + }) + resolve(); + return; + } - function getLiveStartTime() { - if (!isDynamic) return NaN; - if (!liveEdgeFinder) return NaN; - - let liveStartTime = NaN; - const currentRepresentationInfo = getRepresentationInfo(); - const liveEdge = liveEdgeFinder.getLiveEdge(currentRepresentationInfo); - - if (isNaN(liveEdge)) { - return NaN; - } - - const request = findRequestForLiveEdge(liveEdge, currentRepresentationInfo); - - if (request) { - // When low latency mode is selected but browser doesn't support fetch - // start at the beginning of the segment to avoid consuming the whole buffer - if (settings.get().streaming.lowLatencyEnabled) { - liveStartTime = request.duration < mediaPlayerModel.getLiveDelay() ? request.startTime : request.startTime + request.duration - mediaPlayerModel.getLiveDelay(); + // We stop the schedule controller and signal a track switch. That way we request a new init segment next + scheduleController.clearScheduleTimer(); + scheduleController.setSwitchTrack(true); + + // when we are supposed to replace it does not matter if buffering is already completed + if (shouldReplace) { + // Inform other classes like the GapController that we are replacing existing stuff + eventBus.trigger(Events.BUFFER_REPLACEMENT_STARTED, { + mediaType: type, + streamId: streamInfo.id + }, { mediaType: type, streamId: streamInfo.id }); + + // Abort the current request it will be removed from the buffer anyways + fragmentModel.abortRequests(); + + // Abort appending segments to the buffer. Also adjust the appendWindow as we might have been in the progress of prebuffering stuff. + bufferController.prepareForReplacementTrackSwitch(mediaInfo.codec) + .then(() => { + // Timestamp offset couldve been changed by preloading period + const representationInfo = getRepresentationInfo(); + return bufferController.updateBufferTimestampOffset(representationInfo); + }) + .then(() => { + _bufferClearedForReplacement(); + resolve(); + }) + .catch(() => { + _bufferClearedForReplacement(); + resolve(); + }); } else { - liveStartTime = request.startTime; + // We do not replace anything that is already in the buffer. Still we need to prepare the buffer for the track switch + bufferController.prepareForNonReplacementTrackSwitch(mediaInfo.codec) + .then(() => { + _bufferClearedForNonReplacement(); + resolve(); + }) + .catch(() => { + _bufferClearedForNonReplacement(); + resolve(); + }); } - } + }) - return liveStartTime; } - function findRequestForLiveEdge(liveEdge, currentRepresentationInfo) { - try { - let request = null; - let liveDelay = playbackController.getLiveDelay(); - const dvrWindowSize = !isNaN(streamInfo.manifestInfo.DVRWindowSize) ? streamInfo.manifestInfo.DVRWindowSize : liveDelay; - const dvrWindowSafetyMargin = 0.1 * dvrWindowSize; - let startTime; - - // Make sure that we have at least a valid request for the end of the DVR window, otherwise we might try forever - if (!isFinite(dvrWindowSize) || getFragmentRequest(currentRepresentationInfo, liveEdge - dvrWindowSize + dvrWindowSafetyMargin, { - ignoreIsFinished: true - })) { - - // Try to find a request as close as possible to the targeted live edge - while (!request && liveDelay <= dvrWindowSize) { - startTime = liveEdge - liveDelay; - request = getFragmentRequest(currentRepresentationInfo, startTime, { - ignoreIsFinished: true - }); - if (!request) { - liveDelay += 1; // Increase by one second for each iteration - } - } - } + /** + * For an instant track switch we need to adjust the buffering time after the buffer has been pruned. + * @private + */ + function _bufferClearedForReplacement() { + const targetTime = playbackController.getTime(); - if (request) { - playbackController.setLiveDelay(liveDelay, true); - } - logger.debug('live edge: ' + liveEdge + ', live delay: ' + liveDelay + ', live target: ' + startTime); - return request; - } catch (e) { - return null; + if (settings.get().streaming.buffer.flushBufferAtTrackSwitch) { + // For some devices (like chromecast) it is necessary to seek the video element to reset the internal decoding buffer, + // otherwise audio track switch will be effective only once after previous buffered track is consumed + playbackController.seek(targetTime + 0.001, false, true); } - } - function onSeekTarget(e) { - bufferingTime = e.time; - scheduleController.setSeekTarget(e.time); + setExplicitBufferingTime(targetTime); + bufferController.setSeekTarget(targetTime); + scheduleController.startScheduleTimer(); } - function setBufferingTime(value) { - bufferingTime = value; - } + function _bufferClearedForNonReplacement() { + const time = playbackController.getTime(); + const continuousBufferTime = bufferController.getContinuousBufferTimeForTargetTime(time); + const targetTime = isNaN(continuousBufferTime) ? time : continuousBufferTime; - function resetIndexHandler() { - if (indexHandler) { - indexHandler.resetIndex(); - } + setExplicitBufferingTime(targetTime); + scheduleController.startScheduleTimer(); } - function getInitRequest(quality) { - checkInteger(quality); - const representation = representationController ? representationController.getRepresentationForQuality(quality) : null; - return indexHandler ? indexHandler.getInitRequest(getMediaInfo(), representation) : null; - } - function getFragmentRequest(representationInfo, time, options) { - let fragRequest = null; + function _createBufferControllerForType(type, isFragmented) { + let controller = null; + + if (!type) { + errHandler.error(new DashJSError(Errors.MEDIASOURCE_TYPE_UNSUPPORTED_CODE, Errors.MEDIASOURCE_TYPE_UNSUPPORTED_MESSAGE + 'not properly defined')); + return null; + } - if (indexHandler) { - const representation = representationController && representationInfo ? representationController.getRepresentationForQuality(representationInfo.quality) : null; + if (type === Constants.TEXT && !isFragmented) { + controller = NotFragmentedTextBufferController(context).create({ + streamInfo, + type, + mimeType, + fragmentModel, + textController, + errHandler, + settings + }); + } else { + controller = BufferController(context).create({ + streamInfo, + type, + mediaPlayerModel, + manifestModel, + fragmentModel, + errHandler, + mediaController, + representationController, + adapter, + textController, + abrController, + playbackController, + settings + }); + } - // if time and options are undefined, it means the next segment is requested - // otherwise, the segment at this specific time is requested. - if (time !== undefined && options !== undefined) { - fragRequest = indexHandler.getSegmentRequestForTime(getMediaInfo(), representation, time, options); - } else { - fragRequest = indexHandler.getNextSegmentRequest(getMediaInfo(), representation); - } + return controller; + } + + function _onSeekTarget(e) { + if (e && !isNaN(e.time)) { + setExplicitBufferingTime(e.time); + bufferController.setSeekTarget(e.time); } + } - return fragRequest; + function setExplicitBufferingTime(value) { + bufferingTime = value; + shouldUseExplicitTimeForRequest = true; } function finalisePlayList(time, reason) { @@ -759,38 +1170,35 @@ function StreamProcessor(config) { } instance = { - initialize: initialize, - getStreamId: getStreamId, - getType: getType, - isUpdating: isUpdating, - getBufferController: getBufferController, - getFragmentModel: getFragmentModel, - getScheduleController: getScheduleController, - getRepresentationController: getRepresentationController, - getRepresentationInfo: getRepresentationInfo, - getBufferLevel: getBufferLevel, - isBufferingCompleted: isBufferingCompleted, - createBuffer: createBuffer, - updateStreamInfo: updateStreamInfo, - getStreamInfo: getStreamInfo, - selectMediaInfo: selectMediaInfo, - addMediaInfo: addMediaInfo, - getLiveStartTime: getLiveStartTime, - switchTrackAsked: switchTrackAsked, - getMediaInfoArr: getMediaInfoArr, - getMediaInfo: getMediaInfo, - getMediaSource: getMediaSource, - setMediaSource: setMediaSource, - dischargePreBuffer: dischargePreBuffer, - getBuffer: getBuffer, - setBuffer: setBuffer, - setBufferingTime: setBufferingTime, - resetIndexHandler: resetIndexHandler, - getInitRequest: getInitRequest, - getFragmentRequest: getFragmentRequest, - finalisePlayList: finalisePlayList, - probeNextRequest: probeNextRequest, - reset: reset + initialize, + getStreamId, + getType, + isUpdating, + getBufferController, + getFragmentModel, + getScheduleController, + getRepresentationController, + getRepresentationInfo, + getBufferLevel, + isBufferingCompleted, + createBufferSinks, + updateStreamInfo, + getStreamInfo, + selectMediaInfo, + clearMediaInfoArray, + addMediaInfo, + prepareTrackSwitch, + prepareQualityChange, + getMediaInfo, + getMediaSource, + setMediaSource, + getBuffer, + setExplicitBufferingTime, + finalisePlayList, + probeNextRequest, + prepareInnerPeriodPlaybackSeeking, + prepareOuterPeriodPlaybackSeeking, + reset }; setup(); diff --git a/src/streaming/XlinkLoader.js b/src/streaming/XlinkLoader.js index d614a181f2..814def45f9 100644 --- a/src/streaming/XlinkLoader.js +++ b/src/streaming/XlinkLoader.js @@ -50,7 +50,6 @@ function XlinkLoader(config) { dashMetrics: config.dashMetrics, mediaPlayerModel: config.mediaPlayerModel, requestModifier: config.requestModifier, - useFetch: config.settings ? config.settings.get().streaming.lowLatencyEnabled : null, errors: Errors }); diff --git a/src/streaming/constants/ConformanceViolationConstants.js b/src/streaming/constants/ConformanceViolationConstants.js index 21cd0cd33b..d6cbe045ac 100644 --- a/src/streaming/constants/ConformanceViolationConstants.js +++ b/src/streaming/constants/ConformanceViolationConstants.js @@ -43,6 +43,10 @@ export default { NON_COMPLIANT_SMPTE_IMAGE_ATTRIBUTE: { key: 'NON_COMPLIANT_SMPTE_IMAGE_ATTRIBUTE', message: 'SMPTE 2052-1:2013 defines the attribute name as "imageType" and does not define "imagetype"' + }, + INVALID_DVR_WINDOW: { + key: 'INVALID_DVR_WINDOW', + message: 'No valid segment found when applying a specification compliant DVR window calculation. Using SegmentTimeline entries as a fallback.' } } }; diff --git a/src/streaming/constants/Constants.js b/src/streaming/constants/Constants.js index 3d346ce91d..7b905f5094 100644 --- a/src/streaming/constants/Constants.js +++ b/src/streaming/constants/Constants.js @@ -66,20 +66,6 @@ class Constants { */ this.TEXT = 'text'; - /** - * @constant {string} FRAGMENTED_TEXT Fragmented text media type - * @memberof Constants# - * @static - */ - this.FRAGMENTED_TEXT = 'fragmentedText'; - - /** - * @constant {string} EMBEDDED_TEXT Embedded text media type - * @memberof Constants# - * @static - */ - this.EMBEDDED_TEXT = 'embeddedText'; - /** * @constant {string} MUXED Muxed (video/audio in the same chunk) media type * @memberof Constants# @@ -122,6 +108,13 @@ class Constants { */ this.WVTT = 'wvtt'; + /** + * @constant {string} Content Steering + * @memberof Constants# + * @static + */ + this.CONTENT_STEERING = 'contentSteering'; + /** * @constant {string} ABR_STRATEGY_DYNAMIC Dynamic Adaptive bitrate algorithm * @memberof Constants# @@ -171,6 +164,13 @@ class Constants { */ this.ABR_FETCH_THROUGHPUT_CALCULATION_MOOF_PARSING = 'abrFetchThroughputCalculationMoofParsing'; + /** + * @constant {string} ABR_FETCH_THROUGHPUT_CALCULATION_AAST Throughput calculation based on adjusted availability start time in low latency mode + * @memberof Constants# + * @static + */ + this.ABR_FETCH_THROUGHPUT_CALCULATION_AAST = 'abrFetchThroughputCalculationAAST'; + /** * @constant {string} LIVE_CATCHUP_MODE_DEFAULT Throughput calculation based on moof parsing * @memberof Constants# @@ -255,6 +255,13 @@ class Constants { */ this.TRACK_SELECTION_MODE_WIDEST_RANGE = 'widestRange'; + /** + * @constant {string} TRACK_SELECTION_MODE_WIDEST_RANGE makes the player select the track with the highest selectionPriority as defined in the manifest + * @memberof Constants# + * @static + */ + this.TRACK_SELECTION_MODE_HIGHEST_SELECTION_PRIORITY = 'highestSelectionPriority'; + /** * @constant {string} CMCD_MODE_QUERY specifies to attach CMCD metrics as query parameters. * @memberof Constants# @@ -279,12 +286,23 @@ class Constants { this.UTF8 = 'utf-8'; this.SCHEME_ID_URI = 'schemeIdUri'; this.START_TIME = 'starttime'; - this.SERVICE_DESCRIPTION_LL_SCHEME = 'urn:dvb:dash:lowlatency:scope:2019'; - this.SUPPLEMENTAL_PROPERTY_LL_SCHEME = 'urn:dvb:dash:lowlatency:critical:2019'; + this.SERVICE_DESCRIPTION_DVB_LL_SCHEME = 'urn:dvb:dash:lowlatency:scope:2019'; + this.SUPPLEMENTAL_PROPERTY_DVB_LL_SCHEME = 'urn:dvb:dash:lowlatency:critical:2019'; this.XML = 'XML'; this.ARRAY_BUFFER = 'ArrayBuffer'; this.DVB_REPORTING_URL = 'dvb:reportingUrl'; this.DVB_PROBABILITY = 'dvb:probability'; + this.VIDEO_ELEMENT_READY_STATES = { + HAVE_NOTHING: 0, + HAVE_METADATA: 1, + HAVE_CURRENT_DATA: 2, + HAVE_FUTURE_DATA: 3, + HAVE_ENOUGH_DATA: 4 + }; + this.FILE_LOADER_TYPES = { + FETCH: 'fetch_loader', + XHR: 'xhr_loader' + } } constructor() { diff --git a/src/streaming/constants/MetricsConstants.js b/src/streaming/constants/MetricsConstants.js index 04058cc872..edf9c719c3 100644 --- a/src/streaming/constants/MetricsConstants.js +++ b/src/streaming/constants/MetricsConstants.js @@ -55,6 +55,7 @@ class MetricsConstants { this.MANIFEST_UPDATE_TRACK_INFO = 'ManifestUpdateRepresentationInfo'; this.PLAY_LIST = 'PlayList'; this.DVB_ERRORS = 'DVBErrors'; + this.HTTP_REQUEST_DVB_REPORTING_TYPE = 'DVBReporting'; } constructor() { diff --git a/src/streaming/constants/ProtectionConstants.js b/src/streaming/constants/ProtectionConstants.js index b3dccfaad5..904ac9c217 100644 --- a/src/streaming/constants/ProtectionConstants.js +++ b/src/streaming/constants/ProtectionConstants.js @@ -40,6 +40,10 @@ class ProtectionConstants { this.CLEARKEY_KEYSTEM_STRING = 'org.w3.clearkey'; this.WIDEVINE_KEYSTEM_STRING = 'com.widevine.alpha'; this.PLAYREADY_KEYSTEM_STRING = 'com.microsoft.playready'; + this.PLAYREADY_RECOMMENDATION_KEYSTEM_STRING = 'com.microsoft.playready.recommendation'; + this.INITIALIZATION_DATA_TYPE_CENC = 'cenc'; + this.INITIALIZATION_DATA_TYPE_KEYIDS = 'keyids' + this.INITIALIZATION_DATA_TYPE_WEBM = 'webm' } constructor () { diff --git a/src/streaming/controllers/AbrController.js b/src/streaming/controllers/AbrController.js index 71ab8a9f6d..3bf7ff9009 100644 --- a/src/streaming/controllers/AbrController.js +++ b/src/streaming/controllers/AbrController.js @@ -43,8 +43,9 @@ import SwitchRequestHistory from '../rules/SwitchRequestHistory'; import DroppedFramesHistory from '../rules/DroppedFramesHistory'; import ThroughputHistory from '../rules/ThroughputHistory'; import Debug from '../../core/Debug'; -import { HTTPRequest } from '../vo/metrics/HTTPRequest'; -import { checkInteger } from '../utils/SupervisorTools'; +import {HTTPRequest} from '../vo/metrics/HTTPRequest'; +import {checkInteger} from '../utils/SupervisorTools'; +import MediaPlayerEvents from '../MediaPlayerEvents'; const DEFAULT_VIDEO_BITRATE = 1000; const DEFAULT_AUDIO_BITRATE = 100; @@ -71,14 +72,15 @@ function AbrController() { adapter, videoModel, mediaPlayerModel, + customParametersModel, domStorage, playbackIndex, switchHistoryDict, droppedFramesHistory, throughputHistory, - isUsingBufferOccupancyABRDict, - isUsingL2AABRDict, - isUsingLoLPBRDict, + isUsingBufferOccupancyAbrDict, + isUsingL2AAbrDict, + isUsingLoLPAbrDict, dashMetrics, settings; @@ -87,40 +89,106 @@ function AbrController() { resetInitialSettings(); } + /** + * Initialize everything that is not Stream specific. We only have one instance of the ABR Controller for all periods. + */ + function initialize() { + droppedFramesHistory = DroppedFramesHistory(context).create(); + throughputHistory = ThroughputHistory(context).create({ + settings + }); + + abrRulesCollection = ABRRulesCollection(context).create({ + dashMetrics, + customParametersModel, + mediaPlayerModel, + settings + }); + + abrRulesCollection.initialize(); + + eventBus.on(MediaPlayerEvents.QUALITY_CHANGE_RENDERED, _onQualityChangeRendered, instance); + eventBus.on(MediaPlayerEvents.METRIC_ADDED, _onMetricAdded, instance); + eventBus.on(Events.LOADING_PROGRESS, _onFragmentLoadProgress, instance); + } + + /** + * Whenever a StreamProcessor is created it is added to the list of streamProcessorDict + * In addition, the corresponding objects for this object and its stream id are created + * @param {object} type + * @param {object} streamProcessor + */ function registerStreamType(type, streamProcessor) { - switchHistoryDict[type] = switchHistoryDict[type] || SwitchRequestHistory(context).create(); - streamProcessorDict[type] = streamProcessor; - abandonmentStateDict[type] = abandonmentStateDict[type] || {}; - abandonmentStateDict[type].state = MetricsConstants.ALLOW_LOAD; - isUsingBufferOccupancyABRDict[type] = false; - isUsingL2AABRDict[type] = false; - isUsingLoLPBRDict[type] = false; - eventBus.on(Events.LOADING_PROGRESS, onFragmentLoadProgress, instance); + const streamId = streamProcessor.getStreamInfo().id; + + if (!streamProcessorDict[streamId]) { + streamProcessorDict[streamId] = {}; + } + + if (!switchHistoryDict[streamId]) { + switchHistoryDict[streamId] = {}; + } + + if (!abandonmentStateDict[streamId]) { + abandonmentStateDict[streamId] = {}; + } + + switchHistoryDict[streamId][type] = SwitchRequestHistory(context).create(); + streamProcessorDict[streamId][type] = streamProcessor; + + abandonmentStateDict[streamId][type] = {}; + abandonmentStateDict[streamId][type].state = MetricsConstants.ALLOW_LOAD; + + _initializeAbrStrategy(type); + if (type === Constants.VIDEO) { - eventBus.on(Events.QUALITY_CHANGE_RENDERED, onQualityChangeRendered, instance); - droppedFramesHistory = droppedFramesHistory || DroppedFramesHistory(context).create(); setElementSize(); } - eventBus.on(Events.METRIC_ADDED, onMetricAdded, instance); - eventBus.on(Events.PERIOD_SWITCH_COMPLETED, createAbrRulesCollection, instance); - - throughputHistory = throughputHistory || ThroughputHistory(context).create({ - settings: settings - }); } - function unRegisterStreamType(type) { - delete streamProcessorDict[type]; + function _initializeAbrStrategy(type) { + const strategy = settings.get().streaming.abr.ABRStrategy; + + if (strategy === Constants.ABR_STRATEGY_L2A) { + isUsingBufferOccupancyAbrDict[type] = false; + isUsingLoLPAbrDict[type] = false; + isUsingL2AAbrDict[type] = true; + } else if (strategy === Constants.ABR_STRATEGY_LoLP) { + isUsingBufferOccupancyAbrDict[type] = false; + isUsingLoLPAbrDict[type] = true; + isUsingL2AAbrDict[type] = false; + } else if (strategy === Constants.ABR_STRATEGY_BOLA) { + isUsingBufferOccupancyAbrDict[type] = true; + isUsingLoLPAbrDict[type] = false; + isUsingL2AAbrDict[type] = false; + } else if (strategy === Constants.ABR_STRATEGY_THROUGHPUT) { + isUsingBufferOccupancyAbrDict[type] = false; + isUsingLoLPAbrDict[type] = false; + isUsingL2AAbrDict[type] = false; + } else if (strategy === Constants.ABR_STRATEGY_DYNAMIC) { + isUsingBufferOccupancyAbrDict[type] = isUsingBufferOccupancyAbrDict && isUsingBufferOccupancyAbrDict[type] ? isUsingBufferOccupancyAbrDict[type] : false; + isUsingLoLPAbrDict[type] = false; + isUsingL2AAbrDict[type] = false; + } } - function createAbrRulesCollection() { - abrRulesCollection = ABRRulesCollection(context).create({ - dashMetrics: dashMetrics, - mediaPlayerModel: mediaPlayerModel, - settings: settings - }); + function unRegisterStreamType(streamId, type) { + try { + if (streamProcessorDict[streamId] && streamProcessorDict[streamId][type]) { + delete streamProcessorDict[streamId][type]; + } - abrRulesCollection.initialize(); + if (switchHistoryDict[streamId] && switchHistoryDict[streamId][type]) { + delete switchHistoryDict[streamId][type]; + } + + if (abandonmentStateDict[streamId] && abandonmentStateDict[streamId][type]) { + delete abandonmentStateDict[streamId][type]; + } + + } catch (e) { + + } } function resetInitialSettings() { @@ -129,12 +197,17 @@ function AbrController() { abandonmentStateDict = {}; streamProcessorDict = {}; switchHistoryDict = {}; - isUsingBufferOccupancyABRDict = {}; - isUsingL2AABRDict = {}; - isUsingLoLPBRDict = {}; + isUsingBufferOccupancyAbrDict = {}; + isUsingL2AAbrDict = {}; + isUsingLoLPAbrDict = {}; + if (windowResizeEventCalled === undefined) { windowResizeEventCalled = false; } + if (droppedFramesHistory) { + droppedFramesHistory.reset(); + } + playbackIndex = undefined; droppedFramesHistory = undefined; throughputHistory = undefined; @@ -146,10 +219,9 @@ function AbrController() { resetInitialSettings(); - eventBus.off(Events.LOADING_PROGRESS, onFragmentLoadProgress, instance); - eventBus.off(Events.QUALITY_CHANGE_RENDERED, onQualityChangeRendered, instance); - eventBus.off(Events.METRIC_ADDED, onMetricAdded, instance); - eventBus.off(Events.PERIOD_SWITCH_COMPLETED, createAbrRulesCollection, instance); + eventBus.off(Events.LOADING_PROGRESS, _onFragmentLoadProgress, instance); + eventBus.off(MediaPlayerEvents.QUALITY_CHANGE_RENDERED, _onQualityChangeRendered, instance); + eventBus.off(MediaPlayerEvents.METRIC_ADDED, _onMetricAdded, instance); if (abrRulesCollection) { abrRulesCollection.reset(); @@ -168,6 +240,9 @@ function AbrController() { if (config.mediaPlayerModel) { mediaPlayerModel = config.mediaPlayerModel; } + if (config.customParametersModel) { + customParametersModel = config.customParametersModel; + } if (config.dashMetrics) { dashMetrics = config.dashMetrics; } @@ -188,16 +263,84 @@ function AbrController() { } } - function onQualityChangeRendered(e) { + /** + * While fragment loading is in progress we check if we might need to abort the request + * @param {object} e + * @private + */ + function _onFragmentLoadProgress(e) { + const type = e.request.mediaType; + const streamId = e.streamId; + + if (!type || !streamId || !streamProcessorDict[streamId] || !settings.get().streaming.abr.autoSwitchBitrate[type]) { + return; + } + + const streamProcessor = streamProcessorDict[streamId][type]; + if (!streamProcessor) { + return; + } + + const rulesContext = RulesContext(context).create({ + abrController: instance, + streamProcessor: streamProcessor, + currentRequest: e.request, + useBufferOccupancyABR: isUsingBufferOccupancyAbrDict[type], + useL2AABR: isUsingL2AAbrDict[type], + useLoLPABR: isUsingLoLPAbrDict[type], + videoModel + }); + const switchRequest = abrRulesCollection.shouldAbandonFragment(rulesContext, streamId); + + if (switchRequest.quality > SwitchRequest.NO_CHANGE) { + const fragmentModel = streamProcessor.getFragmentModel(); + const request = fragmentModel.getRequests({ + state: FragmentModel.FRAGMENT_MODEL_LOADING, + index: e.request.index + })[0]; + if (request) { + abandonmentStateDict[streamId][type].state = MetricsConstants.ABANDON_LOAD; + switchHistoryDict[streamId][type].reset(); + switchHistoryDict[streamId][type].push({ + oldValue: getQualityFor(type, streamId), + newValue: switchRequest.quality, + confidence: 1, + reason: switchRequest.reason + }); + setPlaybackQuality(type, streamController.getActiveStreamInfo(), switchRequest.quality, switchRequest.reason); + + clearTimeout(abandonmentTimeout); + abandonmentTimeout = setTimeout( + () => { + abandonmentStateDict[streamId][type].state = MetricsConstants.ALLOW_LOAD; + abandonmentTimeout = null; + }, + settings.get().streaming.abandonLoadTimeout + ); + } + } + } + + /** + * Update dropped frames history when the quality was changed + * @param {object} e + * @private + */ + function _onQualityChangeRendered(e) { if (e.mediaType === Constants.VIDEO) { if (playbackIndex !== undefined) { - droppedFramesHistory.push(playbackIndex, videoModel.getPlaybackQuality()); + droppedFramesHistory.push(e.streamId, playbackIndex, videoModel.getPlaybackQuality()); } playbackIndex = e.newQuality; } } - function onMetricAdded(e) { + /** + * When the buffer level is updated we check if we need to change the ABR strategy + * @param e + * @private + */ + function _onMetricAdded(e) { if (e.metric === MetricsConstants.HTTP_REQUEST && e.value && e.value.type === HTTPRequest.MEDIA_SEGMENT_TYPE && (e.mediaType === Constants.AUDIO || e.mediaType === Constants.VIDEO)) { throughputHistory.push(e.mediaType, e.value, settings.get().streaming.abr.useDeadTimeLatency); } @@ -207,57 +350,217 @@ function AbrController() { } } - function getTopQualityIndexFor(type, id) { - let idx; - topQualities[id] = topQualities[id] || {}; + /** + * Returns the highest possible index taking limitations like maxBitrate, representationRatio and portal size into account. + * @param {string} type + * @param {string} streamId + * @return {number} + */ + function getMaxAllowedIndexFor(type, streamId) { + try { + let idx; + topQualities[streamId] = topQualities[streamId] || {}; + + if (!topQualities[streamId].hasOwnProperty(type)) { + topQualities[streamId][type] = 0; + } + + idx = _checkMaxBitrate(type, streamId); + idx = _checkMaxRepresentationRatio(idx, type, streamId); + idx = _checkPortalSize(idx, type, streamId); + return idx; + } catch (e) { + return undefined + } + } + + /** + * Returns the minimum allowed index. We consider thresholds defined in the settings, i.e. minBitrate for the corresponding media type. + * @param {string} type + * @param {string} streamId + * @return {undefined|number} + */ + function getMinAllowedIndexFor(type, streamId) { + try { + return _getMinIndexBasedOnBitrateFor(type, streamId); + } catch (e) { + return undefined + } + } + + /** + * Returns the maximum allowed index. + * @param {string} type + * @param {string} streamId + * @return {undefined|number} + */ + function _getMaxIndexBasedOnBitrateFor(type, streamId) { + try { + const maxBitrate = mediaPlayerModel.getAbrBitrateParameter('maxBitrate', type); + if (maxBitrate > -1) { + return getQualityForBitrate(streamProcessorDict[streamId][type].getMediaInfo(), maxBitrate, streamId); + } else { + return undefined; + } + } catch (e) { + return undefined + } + } + + /** + * Returns the minimum allowed index. + * @param {string} type + * @param {string} streamId + * @return {undefined|number} + */ + function _getMinIndexBasedOnBitrateFor(type, streamId) { + try { + const minBitrate = mediaPlayerModel.getAbrBitrateParameter('minBitrate', type); + + if (minBitrate > -1) { + const mediaInfo = streamProcessorDict[streamId][type].getMediaInfo(); + const bitrateList = getBitrateList(mediaInfo); + // This returns the quality index <= for the given bitrate + let minIdx = getQualityForBitrate(mediaInfo, minBitrate, streamId); + if (bitrateList[minIdx] && minIdx < bitrateList.length - 1 && bitrateList[minIdx].bitrate < minBitrate * 1000) { + minIdx++; // Go to the next bitrate + } + return minIdx; + } else { + return undefined; + } + } catch (e) { + return undefined; + } + } + + /** + * Returns the maximum possible index + * @param type + * @param streamId + * @return {number|*} + */ + function _checkMaxBitrate(type, streamId) { + let idx = topQualities[streamId][type]; + let newIdx = idx; + + if (!streamProcessorDict[streamId] || !streamProcessorDict[streamId][type]) { + return newIdx; + } + + const minIdx = getMinAllowedIndexFor(type, streamId); + if (minIdx !== undefined) { + newIdx = Math.max(idx, minIdx); + } + + const maxIdx = _getMaxIndexBasedOnBitrateFor(type, streamId); + if (maxIdx !== undefined) { + newIdx = Math.min(newIdx, maxIdx); + } + + return newIdx; + } + + /** + * Returns the maximum index according to maximum representation ratio + * @param idx + * @param type + * @param streamId + * @return {number|*} + * @private + */ + function _checkMaxRepresentationRatio(idx, type, streamId) { + let maxIdx = topQualities[streamId][type] + const maxRepresentationRatio = settings.get().streaming.abr.maxRepresentationRatio[type]; + + if (isNaN(maxRepresentationRatio) || maxRepresentationRatio >= 1 || maxRepresentationRatio < 0) { + return idx; + } + return Math.min(idx, Math.round(maxIdx * maxRepresentationRatio)); + } + + /** + * Returns the maximum index according to the portal size + * @param idx + * @param type + * @param streamId + * @return {number|*} + * @private + */ + function _checkPortalSize(idx, type, streamId) { + if (type !== Constants.VIDEO || !settings.get().streaming.abr.limitBitrateByPortal || !streamProcessorDict[streamId] || !streamProcessorDict[streamId][type]) { + return idx; + } + + if (!windowResizeEventCalled) { + setElementSize(); + } + const streamInfo = streamProcessorDict[streamId][type].getStreamInfo(); + const representation = adapter.getAdaptationForType(streamInfo.index, type, streamInfo).Representation_asArray; + let newIdx = idx; + + if (elementWidth > 0 && elementHeight > 0) { + while ( + newIdx > 0 && + representation[newIdx] && + elementWidth < representation[newIdx].width && + elementWidth - representation[newIdx - 1].width < representation[newIdx].width - elementWidth) { + newIdx = newIdx - 1; + } - if (!topQualities[id].hasOwnProperty(type)) { - topQualities[id][type] = 0; + // Make sure that in case of multiple representation elements have same + // resolution, every such element is included + while (newIdx < representation.length - 1 && representation[newIdx].width === representation[newIdx + 1].width) { + newIdx = newIdx + 1; + } } - idx = checkMaxBitrate(topQualities[id][type], type); - idx = checkMaxRepresentationRatio(idx, type, topQualities[id][type]); - idx = checkPortalSize(idx, type); - return idx; + return newIdx; } /** * Gets top BitrateInfo for the player * @param {string} type - 'video' or 'audio' are the type options. + * @param {string} streamId - Id of the stream * @returns {BitrateInfo | null} */ - function getTopBitrateInfoFor(type) { - if (type && streamProcessorDict && streamProcessorDict[type]) { - const streamInfo = streamProcessorDict[type].getStreamInfo(); - if (streamInfo && streamInfo.id) { - const idx = getTopQualityIndexFor(type, streamInfo.id); - const bitrates = getBitrateList(streamProcessorDict[type].getMediaInfo()); - return bitrates[idx] ? bitrates[idx] : null; - } + function getTopBitrateInfoFor(type, streamId = null) { + if (!streamId) { + streamId = streamController.getActiveStreamInfo().id; + } + if (type && streamProcessorDict && streamProcessorDict[streamId] && streamProcessorDict[streamId][type]) { + const idx = getMaxAllowedIndexFor(type, streamId); + const bitrates = getBitrateList(streamProcessorDict[streamId][type].getMediaInfo()); + return bitrates[idx] ? bitrates[idx] : null; } return null; } /** + * Returns the initial bitrate for a specific media type and stream id * @param {string} type + * @param {string} streamId * @returns {number} A value of the initial bitrate, kbps * @memberof AbrController# */ - function getInitialBitrateFor(type) { + function getInitialBitrateFor(type, streamId) { checkConfig(); - if (type === Constants.TEXT || type === Constants.FRAGMENTED_TEXT) { + + if (type === Constants.TEXT) { return NaN; } + const savedBitrate = domStorage.getSavedBitrateSettings(type); - let configBitrate = settings.get().streaming.abr.initialBitrate[type]; + let configBitrate = mediaPlayerModel.getAbrBitrateParameter('initialBitrate', type); let configRatio = settings.get().streaming.abr.initialRepresentationRatio[type]; if (configBitrate === -1) { if (configRatio > -1) { - const representation = adapter.getAdaptationForType(0, type).Representation; + const streamInfo = streamProcessorDict[streamId][type].getStreamInfo(); + const representation = adapter.getAdaptationForType(streamInfo.index, type, streamInfo).Representation_asArray; if (Array.isArray(representation)) { const repIdx = Math.max(Math.round(representation.length * configRatio) - 1, 0); - configBitrate = representation[repIdx].bandwidth; + configBitrate = representation[repIdx].bandwidth / 1000; } else { configBitrate = 0; } @@ -271,142 +574,191 @@ function AbrController() { return configBitrate; } - function getMaxAllowedBitrateFor(type) { - return settings.get().streaming.abr.maxBitrate[type]; - } - - function getMinAllowedBitrateFor(type) { - return settings.get().streaming.abr.minBitrate[type]; - } - - function getMaxAllowedIndexFor(type) { - const maxBitrate = getMaxAllowedBitrateFor(type); - if (maxBitrate > -1) { - return getQualityForBitrate(streamProcessorDict[type].getMediaInfo(), maxBitrate); - } else { - return undefined; - } - } + /** + * This function is called by the scheduleControllers to check if the quality should be changed. + * Consider this the main entry point for the ABR decision logic + * @param {string} type + * @param {string} streamId + */ + function checkPlaybackQuality(type, streamId) { + try { + if (!type || !streamProcessorDict || !streamProcessorDict[streamId] || !streamProcessorDict[streamId][type]) { + return false; + } - function getMinAllowedIndexFor(type) { - const minBitrate = getMinAllowedBitrateFor(type); + if (droppedFramesHistory) { + const playbackQuality = videoModel.getPlaybackQuality(); + if (playbackQuality) { + droppedFramesHistory.push(streamId, playbackIndex, playbackQuality); + } + } - if (minBitrate > -1) { - const mediaInfo = streamProcessorDict[type].getMediaInfo(); - const bitrateList = getBitrateList(mediaInfo); - // This returns the quality index <= for the given bitrate - let minIdx = getQualityForBitrate(mediaInfo, minBitrate); - if (bitrateList[minIdx] && minIdx < bitrateList.length - 1 && bitrateList[minIdx].bitrate < minBitrate * 1000) { - minIdx++; // Go to the next bitrate + // ABR is turned off, do nothing + if (!settings.get().streaming.abr.autoSwitchBitrate[type]) { + return false; } - return minIdx; - } else { - return undefined; - } - } - function checkPlaybackQuality(type) { - if (type && streamProcessorDict && streamProcessorDict[type]) { - const streamInfo = streamProcessorDict[type].getStreamInfo(); - const streamId = streamInfo ? streamInfo.id : null; - const oldQuality = getQualityFor(type); + const oldQuality = getQualityFor(type, streamId); const rulesContext = RulesContext(context).create({ abrController: instance, - streamProcessor: streamProcessorDict[type], - currentValue: oldQuality, - switchHistory: switchHistoryDict[type], + switchHistory: switchHistoryDict[streamId][type], droppedFramesHistory: droppedFramesHistory, - useBufferOccupancyABR: useBufferOccupancyABR(type), - useL2AABR: useL2AABR(type), - useLoLPABR: useLoLPABR(type), + streamProcessor: streamProcessorDict[streamId][type], + currentValue: oldQuality, + useBufferOccupancyABR: isUsingBufferOccupancyAbrDict[type], + useL2AABR: isUsingL2AAbrDict[type], + useLoLPABR: isUsingLoLPAbrDict[type], videoModel }); + const minIdx = getMinAllowedIndexFor(type, streamId); + const maxIdx = getMaxAllowedIndexFor(type, streamId); + const switchRequest = abrRulesCollection.getMaxQuality(rulesContext); + let newQuality = switchRequest.quality; - if (droppedFramesHistory) { - const playbackQuality = videoModel.getPlaybackQuality(); - if (playbackQuality) { - droppedFramesHistory.push(playbackIndex, playbackQuality); - } + if (minIdx !== undefined && ((newQuality > SwitchRequest.NO_CHANGE) ? newQuality : oldQuality) < minIdx) { + newQuality = minIdx; } - if (!!settings.get().streaming.abr.autoSwitchBitrate[type]) { - const minIdx = getMinAllowedIndexFor(type); - const topQualityIdx = getTopQualityIndexFor(type, streamId); - const switchRequest = abrRulesCollection.getMaxQuality(rulesContext); - let newQuality = switchRequest.quality; - if (minIdx !== undefined && ((newQuality > SwitchRequest.NO_CHANGE) ? newQuality : oldQuality) < minIdx) { - newQuality = minIdx; - } - if (newQuality > topQualityIdx) { - newQuality = topQualityIdx; - } + if (newQuality > maxIdx) { + newQuality = maxIdx; + } + + switchHistoryDict[streamId][type].push({ oldValue: oldQuality, newValue: newQuality }); + + if (newQuality > SwitchRequest.NO_CHANGE && newQuality !== oldQuality && (abandonmentStateDict[streamId][type].state === MetricsConstants.ALLOW_LOAD || newQuality < oldQuality)) { + _changeQuality(type, oldQuality, newQuality, maxIdx, switchRequest.reason, streamId); + return true; + } + + return false; + } catch (e) { + return false; + } + + } + + /** + * Returns the current quality for a specific media type and a specific streamId + * @param {string} type + * @param {string} streamId + * @return {number|*} + */ + function getQualityFor(type, streamId = null) { + try { + if (!streamId) { + streamId = streamController.getActiveStreamInfo().id; + } + if (type && streamProcessorDict[streamId] && streamProcessorDict[streamId][type]) { + let quality; - switchHistoryDict[type].push({ oldValue: oldQuality, newValue: newQuality }); + if (streamId) { + qualityDict[streamId] = qualityDict[streamId] || {}; - if (newQuality > SwitchRequest.NO_CHANGE && newQuality != oldQuality) { - if (abandonmentStateDict[type].state === MetricsConstants.ALLOW_LOAD || newQuality > oldQuality) { - changeQuality(type, oldQuality, newQuality, topQualityIdx, switchRequest.reason); + if (!qualityDict[streamId].hasOwnProperty(type)) { + qualityDict[streamId][type] = QUALITY_DEFAULT; } - } else if (settings.get().debug.logLevel === Debug.LOG_LEVEL_DEBUG) { - const bufferLevel = dashMetrics.getCurrentBufferLevel(type, true); - logger.debug('[' + type + '] stay on ' + oldQuality + '/' + topQualityIdx + ' (buffer: ' + bufferLevel + ')'); + + quality = qualityDict[streamId][type]; + return quality; } } + return QUALITY_DEFAULT; + } catch (e) { + return QUALITY_DEFAULT; } } - function setPlaybackQuality(type, streamInfo, newQuality, reason) { - const id = streamInfo.id; - const oldQuality = getQualityFor(type); + /** + * Sets the new playback quality. Starts from index 0. + * If the index of the new quality is the same as the old one changeQuality will not be called. + * @param {string} type + * @param {object} streamInfo + * @param {number} newQuality + * @param {string} reason + */ + function setPlaybackQuality(type, streamInfo, newQuality, reason = null) { + if (!streamInfo || !streamInfo.id || !type) { + return; + } + const streamId = streamInfo.id; + const oldQuality = getQualityFor(type, streamId); checkInteger(newQuality); - const topQualityIdx = getTopQualityIndexFor(type, id); + const topQualityIdx = getMaxAllowedIndexFor(type, streamId); + if (newQuality !== oldQuality && newQuality >= 0 && newQuality <= topQualityIdx) { - changeQuality(type, oldQuality, newQuality, topQualityIdx, reason); + _changeQuality(type, oldQuality, newQuality, topQualityIdx, reason, streamId); } } - function changeQuality(type, oldQuality, newQuality, topQualityIdx, reason) { - if (type && streamProcessorDict[type]) { - const streamInfo = streamProcessorDict[type].getStreamInfo(); - const id = streamInfo ? streamInfo.id : null; - if (settings.get().debug.logLevel === Debug.LOG_LEVEL_DEBUG) { - const bufferLevel = dashMetrics.getCurrentBufferLevel(type); - logger.info('[' + type + '] switch from ' + oldQuality + ' to ' + newQuality + '/' + topQualityIdx + ' (buffer: ' + bufferLevel + ') ' + (reason ? JSON.stringify(reason) : '.')); - } - setQualityFor(type, id, newQuality); + /** + * + * @param {string} streamId + * @param {type} type + * @return {*|null} + */ + function getAbandonmentStateFor(streamId, type) { + return abandonmentStateDict[streamId] && abandonmentStateDict[streamId][type] ? abandonmentStateDict[streamId][type].state : null; + } + + + /** + * Changes the internal qualityDict values according to the new quality + * @param {string} type + * @param {number} oldQuality + * @param {number} newQuality + * @param {number} maxIdx + * @param {string} reason + * @param {object} streamId + * @private + */ + function _changeQuality(type, oldQuality, newQuality, maxIdx, reason, streamId) { + if (type && streamProcessorDict[streamId] && streamProcessorDict[streamId][type]) { + const streamInfo = streamProcessorDict[streamId][type].getStreamInfo(); + const isDynamic = streamInfo && streamInfo.manifestInfo && streamInfo.manifestInfo.isDynamic; + const bufferLevel = dashMetrics.getCurrentBufferLevel(type); + logger.info('Stream ID: ' + streamId + ' [' + type + '] switch from ' + oldQuality + ' to ' + newQuality + '/' + maxIdx + ' (buffer: ' + bufferLevel + ') ' + (reason ? JSON.stringify(reason) : '.')); + + qualityDict[streamId] = qualityDict[streamId] || {}; + qualityDict[streamId][type] = newQuality; + const bitrateInfo = _getBitrateInfoForQuality(streamId, type, newQuality); eventBus.trigger(Events.QUALITY_CHANGE_REQUESTED, { - oldQuality: oldQuality, - newQuality: newQuality, - reason: reason + oldQuality, + newQuality, + reason, + streamInfo, + bitrateInfo, + maxIdx, + mediaType: type }, { streamId: streamInfo.id, mediaType: type } ); - const bitrate = throughputHistory.getAverageThroughput(type); + const bitrate = throughputHistory.getAverageThroughput(type, isDynamic); if (!isNaN(bitrate)) { domStorage.setSavedBitrateSettings(type, bitrate); } } } - function setAbandonmentStateFor(type, state) { - abandonmentStateDict[type].state = state; - } - - function getAbandonmentStateFor(type) { - return abandonmentStateDict[type] ? abandonmentStateDict[type].state : null; + function _getBitrateInfoForQuality(streamId, type, idx) { + if (type && streamProcessorDict && streamProcessorDict[streamId] && streamProcessorDict[streamId][type]) { + const bitrates = getBitrateList(streamProcessorDict[streamId][type].getMediaInfo()); + return bitrates[idx] ? bitrates[idx] : null; + } + return null; } /** * @param {MediaInfo} mediaInfo * @param {number} bitrate A bitrate value, kbps - * @param {number} latency Expected latency of connection, ms + * @param {String} streamId Period ID + * @param {number|null} latency Expected latency of connection, ms * @returns {number} A quality index <= for the given bitrate * @memberof AbrController# */ - function getQualityForBitrate(mediaInfo, bitrate, latency) { - const voRepresentation = mediaInfo && mediaInfo.type ? streamProcessorDict[mediaInfo.type].getRepresentationInfo() : null; + function getQualityForBitrate(mediaInfo, bitrate, streamId, latency = null) { + const voRepresentation = mediaInfo && mediaInfo.type ? streamProcessorDict[streamId][mediaInfo.type].getRepresentationInfo() : null; if (settings.get().streaming.abr.useDeadTimeLatency && latency && voRepresentation && voRepresentation.fragmentDuration) { latency = latency / 1000; @@ -459,64 +811,36 @@ function AbrController() { } function _updateAbrStrategy(mediaType, bufferLevel) { + // else ABR_STRATEGY_DYNAMIC const strategy = settings.get().streaming.abr.ABRStrategy; - if (strategy === Constants.ABR_STRATEGY_L2A) { - isUsingBufferOccupancyABRDict[mediaType] = false; - isUsingLoLPBRDict[mediaType] = false; - isUsingL2AABRDict[mediaType] = true; - return; + if (strategy === Constants.ABR_STRATEGY_DYNAMIC) { + _updateDynamicAbrStrategy(mediaType, bufferLevel); } - if (strategy === Constants.ABR_STRATEGY_LoLP) { - isUsingBufferOccupancyABRDict[mediaType] = false; - isUsingLoLPBRDict[mediaType] = true; - isUsingL2AABRDict[mediaType] = false; - return; - } else if (strategy === Constants.ABR_STRATEGY_BOLA) { - isUsingBufferOccupancyABRDict[mediaType] = true; - isUsingLoLPBRDict[mediaType] = false; - isUsingL2AABRDict[mediaType] = false; - return; - } else if (strategy === Constants.ABR_STRATEGY_THROUGHPUT) { - isUsingBufferOccupancyABRDict[mediaType] = false; - isUsingLoLPBRDict[mediaType] = false; - isUsingL2AABRDict[mediaType] = false; - return; - } - // else ABR_STRATEGY_DYNAMIC - _updateDynamicAbrStrategy(mediaType, bufferLevel); } function _updateDynamicAbrStrategy(mediaType, bufferLevel) { - const stableBufferTime = mediaPlayerModel.getStableBufferTime(); - const switchOnThreshold = stableBufferTime; - const switchOffThreshold = 0.5 * stableBufferTime; - - const useBufferABR = isUsingBufferOccupancyABRDict[mediaType]; - const newUseBufferABR = bufferLevel > (useBufferABR ? switchOffThreshold : switchOnThreshold); // use hysteresis to avoid oscillating rules - isUsingBufferOccupancyABRDict[mediaType] = newUseBufferABR; - - if (newUseBufferABR !== useBufferABR) { - if (newUseBufferABR) { - logger.info('[' + mediaType + '] switching from throughput to buffer occupancy ABR rule (buffer: ' + bufferLevel.toFixed(3) + ').'); - } else { - logger.info('[' + mediaType + '] switching from buffer occupancy to throughput ABR rule (buffer: ' + bufferLevel.toFixed(3) + ').'); + try { + const stableBufferTime = mediaPlayerModel.getStableBufferTime(); + const switchOnThreshold = stableBufferTime; + const switchOffThreshold = 0.5 * stableBufferTime; + + const useBufferABR = isUsingBufferOccupancyAbrDict[mediaType]; + const newUseBufferABR = bufferLevel > (useBufferABR ? switchOffThreshold : switchOnThreshold); // use hysteresis to avoid oscillating rules + isUsingBufferOccupancyAbrDict[mediaType] = newUseBufferABR; + + if (newUseBufferABR !== useBufferABR) { + if (newUseBufferABR) { + logger.info('[' + mediaType + '] switching from throughput to buffer occupancy ABR rule (buffer: ' + bufferLevel.toFixed(3) + ').'); + } else { + logger.info('[' + mediaType + '] switching from buffer occupancy to throughput ABR rule (buffer: ' + bufferLevel.toFixed(3) + ').'); + } } + } catch (e) { + logger.error(e); } } - function useBufferOccupancyABR(mediaType) { - return isUsingBufferOccupancyABRDict[mediaType]; - } - - function useL2AABR(mediaType) { - return isUsingL2AABRDict[mediaType]; - } - - function useLoLPABR(mediaType) { - return isUsingLoLPBRDict[mediaType]; - } - function getThroughputHistory() { return throughputHistory; } @@ -526,80 +850,23 @@ function AbrController() { const streamId = mediaInfo.streamInfo.id; const max = mediaInfo.representationCount - 1; - setTopQualityIndex(type, streamId, max); + topQualities[streamId] = topQualities[streamId] || {}; + topQualities[streamId][type] = max; return max; } function isPlayingAtTopQuality(streamInfo) { const streamId = streamInfo ? streamInfo.id : null; - const audioQuality = getQualityFor(Constants.AUDIO); - const videoQuality = getQualityFor(Constants.VIDEO); + const audioQuality = getQualityFor(Constants.AUDIO, streamId); + const videoQuality = getQualityFor(Constants.VIDEO, streamId); - const isAtTop = (audioQuality === getTopQualityIndexFor(Constants.AUDIO, streamId)) && - (videoQuality === getTopQualityIndexFor(Constants.VIDEO, streamId)); + const isAtTop = (audioQuality === getMaxAllowedIndexFor(Constants.AUDIO, streamId)) && + (videoQuality === getMaxAllowedIndexFor(Constants.VIDEO, streamId)); return isAtTop; } - function getQualityFor(type) { - if (type && streamProcessorDict[type]) { - const streamInfo = streamProcessorDict[type].getStreamInfo(); - const id = streamInfo ? streamInfo.id : null; - let quality; - - if (id) { - qualityDict[id] = qualityDict[id] || {}; - - if (!qualityDict[id].hasOwnProperty(type)) { - qualityDict[id][type] = QUALITY_DEFAULT; - } - - quality = qualityDict[id][type]; - return quality; - } - } - return QUALITY_DEFAULT; - } - - function setQualityFor(type, id, value) { - qualityDict[id] = qualityDict[id] || {}; - qualityDict[id][type] = value; - } - - function setTopQualityIndex(type, id, value) { - topQualities[id] = topQualities[id] || {}; - topQualities[id][type] = value; - } - - function checkMaxBitrate(idx, type) { - let newIdx = idx; - - if (!streamProcessorDict[type]) { - return newIdx; - } - - const minIdx = getMinAllowedIndexFor(type); - if (minIdx !== undefined) { - newIdx = Math.max(idx, minIdx); - } - - const maxIdx = getMaxAllowedIndexFor(type); - if (maxIdx !== undefined) { - newIdx = Math.min(newIdx, maxIdx); - } - - return newIdx; - } - - function checkMaxRepresentationRatio(idx, type, maxIdx) { - const maxRepresentationRatio = settings.get().streaming.abr.maxRepresentationRatio[type]; - if (isNaN(maxRepresentationRatio) || maxRepresentationRatio >= 1 || maxRepresentationRatio < 0) { - return idx; - } - return Math.min(idx, Math.round(maxIdx * maxRepresentationRatio)); - } - function setWindowResizeEventCalled(value) { windowResizeEventCalled = value; } @@ -613,108 +880,44 @@ function AbrController() { } } - function checkPortalSize(idx, type) { - if (type !== Constants.VIDEO || !settings.get().streaming.abr.limitBitrateByPortal || !streamProcessorDict[type]) { - return idx; + function clearDataForStream(streamId) { + if (droppedFramesHistory) { + droppedFramesHistory.clearForStream(streamId); } - - if (!windowResizeEventCalled) { - setElementSize(); + if (streamProcessorDict[streamId]) { + delete streamProcessorDict[streamId]; } - - const representation = adapter.getAdaptationForType(0, type).Representation; - let newIdx = idx; - - if (elementWidth > 0 && elementHeight > 0) { - while ( - newIdx > 0 && - representation[newIdx] && - elementWidth < representation[newIdx].width && - elementWidth - representation[newIdx - 1].width < representation[newIdx].width - elementWidth) { - newIdx = newIdx - 1; - } - - // Make sure that in case of multiple representation elements have same - // resolution, every such element is included - while (newIdx < representation.length - 1 && representation[newIdx].width === representation[newIdx + 1].width) { - newIdx = newIdx + 1; - } + if (switchHistoryDict[streamId]) { + delete switchHistoryDict[streamId]; } - return newIdx; - } - - function onFragmentLoadProgress(e) { - const type = e.request.mediaType; - if (!!settings.get().streaming.abr.autoSwitchBitrate[type]) { - const streamProcessor = streamProcessorDict[type]; - if (!streamProcessor) return; // There may be a fragment load in progress when we switch periods and recreated some controllers. - - const rulesContext = RulesContext(context).create({ - abrController: instance, - streamProcessor: streamProcessor, - currentRequest: e.request, - useBufferOccupancyABR: useBufferOccupancyABR(type), - useL2AABR: useL2AABR(type), - useLoLPABR: useLoLPABR(type), - videoModel - }); - const switchRequest = abrRulesCollection.shouldAbandonFragment(rulesContext); - - if (switchRequest.quality > SwitchRequest.NO_CHANGE) { - const fragmentModel = streamProcessor.getFragmentModel(); - const request = fragmentModel.getRequests({ - state: FragmentModel.FRAGMENT_MODEL_LOADING, - index: e.request.index - })[0]; - if (request) { - //TODO Check if we should abort or if better to finish download. check bytesLoaded/Total - fragmentModel.abortRequests(); - setAbandonmentStateFor(type, MetricsConstants.ABANDON_LOAD); - switchHistoryDict[type].reset(); - switchHistoryDict[type].push({ - oldValue: getQualityFor(type), - newValue: switchRequest.quality, - confidence: 1, - reason: switchRequest.reason - }); - setPlaybackQuality(type, streamController.getActiveStreamInfo(), switchRequest.quality, switchRequest.reason); - - clearTimeout(abandonmentTimeout); - abandonmentTimeout = setTimeout( - () => { - setAbandonmentStateFor(type, MetricsConstants.ALLOW_LOAD); - abandonmentTimeout = null; - }, - settings.get().streaming.abandonLoadTimeout - ); - } - } + if (abandonmentStateDict[streamId]) { + delete abandonmentStateDict[streamId]; } } instance = { - isPlayingAtTopQuality: isPlayingAtTopQuality, - updateTopQualityIndex: updateTopQualityIndex, - getThroughputHistory: getThroughputHistory, - getBitrateList: getBitrateList, - getQualityForBitrate: getQualityForBitrate, - getTopBitrateInfoFor: getTopBitrateInfoFor, - getMaxAllowedIndexFor: getMaxAllowedIndexFor, - getMinAllowedIndexFor: getMinAllowedIndexFor, - getInitialBitrateFor: getInitialBitrateFor, - getQualityFor: getQualityFor, - getAbandonmentStateFor: getAbandonmentStateFor, - setPlaybackQuality: setPlaybackQuality, - checkPlaybackQuality: checkPlaybackQuality, - getTopQualityIndexFor: getTopQualityIndexFor, - setElementSize: setElementSize, - setWindowResizeEventCalled: setWindowResizeEventCalled, - createAbrRulesCollection: createAbrRulesCollection, - registerStreamType: registerStreamType, - unRegisterStreamType: unRegisterStreamType, - setConfig: setConfig, - reset: reset + initialize, + isPlayingAtTopQuality, + updateTopQualityIndex, + clearDataForStream, + getThroughputHistory, + getBitrateList, + getQualityForBitrate, + getTopBitrateInfoFor, + getMinAllowedIndexFor, + getMaxAllowedIndexFor, + getInitialBitrateFor, + getQualityFor, + getAbandonmentStateFor, + setPlaybackQuality, + checkPlaybackQuality, + setElementSize, + setWindowResizeEventCalled, + registerStreamType, + unRegisterStreamType, + setConfig, + reset }; setup(); diff --git a/src/streaming/controllers/BufferController.js b/src/streaming/controllers/BufferController.js index d3458c9a77..7910a9e6ba 100644 --- a/src/streaming/controllers/BufferController.js +++ b/src/streaming/controllers/BufferController.js @@ -32,8 +32,6 @@ import Constants from '../constants/Constants'; import MetricsConstants from '../constants/MetricsConstants'; import FragmentModel from '../models/FragmentModel'; import SourceBufferSink from '../SourceBufferSink'; -import PreBufferSink from '../PreBufferSink'; -import AbrController from './AbrController'; import EventBus from '../../core/EventBus'; import Events from '../../core/events/Events'; import FactoryMaker from '../../core/FactoryMaker'; @@ -42,8 +40,8 @@ import InitCache from '../utils/InitCache'; import DashJSError from '../vo/DashJSError'; import Errors from '../../core/errors/Errors'; import {HTTPRequest} from '../vo/metrics/HTTPRequest'; +import MediaPlayerEvents from '../../streaming/MediaPlayerEvents'; -const BUFFERING_COMPLETED_THRESHOLD = 0.1; const BUFFER_END_THRESHOLD = 0.5; const BUFFER_RANGE_CALCULATION_THRESHOLD = 0.01; const QUOTA_EXCEEDED_ERROR_CODE = 22; @@ -58,7 +56,6 @@ function BufferController(config) { const errHandler = config.errHandler; const fragmentModel = config.fragmentModel; const representationController = config.representationController; - const mediaController = config.mediaController; const adapter = config.adapter; const textController = config.textController; const abrController = config.abrController; @@ -69,27 +66,22 @@ function BufferController(config) { let instance, logger, - requiredQuality, isBufferingCompleted, bufferLevel, criticalBufferLevel, mediaSource, maxAppendedIndex, - lastIndex, - buffer, - dischargeBuffer, - dischargeFragments, + maximumIndex, + sourceBufferSink, bufferState, appendedBytesInfo, wallclockTicked, isPruningInProgress, isQuotaExceeded, initCache, - seekTarget, - seekClearedBufferingCompleted, pendingPruningRanges, replacingBuffer, - mediaChunk; + seekTarget; function setup() { @@ -99,119 +91,131 @@ function BufferController(config) { resetInitialSettings(); } - function getBufferControllerType() { - return BUFFER_CONTROLLER_TYPE; - } - - function initialize(Source) { - setMediaSource(Source); - - requiredQuality = abrController.getQualityFor(type); - - eventBus.on(Events.DATA_UPDATE_COMPLETED, onDataUpdateCompleted, this); - eventBus.on(Events.INIT_FRAGMENT_LOADED, onInitFragmentLoaded, this); - eventBus.on(Events.MEDIA_FRAGMENT_LOADED, onMediaFragmentLoaded, this); - eventBus.on(Events.QUALITY_CHANGE_REQUESTED, onQualityChanged, this); - eventBus.on(Events.STREAM_COMPLETED, onStreamCompleted, this); - eventBus.on(Events.PLAYBACK_PLAYING, onPlaybackPlaying, this); - eventBus.on(Events.PLAYBACK_PROGRESS, onPlaybackProgression, this); - eventBus.on(Events.PLAYBACK_TIME_UPDATED, onPlaybackProgression, this); - eventBus.on(Events.PLAYBACK_RATE_CHANGED, onPlaybackRateChanged, this); - eventBus.on(Events.PLAYBACK_SEEKING, onPlaybackSeeking, this); - eventBus.on(Events.PLAYBACK_SEEKED, onPlaybackSeeked, this); - eventBus.on(Events.PLAYBACK_STALLED, onPlaybackStalled, this); - eventBus.on(Events.WALLCLOCK_TIME_UPDATED, onWallclockTimeUpdated, this); - eventBus.on(Events.CURRENT_TRACK_CHANGED, onCurrentTrackChanged, this, { priority: EventBus.EVENT_PRIORITY_HIGH }); - eventBus.on(Events.SOURCEBUFFER_REMOVE_COMPLETED, onRemoved, this); + /** + * Initialize the BufferController. Sets the media source and registers the event handlers. + * @param {object} mediaSource + */ + function initialize(mediaSource) { + setMediaSource(mediaSource); + + eventBus.on(Events.INIT_FRAGMENT_LOADED, _onInitFragmentLoaded, instance); + eventBus.on(Events.MEDIA_FRAGMENT_LOADED, _onMediaFragmentLoaded, instance); + eventBus.on(Events.WALLCLOCK_TIME_UPDATED, _onWallclockTimeUpdated, instance); + + eventBus.on(MediaPlayerEvents.PLAYBACK_PLAYING, _onPlaybackPlaying, instance); + eventBus.on(MediaPlayerEvents.PLAYBACK_PROGRESS, _onPlaybackProgression, instance); + eventBus.on(MediaPlayerEvents.PLAYBACK_TIME_UPDATED, _onPlaybackProgression, instance); + eventBus.on(MediaPlayerEvents.PLAYBACK_RATE_CHANGED, _onPlaybackRateChanged, instance); + eventBus.on(MediaPlayerEvents.PLAYBACK_STALLED, _onPlaybackStalled, instance); } + /** + * Returns the stream id + * @return {string} + */ function getStreamId() { return streamInfo.id; } + /** + * Returns the media type + * @return {type} + */ function getType() { return type; } - - function getRepresentationInfo(quality) { - return adapter.convertDataToRepresentationInfo(representationController.getRepresentationForQuality(quality)); + /** + * Returns the type of the BufferController. We distinguish between standard buffer controllers and buffer controllers related to texttracks. + * @return {string} + */ + function getBufferControllerType() { + return BUFFER_CONTROLLER_TYPE; } - function createBuffer(mediaInfoArr, oldBuffers) { - if (!initCache || !mediaInfoArr) return null; - const mediaInfo = mediaInfoArr[0]; - if (mediaSource) { - try { - if (oldBuffers && oldBuffers[type]) { - buffer = SourceBufferSink(context).create(mediaSource, mediaInfo, onAppended.bind(this), oldBuffers[type]); - } else { - buffer = SourceBufferSink(context).create(mediaSource, mediaInfo, onAppended.bind(this), null); - } - if (settings.get().streaming.useAppendWindow) { - buffer.updateAppendWindow(streamInfo); - } - if (typeof buffer.getBuffer().initialize === 'function') { - buffer.getBuffer().initialize(type, streamInfo, mediaInfoArr, fragmentModel); - } - } catch (e) { - logger.fatal('Caught error on create SourceBuffer: ' + e); - errHandler.error(new DashJSError(Errors.MEDIASOURCE_TYPE_UNSUPPORTED_CODE, Errors.MEDIASOURCE_TYPE_UNSUPPORTED_MESSAGE + type)); - } - } else { - buffer = PreBufferSink(context).create(onAppended.bind(this)); - } - updateBufferTimestampOffset(getRepresentationInfo(requiredQuality)); - return buffer; + /** + * Sets the mediasource. + * @param {object} value + */ + function setMediaSource(value) { + mediaSource = value; } - function dischargePreBuffer() { - if (buffer && dischargeBuffer && typeof dischargeBuffer.discharge === 'function') { - const ranges = dischargeBuffer.getAllBufferRanges(); + /** + * Get the RepresentationInfo for a certain quality. + * @param {number} quality + * @return {object} + * @private + */ + function _getRepresentationInfo(quality) { + return adapter.convertRepresentationToRepresentationInfo(representationController.getRepresentationForQuality(quality)); + } - if (ranges.length > 0) { - let rangeStr = 'Beginning ' + type + 'PreBuffer discharge, adding buffer for:'; - for (let i = 0; i < ranges.length; i++) { - rangeStr += ' start: ' + ranges.start(i) + ', end: ' + ranges.end(i) + ';'; - } - logger.debug(rangeStr); - } else { - logger.debug('PreBuffer discharge requested, but there were no media segments in the PreBuffer.'); + /** + * Creates a SourceBufferSink object + * @param {object} mediaInfo + * @param {array} oldBufferSinks + * @return {object|null} SourceBufferSink + */ + function createBufferSink(mediaInfo, oldBufferSinks = []) { + return new Promise((resolve, reject) => { + if (!initCache || !mediaInfo || !mediaSource) { + resolve(null); + return; } - //A list of fragments to supress bytesAppended events for. This makes transferring from a prebuffer to a sourcebuffer silent. - dischargeFragments = []; - let chunks = dischargeBuffer.discharge(); - let lastInit = null; - for (let j = 0; j < chunks.length; j++) { - const chunk = chunks[j]; - if (chunk.segmentType !== 'InitializationSegment') { - const initChunk = initCache.extract(chunk.streamId, chunk.representationId); - if (initChunk) { - if (lastInit !== initChunk) { - dischargeFragments.push(initChunk); - buffer.append(initChunk); - lastInit = initChunk; - } - } - } - dischargeFragments.push(chunk); - buffer.append(chunk); - } + const requiredQuality = abrController.getQualityFor(type, streamInfo.id); + sourceBufferSink = SourceBufferSink(context).create({ + mediaSource, + textController, + eventBus + }); + _initializeSink(mediaInfo, oldBufferSinks, requiredQuality) + .then(() => { + return updateBufferTimestampOffset(_getRepresentationInfo(requiredQuality)); + }) + .then(() => { + resolve(sourceBufferSink); + }) + .catch((e) => { + logger.fatal('Caught error on create SourceBuffer: ' + e); + errHandler.error(new DashJSError(Errors.MEDIASOURCE_TYPE_UNSUPPORTED_CODE, Errors.MEDIASOURCE_TYPE_UNSUPPORTED_MESSAGE + type)); + reject(e); + }); + }); + } + + function _initializeSink(mediaInfo, oldBufferSinks, requiredQuality) { + const selectedRepresentation = _getRepresentationInfo(requiredQuality); - dischargeBuffer.reset(); - dischargeBuffer = null; + if (oldBufferSinks && oldBufferSinks[type] && (type === Constants.VIDEO || type === Constants.AUDIO)) { + return sourceBufferSink.initializeForStreamSwitch(mediaInfo, selectedRepresentation, oldBufferSinks[type]); + } else { + return sourceBufferSink.initializeForFirstUse(streamInfo, mediaInfo, selectedRepresentation); } } - function onInitFragmentLoaded(e) { - logger.info('Init fragment finished loading saving to', type + '\'s init cache'); - initCache.save(e.chunk); + + /** + * Callback handler when init segment has been loaded. Based on settings, the init segment is saved to the cache, and appended to the buffer. + * @param {object} e + * @private + */ + function _onInitFragmentLoaded(e) { + if (settings.get().streaming.cacheInitSegments) { + logger.info('Init fragment finished loading saving to', type + '\'s init cache'); + initCache.save(e.chunk); + } logger.debug('Append Init fragment', type, ' with representationId:', e.chunk.representationId, ' and quality:', e.chunk.quality, ', data size:', e.chunk.bytes.byteLength); - appendToBuffer(e.chunk); + _appendToBuffer(e.chunk); } - function appendInitSegment(representationId) { + /** + * Append the init segment for a certain representation to the buffer. If the init segment is cached we take the one from the cache. Otherwise the function returns false and the segment has to be requested again. + * @param {string} representationId + * @return {boolean} + */ + function appendInitSegmentFromCache(representationId) { // Get init segment from cache const chunk = initCache.extract(streamInfo.id, representationId); @@ -222,38 +226,39 @@ function BufferController(config) { // Append init segment into buffer logger.info('Append Init fragment', type, ' with representationId:', chunk.representationId, ' and quality:', chunk.quality, ', data size:', chunk.bytes.byteLength); - appendToBuffer(chunk); + _appendToBuffer(chunk); + return true; } - function onMediaFragmentLoaded(e) { - const chunk = e.chunk; - - if (replacingBuffer) { - mediaChunk = chunk; - const ranges = buffer && buffer.getAllBufferRanges(); - if (ranges && ranges.length > 0 && playbackController.getTimeToStreamEnd() > settings.get().streaming.stallThreshold) { - logger.debug('Clearing buffer because track changed - ' + (ranges.end(ranges.length - 1) + BUFFER_END_THRESHOLD)); - clearBuffers([{ - start: 0, - end: ranges.end(ranges.length - 1) + BUFFER_END_THRESHOLD, - force: true // Force buffer removal even when buffering is completed and MediaSource is ended - }]); - } - } else { - appendToBuffer(chunk); - } + /** + * Calls the _appendToBuffer function to append the segment to the buffer. In case of a track switch the buffer might be cleared. + * @param {object} e + */ + function _onMediaFragmentLoaded(e) { + _appendToBuffer(e.chunk, e.request); } - function appendToBuffer(chunk) { - buffer.append(chunk); + /** + * Append data to the MSE buffer using the SourceBufferSink + * @param {object} chunk + * @private + */ + function _appendToBuffer(chunk, request = null) { + sourceBufferSink.append(chunk, request) + .then((e) => { + _onAppended(e); + }) + .catch((e) => { + _onAppended(e); + }); if (chunk.mediaInfo.type === Constants.VIDEO) { - triggerEvent(Events.VIDEO_CHUNK_RECEIVED, {chunk: chunk}); + _triggerEvent(Events.VIDEO_CHUNK_RECEIVED, { chunk: chunk }); } } - function showBufferRanges(ranges) { + function _showBufferRanges(ranges) { if (ranges && ranges.length > 0) { for (let i = 0, len = ranges.length; i < len; i++) { logger.debug('Buffered range: ' + ranges.start(i) + ' - ' + ranges.end(i) + ', currentTime = ', playbackController.getTime()); @@ -261,33 +266,16 @@ function BufferController(config) { } } - function onAppended(e) { + function _onAppended(e) { if (e.error) { + // If we receive a QUOTA_EXCEEDED_ERROR_CODE we should adjust the target buffer times to avoid this error in the future. if (e.error.code === QUOTA_EXCEEDED_ERROR_CODE) { - isQuotaExceeded = true; - criticalBufferLevel = getTotalBufferedTime() * 0.8; - logger.warn('Quota exceeded, Critical Buffer: ' + criticalBufferLevel); - - if (criticalBufferLevel > 0) { - // recalculate buffer lengths according to criticalBufferLevel - const bufferToKeep = Math.max(0.2 * criticalBufferLevel, 1); - const bufferAhead = criticalBufferLevel - bufferToKeep; - const bufferTimeAtTopQuality = Math.min(settings.get().streaming.bufferTimeAtTopQuality, bufferAhead * 0.9); - const bufferTimeAtTopQualityLongForm = Math.min(settings.get().streaming.bufferTimeAtTopQualityLongForm, bufferAhead * 0.9); - const s = { - streaming: { - bufferToKeep: parseFloat(bufferToKeep.toFixed(5)), - bufferTimeAtTopQuality: parseFloat(bufferTimeAtTopQuality.toFixed(5)), - bufferTimeAtTopQualityLongForm: parseFloat(bufferTimeAtTopQualityLongForm.toFixed(5)) - } - }; - settings.update(s); - } + _handleQuotaExceededError(); } if (e.error.code === QUOTA_EXCEEDED_ERROR_CODE || !hasEnoughSpaceToAppend()) { logger.warn('Clearing playback buffer to overcome quota exceed situation'); - // Notify Schedulecontroller to stop scheduling until buffer has been pruned - triggerEvent(Events.QUOTA_EXCEEDED, { + // Notify ScheduleController to stop scheduling until buffer has been pruned + _triggerEvent(Events.QUOTA_EXCEEDED, { criticalBufferLevel: criticalBufferLevel, quotaExceededTime: e.chunk.start }); @@ -295,61 +283,71 @@ function BufferController(config) { } return; } - isQuotaExceeded = false; + // Check if session has not been stopped in the meantime (while last segment was being appended) + if (!sourceBufferSink) return; + + _updateBufferLevel(); + + isQuotaExceeded = false; appendedBytesInfo = e.chunk; + + if (!appendedBytesInfo || !appendedBytesInfo.endFragment) { + return; + } + if (appendedBytesInfo && !isNaN(appendedBytesInfo.index)) { maxAppendedIndex = Math.max(appendedBytesInfo.index, maxAppendedIndex); - checkIfBufferingCompleted(); + _checkIfBufferingCompleted(); } - const ranges = buffer.getAllBufferRanges(); + const ranges = sourceBufferSink.getAllBufferRanges(); if (appendedBytesInfo.segmentType === HTTPRequest.MEDIA_SEGMENT_TYPE) { - showBufferRanges(ranges); - onPlaybackProgression(); - adjustSeekTarget(); - } else if (replacingBuffer) { - // When replacing buffer due to switch track, and once new initialization segment has been appended - // (and previous buffered data removed) then seek stream to current time - const currentTime = playbackController.getTime(); - logger.debug('AppendToBuffer seek target should be ' + currentTime); - triggerEvent(Events.SEEK_TARGET, {time: currentTime}); + _showBufferRanges(ranges); + _onPlaybackProgression(); + _adjustSeekTarget(); } - let suppressAppendedEvent = false; - if (dischargeFragments) { - if (dischargeFragments.indexOf(appendedBytesInfo) > 0) { - suppressAppendedEvent = true; - } - dischargeFragments = null; - } - if (appendedBytesInfo && !suppressAppendedEvent) { - triggerEvent(appendedBytesInfo.endFragment ? Events.BYTES_APPENDED_END_FRAGMENT : Events.BYTES_APPENDED, { + if (appendedBytesInfo) { + _triggerEvent(Events.BYTES_APPENDED_END_FRAGMENT, { quality: appendedBytesInfo.quality, startTime: appendedBytesInfo.start, index: appendedBytesInfo.index, bufferedRanges: ranges, + segmentType: appendedBytesInfo.segmentType, mediaType: type }); } } - function adjustSeekTarget() { - // Check buffered data only for audio and video - if (type !== Constants.AUDIO && type !== Constants.VIDEO) return; + /** + * In some cases the segment we requested might start at a different time than we initially aimed for. segments timeline/template tolerance. + * We might need to do an internal seek if there is drift. + * @private + */ + function _adjustSeekTarget() { if (isNaN(seekTarget)) return; + // Check buffered data only for audio and video + if (type !== Constants.AUDIO && type !== Constants.VIDEO) { + seekTarget = NaN; + return; + } // Check if current buffered range already contains seek target (and current video element time) const currentTime = playbackController.getTime(); - let range = getRangeAt(seekTarget, 0); - if (currentTime === seekTarget && range) return; + const rangeAtCurrenTime = getRangeAt(currentTime, 0); + const rangeAtSeekTarget = getRangeAt(seekTarget, 0); + if (rangeAtCurrenTime && rangeAtSeekTarget && rangeAtCurrenTime.start === rangeAtSeekTarget.start) { + seekTarget = NaN; + return; + } // Get buffered range corresponding to the seek target const segmentDuration = representationController.getCurrentRepresentation().segmentDuration; - range = getRangeAt(seekTarget, segmentDuration); + const range = getRangeAt(seekTarget, segmentDuration); if (!range) return; - if (Math.abs(currentTime - seekTarget) > segmentDuration) { + if (settings.get().streaming.buffer.enableSeekDecorrelationFix && Math.abs(currentTime - seekTarget) > segmentDuration) { // If current video model time is decorrelated from seek target (and appended buffer) then seek video element // (in case of live streams on some browsers/devices for which we can't set video element time at unavalaible range) @@ -357,145 +355,257 @@ function BufferController(config) { if (seekTarget <= range.end) { // Seek video element to seek target or range start if appended buffer starts after seek target (segments timeline/template tolerance) playbackController.seek(Math.max(seekTarget, range.start), false, true); - seekTarget = NaN; } } else if (currentTime < range.start) { // If appended buffer starts after seek target (segments timeline/template tolerance) then seek to range start playbackController.seek(range.start, false, true); - seekTarget = NaN; } } - function onQualityChanged(e) { - if (requiredQuality === e.newQuality) return; - - updateBufferTimestampOffset(this.getRepresentationInfo(e.newQuality)); - requiredQuality = e.newQuality; + function _handleQuotaExceededError() { + isQuotaExceeded = true; + criticalBufferLevel = getTotalBufferedTime() * 0.8; + logger.warn('Quota exceeded, Critical Buffer: ' + criticalBufferLevel); + + if (criticalBufferLevel > 0) { + // recalculate buffer lengths according to criticalBufferLevel + const bufferToKeep = Math.max(0.2 * criticalBufferLevel, 1); + const bufferAhead = criticalBufferLevel - bufferToKeep; + const bufferTimeAtTopQuality = Math.min(settings.get().streaming.buffer.bufferTimeAtTopQuality, bufferAhead * 0.9); + const bufferTimeAtTopQualityLongForm = Math.min(settings.get().streaming.buffer.bufferTimeAtTopQualityLongForm, bufferAhead * 0.9); + const s = { + streaming: { + buffer: { + bufferToKeep: parseFloat(bufferToKeep.toFixed(5)), + bufferTimeAtTopQuality: parseFloat(bufferTimeAtTopQuality.toFixed(5)), + bufferTimeAtTopQualityLongForm: parseFloat(bufferTimeAtTopQualityLongForm.toFixed(5)) + } + } + }; + settings.update(s); + } } //********************************************************************** // START Buffer Level, State & Sufficiency Handling. //********************************************************************** - function onPlaybackSeeking(e) { - if (!buffer) return; - seekTarget = e.seekTime; + function prepareForPlaybackSeek() { if (isBufferingCompleted) { - seekClearedBufferingCompleted = true; - isBufferingCompleted = false; - //a seek command has occured, reset lastIndex value, it will be set next time that onStreamCompleted will be called. - lastIndex = Number.POSITIVE_INFINITY; - } - if (type !== Constants.FRAGMENTED_TEXT) { - // remove buffer after seeking operations - pruneAllSafely(); - } else { - onPlaybackProgression(); + setIsBufferingCompleted(false); } + + // Abort the current request and empty all possible segments to be appended + return sourceBufferSink.abort(); } - function onPlaybackSeeked() { - seekTarget = NaN; + function prepareForReplacementTrackSwitch(codec) { + return new Promise((resolve, reject) => { + sourceBufferSink.abort() + .then(() => { + return updateAppendWindow(); + }) + .then(() => { + return sourceBufferSink.changeType(codec); + }) + .then(() => { + return pruneAllSafely(); + }) + .then(() => { + setIsBufferingCompleted(false); + resolve(); + }) + .catch((e) => { + reject(e); + }); + }); + } + + function prepareForReplacementQualitySwitch() { + return new Promise((resolve, reject) => { + sourceBufferSink.abort() + .then(() => { + return updateAppendWindow(); + }) + .then(() => { + return pruneAllSafely(); + }) + .then(() => { + setIsBufferingCompleted(false); + resolve(); + }) + .catch((e) => { + reject(e); + }); + }); + } + + function prepareForNonReplacementTrackSwitch(codec) { + return new Promise((resolve, reject) => { + updateAppendWindow() + .then(() => { + return sourceBufferSink.changeType(codec); + }) + .then(() => { + resolve(); + }) + .catch((e) => { + reject(e); + }); + }); } - // Prune full buffer but what is around current time position function pruneAllSafely() { - if (!buffer) return; - buffer.waitForUpdateEnd(() => { - const ranges = getAllRangesWithSafetyFactor(); + return new Promise((resolve, reject) => { + let ranges = getAllRangesWithSafetyFactor(); + if (!ranges || ranges.length === 0) { - onPlaybackProgression(); + _onPlaybackProgression(); + resolve(); + return; } - clearBuffers(ranges); + + clearBuffers(ranges) + .then(() => { + resolve(); + }) + .catch((e) => { + reject(e); + }); }); } - // Get all buffer ranges but a range around current time position - function getAllRangesWithSafetyFactor() { - if (!buffer) return; + function getAllRangesWithSafetyFactor(seekTime) { const clearRanges = []; - const ranges = buffer.getAllBufferRanges(); + const ranges = sourceBufferSink.getAllBufferRanges(); + + // no valid ranges if (!ranges || ranges.length === 0) { return clearRanges; } - const currentTime = playbackController.getTime(); - const endOfBuffer = ranges.end(ranges.length - 1) + BUFFER_END_THRESHOLD; - - const currentTimeRequest = fragmentModel.getRequests({ - state: FragmentModel.FRAGMENT_MODEL_EXECUTED, - time: currentTime, - threshold: BUFFER_RANGE_CALCULATION_THRESHOLD - })[0]; - - // There is no request in current time position yet. Let's remove everything - if (!currentTimeRequest) { - logger.debug('getAllRangesWithSafetyFactor - No request found in current time position, removing full buffer 0 -', endOfBuffer); + // if no target time is provided we clear everyhing + if ((!seekTime && seekTime !== 0) || isNaN(seekTime)) { clearRanges.push({ - start: 0, - end: endOfBuffer + start: ranges.start(0), + end: ranges.end(ranges.length - 1) + BUFFER_END_THRESHOLD }); - } else { - // Build buffer behind range. To avoid pruning time around current time position, - // we include fragment right behind the one in current time position - const behindRange = { - start: 0, - end: currentTimeRequest.startTime - settings.get().streaming.stallThreshold - }; - const prevReq = fragmentModel.getRequests({ + } + + // otherwise we need to calculate the correct pruning range + else { + + const behindPruningRange = _getRangeBehindForPruning(seekTime, ranges); + const aheadPruningRange = _getRangeAheadForPruning(seekTime, ranges); + + if (behindPruningRange) { + clearRanges.push(behindPruningRange); + } + + if (aheadPruningRange) { + clearRanges.push(aheadPruningRange); + } + } + + return clearRanges; + } + + function _getRangeBehindForPruning(targetTime, ranges) { + const bufferToKeepBehind = settings.get().streaming.buffer.bufferToKeep; + const startOfBuffer = ranges.start(0); + + // if we do a seek ahead of the current play position we do need to prune behind the new play position + const behindDiff = targetTime - startOfBuffer; + if (behindDiff > bufferToKeepBehind) { + + let rangeEnd = Math.max(0, targetTime - bufferToKeepBehind); + // Ensure we keep full range of current fragment + const currentTimeRequest = fragmentModel.getRequests({ state: FragmentModel.FRAGMENT_MODEL_EXECUTED, - time: currentTimeRequest.startTime - (currentTimeRequest.duration / 2), + time: targetTime, threshold: BUFFER_RANGE_CALCULATION_THRESHOLD })[0]; - if (prevReq && prevReq.startTime != currentTimeRequest.startTime) { - behindRange.end = prevReq.startTime; + + if (currentTimeRequest) { + rangeEnd = Math.min(currentTimeRequest.startTime, rangeEnd); } - if (behindRange.start < behindRange.end && behindRange.end > ranges.start(0)) { - clearRanges.push(behindRange); + if (rangeEnd > 0) { + return { + start: startOfBuffer, + end: rangeEnd + }; } + } - // Build buffer ahead range. To avoid pruning time around current time position, - // we include fragment right after the one in current time position - const aheadRange = { - start: currentTimeRequest.startTime + currentTimeRequest.duration + settings.get().streaming.stallThreshold, - end: endOfBuffer - }; - const nextReq = fragmentModel.getRequests({ + return null; + } + + function _getRangeAheadForPruning(targetTime, ranges) { + // if we do a seek behind the current play position we do need to prune ahead of the new play position + const endOfBuffer = ranges.end(ranges.length - 1) + BUFFER_END_THRESHOLD; + const isLongFormContent = streamInfo.manifestInfo.duration >= settings.get().streaming.buffer.longFormContentDurationThreshold; + const bufferToKeepAhead = isLongFormContent ? settings.get().streaming.buffer.bufferTimeAtTopQualityLongForm : settings.get().streaming.buffer.bufferTimeAtTopQuality; + const aheadDiff = endOfBuffer - targetTime; + + if (aheadDiff > bufferToKeepAhead) { + + let rangeStart = targetTime + bufferToKeepAhead; + // Ensure we keep full range of current fragment + const currentTimeRequest = fragmentModel.getRequests({ state: FragmentModel.FRAGMENT_MODEL_EXECUTED, - time: currentTimeRequest.startTime + currentTimeRequest.duration + settings.get().streaming.stallThreshold, + time: targetTime, threshold: BUFFER_RANGE_CALCULATION_THRESHOLD })[0]; - if (nextReq && nextReq.startTime !== currentTimeRequest.startTime) { - aheadRange.start = nextReq.startTime + nextReq.duration + settings.get().streaming.stallThreshold; + + if (currentTimeRequest) { + rangeStart = Math.max(currentTimeRequest.startTime + currentTimeRequest.duration, rangeStart); + } + + // Never remove the contiguous range of targetTime in order to avoid flushes & reenqueues when the user doesn't want it + const avoidCurrentTimeRangePruning = settings.get().streaming.buffer.avoidCurrentTimeRangePruning; + if (avoidCurrentTimeRangePruning) { + for (let i = 0; i < ranges.length; i++) { + if (ranges.start(i) <= targetTime && targetTime <= ranges.end(i) + && ranges.start(i) <= rangeStart && rangeStart <= ranges.end(i)) { + let oldRangeStart = rangeStart; + if (i + 1 < ranges.length) { + rangeStart = ranges.start(i+1); + } else { + rangeStart = ranges.end(i) + 1; + } + logger.debug('Buffered range [' + ranges.start(i) + ', ' + ranges.end(i) + '] overlaps with targetTime ' + targetTime + ' and range to be pruned [' + oldRangeStart + ', ' + endOfBuffer + '], using [' + rangeStart + ', ' + endOfBuffer +'] instead' + ((rangeStart < endOfBuffer) ? '' : ' (no actual pruning)')); + break; + } + } } - if (aheadRange.start < aheadRange.end && aheadRange.start < endOfBuffer) { - clearRanges.push(aheadRange); + + if (rangeStart < endOfBuffer) { + return { + start: rangeStart, + end: endOfBuffer + }; } } - return clearRanges; - } - - function getWorkingTime() { - return isNaN(seekTarget) ? playbackController.getTime() : seekTarget; + return null; } - function onPlaybackProgression() { - if (!replacingBuffer || (type === Constants.FRAGMENTED_TEXT && textController.isTextEnabled())) { - updateBufferLevel(); + function _onPlaybackProgression() { + if (!replacingBuffer || (type === Constants.TEXT && textController.isTextEnabled())) { + _updateBufferLevel(); } } - function onPlaybackStalled() { + function _onPlaybackStalled() { checkIfSufficientBuffer(); } - function onPlaybackPlaying() { - seekTarget = NaN; + function _onPlaybackPlaying() { checkIfSufficientBuffer(); + seekTarget = NaN; } function getRangeAt(time, tolerance) { - const ranges = buffer.getAllBufferRanges(); + const ranges = sourceBufferSink.getAllBufferRanges(); let start = 0; let end = 0; let firstStart = null; @@ -548,8 +658,8 @@ function BufferController(config) { length; // Consider gap/discontinuity limit as tolerance - if (settings.get().streaming.jumpGaps) { - tolerance = settings.get().streaming.smallGapLimit; + if (settings.get().streaming.gaps.jumpGaps) { + tolerance = settings.get().streaming.gaps.smallGapLimit; } range = getRangeAt(time, tolerance); @@ -563,20 +673,22 @@ function BufferController(config) { return length; } - function updateBufferLevel() { + function _updateBufferLevel() { if (playbackController) { - bufferLevel = getBufferLength(getWorkingTime() || 0); - triggerEvent(Events.BUFFER_LEVEL_UPDATED, {bufferLevel: bufferLevel}); + const tolerance = settings.get().streaming.gaps.jumpGaps && !isNaN(settings.get().streaming.gaps.smallGapLimit) ? settings.get().streaming.gaps.smallGapLimit : NaN; + bufferLevel = Math.max(getBufferLength(playbackController.getTime() || 0, tolerance), 0); + _triggerEvent(Events.BUFFER_LEVEL_UPDATED, { mediaType: type, bufferLevel: bufferLevel }); checkIfSufficientBuffer(); } } - function checkIfBufferingCompleted() { - const isLastIdxAppended = maxAppendedIndex >= lastIndex - 1; // Handles 0 and non 0 based request index - if (isLastIdxAppended && !isBufferingCompleted && buffer.discharge === undefined) { - isBufferingCompleted = true; - logger.debug('checkIfBufferingCompleted trigger BUFFERING_COMPLETED for ' + type); - triggerEvent(Events.BUFFERING_COMPLETED); + function _checkIfBufferingCompleted() { + const isLastIdxAppended = maxAppendedIndex >= maximumIndex - 1; // Handles 0 and non 0 based request index + // To avoid rounding error when comparing, the stream time and buffer level only must be within 5 decimal places + const periodBuffered = playbackController.getTimeToStreamEnd(streamInfo) - bufferLevel < 0.00001; + if ((isLastIdxAppended || periodBuffered) && !isBufferingCompleted) { + setIsBufferingCompleted(true); + logger.debug(`checkIfBufferingCompleted trigger BUFFERING_COMPLETED for stream id ${streamInfo.id} and type ${type}`); } } @@ -584,42 +696,35 @@ function BufferController(config) { // No need to check buffer if type is not audio or video (for example if several errors occur during text parsing, so that the buffer cannot be filled, no error must occur on video playback) if (type !== Constants.AUDIO && type !== Constants.VIDEO) return; - if (seekClearedBufferingCompleted && !isBufferingCompleted && bufferLevel > 0 && playbackController && playbackController.getTimeToStreamEnd() - bufferLevel < BUFFERING_COMPLETED_THRESHOLD) { - seekClearedBufferingCompleted = false; - isBufferingCompleted = true; - logger.debug('checkIfSufficientBuffer trigger BUFFERING_COMPLETED for type ' + type); - triggerEvent(Events.BUFFERING_COMPLETED); - } - - // When the player is working in low latency mode, the buffer is often below settings.get().streaming.stallThreshold. + // When the player is working in low latency mode, the buffer is often below STALL_THRESHOLD. // So, when in low latency mode, change dash.js behavior so it notifies a stall just when // buffer reach 0 seconds - if (((!settings.get().streaming.lowLatencyEnabled && bufferLevel < settings.get().streaming.stallThreshold) || bufferLevel === 0) && !isBufferingCompleted) { - notifyBufferStateChanged(MetricsConstants.BUFFER_EMPTY); + if (((!playbackController.getLowLatencyModeEnabled() && bufferLevel < settings.get().streaming.buffer.stallThreshold) || bufferLevel === 0) && !isBufferingCompleted) { + _notifyBufferStateChanged(MetricsConstants.BUFFER_EMPTY); } else { - if (isBufferingCompleted || bufferLevel >= streamInfo.manifestInfo.minBufferTime) { - notifyBufferStateChanged(MetricsConstants.BUFFER_LOADED); + if (isBufferingCompleted || bufferLevel >= settings.get().streaming.buffer.stallThreshold || (playbackController.getLowLatencyModeEnabled() && bufferLevel > 0)) { + _notifyBufferStateChanged(MetricsConstants.BUFFER_LOADED); } } } - function notifyBufferStateChanged(state) { + function _notifyBufferStateChanged(state) { if (bufferState === state || (state === MetricsConstants.BUFFER_EMPTY && playbackController.getTime() === 0) || // Don't trigger BUFFER_EMPTY if it's initial loading - (type === Constants.FRAGMENTED_TEXT && !textController.isTextEnabled())) { + (type === Constants.TEXT && !textController.isTextEnabled())) { return; } bufferState = state; - triggerEvent(Events.BUFFER_LEVEL_STATE_CHANGED, {state: state}); - triggerEvent(state === MetricsConstants.BUFFER_LOADED ? Events.BUFFER_LOADED : Events.BUFFER_EMPTY); + _triggerEvent(Events.BUFFER_LEVEL_STATE_CHANGED, { state: state }); + _triggerEvent(state === MetricsConstants.BUFFER_LOADED ? Events.BUFFER_LOADED : Events.BUFFER_EMPTY); logger.debug(state === MetricsConstants.BUFFER_LOADED ? 'Got enough buffer to start' : 'Waiting for more buffer before starting playback'); } /* prune buffer on our own in background to avoid browsers pruning buffer silently */ function pruneBuffer() { - if (!buffer || type === Constants.FRAGMENTED_TEXT) { + if (!sourceBufferSink || type === Constants.TEXT) { return; } @@ -630,13 +735,13 @@ function BufferController(config) { function getClearRanges() { const clearRanges = []; - const ranges = buffer.getAllBufferRanges(); + const ranges = sourceBufferSink.getAllBufferRanges(); if (!ranges || ranges.length === 0) { return clearRanges; } const currentTime = playbackController.getTime(); - let startRangeToKeep = Math.max(0, currentTime - settings.get().streaming.bufferToKeep); + let startRangeToKeep = Math.max(0, currentTime - settings.get().streaming.buffer.bufferToKeep); const currentTimeRequest = fragmentModel.getRequests({ state: FragmentModel.FRAGMENT_MODEL_EXECUTED, @@ -669,77 +774,106 @@ function BufferController(config) { } function clearBuffers(ranges) { - if (!ranges || !buffer || ranges.length === 0) return; + return new Promise((resolve, reject) => { + if (!ranges || !sourceBufferSink || ranges.length === 0) { + resolve(); + return; + } - pendingPruningRanges.push.apply(pendingPruningRanges, ranges); - if (isPruningInProgress) { - return; - } + const promises = []; + ranges.forEach((range) => { + promises.push(_addClearRangeWithPromise(range)); + }); + + + if (!isPruningInProgress) { + clearNextRange(); + } + + Promise.all(promises) + .then(() => { + resolve(); + }) + .catch((e) => { + reject(e); + }); + }); + } - clearNextRange(); + function _addClearRangeWithPromise(range) { + return new Promise((resolve, reject) => { + range.resolve = resolve; + range.reject = reject; + pendingPruningRanges.push(range); + }); } function clearNextRange() { - // If there's nothing to prune reset state - if (pendingPruningRanges.length === 0 || !buffer) { - logger.debug('Nothing to prune, halt pruning'); - pendingPruningRanges = []; - isPruningInProgress = false; - return; - } + try { + // If there's nothing to prune reset state + if (pendingPruningRanges.length === 0 || !sourceBufferSink) { + logger.debug('Nothing to prune, halt pruning'); + pendingPruningRanges = []; + isPruningInProgress = false; + return; + } - const sourceBuffer = buffer.getBuffer(); - // If there's nothing buffered any pruning is invalid, so reset our state - if (!sourceBuffer || !sourceBuffer.buffered || sourceBuffer.buffered.length === 0) { - logger.debug('SourceBuffer is empty (or does not exist), halt pruning'); - pendingPruningRanges = []; - isPruningInProgress = false; - return; - } + const sourceBuffer = sourceBufferSink.getBuffer(); + // If there's nothing buffered any pruning is invalid, so reset our state + if (!sourceBuffer || !sourceBuffer.buffered || sourceBuffer.buffered.length === 0) { + logger.debug('SourceBuffer is empty (or does not exist), halt pruning'); + pendingPruningRanges = []; + isPruningInProgress = false; + return; + } - const range = pendingPruningRanges.shift(); - logger.debug('Removing buffer from:', range.start, 'to', range.end); - isPruningInProgress = true; + const range = pendingPruningRanges.shift(); + logger.debug(`${type}: Removing buffer from: ${range.start} to ${range.end}`); + isPruningInProgress = true; - // If removing buffer ahead current playback position, update maxAppendedIndex - const currentTime = playbackController.getTime(); - if (currentTime < range.end) { - isBufferingCompleted = false; - maxAppendedIndex = 0; - } + // If removing buffer ahead current playback position, update maxAppendedIndex + const currentTime = playbackController.getTime(); + if (currentTime < range.end) { + setIsBufferingCompleted(false); + } - buffer.remove(range.start, range.end, range.force); + sourceBufferSink.remove(range) + .then((e) => { + _onRemoved(e); + }) + .catch((e) => { + _onRemoved(e); + }); + } catch (e) { + isPruningInProgress = false; + } } - function onRemoved(e) { - if (buffer !== e.buffer) return; - + function _onRemoved(e) { logger.debug('onRemoved buffer from:', e.from, 'to', e.to); - const ranges = buffer.getAllBufferRanges(); - showBufferRanges(ranges); + const ranges = sourceBufferSink.getAllBufferRanges(); + _showBufferRanges(ranges); if (pendingPruningRanges.length === 0) { isPruningInProgress = false; + _updateBufferLevel(); } if (e.unintended) { - logger.warn('Detected unintended removal from:', e.from, 'to', e.to, 'setting index handler time to', e.from); - triggerEvent(Events.SEEK_TARGET, {time: e.from, mediaType: type, streamId: streamInfo.id}); + logger.warn('Detected unintended removal from:', e.from, 'to', e.to, 'setting streamprocessor time to', e.from); + _triggerEvent(Events.SEEK_TARGET, { time: e.from }); } if (isPruningInProgress) { clearNextRange(); } else { if (!replacingBuffer) { - updateBufferLevel(); + _updateBufferLevel(); } else { replacingBuffer = false; - if (mediaChunk) { - appendToBuffer(mediaChunk); - } } - triggerEvent(Events.BUFFER_CLEARED, { + _triggerEvent(Events.BUFFER_CLEARED, { from: e.from, to: e.to, unintended: e.unintended, @@ -750,117 +884,148 @@ function BufferController(config) { } function updateBufferTimestampOffset(representationInfo) { - if (!representationInfo || representationInfo.MSETimeOffset === undefined) return; - // Each track can have its own @presentationTimeOffset, so we should set the offset - // if it has changed after switching the quality or updating an mpd - if (buffer && buffer.updateTimestampOffset) { - buffer.updateTimestampOffset(representationInfo.MSETimeOffset); - } + return new Promise((resolve) => { + if (!representationInfo || representationInfo.MSETimeOffset === undefined || !sourceBufferSink || !sourceBufferSink.updateTimestampOffset) { + resolve(); + return; + } + // Each track can have its own @presentationTimeOffset, so we should set the offset + // if it has changed after switching the quality or updating an mpd + sourceBufferSink.updateTimestampOffset(representationInfo.MSETimeOffset) + .then(() => { + resolve(); + }) + .catch(() => { + resolve(); + }); + }); + } function updateAppendWindow() { - if (buffer && !isBufferingCompleted) { - buffer.updateAppendWindow(streamInfo); + if (sourceBufferSink && !isBufferingCompleted) { + return sourceBufferSink.updateAppendWindow(streamInfo); } + return Promise.resolve(); } - function onDataUpdateCompleted(e) { - if (e.error || isBufferingCompleted) return; - updateBufferTimestampOffset(e.currentRepresentation); - } - - function onStreamCompleted(e) { - lastIndex = e.request.index; - checkIfBufferingCompleted(); - } - - function onCurrentTrackChanged(e) { - if (e.newMediaInfo.streamInfo.id !== streamInfo.id || e.newMediaInfo.type !== type) return; - - const ranges = buffer && buffer.getAllBufferRanges(); - if (!ranges) return; - - logger.info('Track change asked'); - if (mediaController.getSwitchMode(type) === Constants.TRACK_SWITCH_MODE_ALWAYS_REPLACE) { - if (ranges && ranges.length > 0 && playbackController.getTimeToStreamEnd() > settings.get().streaming.stallThreshold) { - isBufferingCompleted = false; - lastIndex = Number.POSITIVE_INFINITY; - } + function segmentRequestingCompleted(segmentIndex) { + if (!isNaN(segmentIndex)) { + maximumIndex = segmentIndex; + _checkIfBufferingCompleted(); } } - function onWallclockTimeUpdated() { + function _onWallclockTimeUpdated() { wallclockTicked++; const secondsElapsed = (wallclockTicked * (settings.get().streaming.wallclockTimeUpdateInterval / 1000)); - if ((secondsElapsed >= settings.get().streaming.bufferPruningInterval)) { + if ((secondsElapsed >= settings.get().streaming.buffer.bufferPruningInterval)) { wallclockTicked = 0; pruneBuffer(); } } - function onPlaybackRateChanged() { + function _onPlaybackRateChanged() { checkIfSufficientBuffer(); } function getBuffer() { - return buffer; - } - - function setBuffer(newBuffer) { - buffer = newBuffer; + return sourceBufferSink; } function getBufferLevel() { return bufferLevel; } - function setMediaSource(value, mediaInfo) { - mediaSource = value; - if (buffer && mediaInfo) { //if we have a prebuffer, we should prepare to discharge it, and make a new sourceBuffer ready - if (typeof buffer.discharge === 'function') { - dischargeBuffer = buffer; - createBuffer(mediaInfo); - } - } - } - function getMediaSource() { return mediaSource; } - function replaceBuffer() { - replacingBuffer = true; - } - function getIsBufferingCompleted() { return isBufferingCompleted; } + function setIsBufferingCompleted(value) { + if (isBufferingCompleted === value) { + return; + } + + isBufferingCompleted = value; + + if (isBufferingCompleted) { + _triggerEvent(Events.BUFFERING_COMPLETED); + } else { + maximumIndex = Number.POSITIVE_INFINITY; + } + } + function getIsPruningInProgress() { return isPruningInProgress; } function getTotalBufferedTime() { - const ranges = buffer.getAllBufferRanges(); - let totalBufferedTime = 0; - let ln, - i; + try { + const ranges = sourceBufferSink.getAllBufferRanges(); + let totalBufferedTime = 0; + let ln, + i; + + if (!ranges) return totalBufferedTime; - if (!ranges) return totalBufferedTime; + for (i = 0, ln = ranges.length; i < ln; i++) { + totalBufferedTime += ranges.end(i) - ranges.start(i); + } - for (i = 0, ln = ranges.length; i < ln; i++) { - totalBufferedTime += ranges.end(i) - ranges.start(i); + return totalBufferedTime; + } catch (e) { + return 0; } + } + + /** + * This function returns the maximum time for which the buffer is continuous starting from a target time. + * As soon as there is a gap we return the time before the gap starts + * @param {number} targetTime + */ + function getContinuousBufferTimeForTargetTime(targetTime) { + try { + let adjustedTime = targetTime; + const ranges = sourceBufferSink.getAllBufferRanges(); + + if (!ranges || ranges.length === 0) { + return NaN; + } + + let i = 0; + + while (adjustedTime === targetTime && i < ranges.length) { + const start = ranges.start(i); + const end = ranges.end(i); + + if (adjustedTime >= start && adjustedTime <= end) { + adjustedTime = end; + } + + i += 1; + } - return totalBufferedTime; + return adjustedTime === targetTime ? NaN : adjustedTime; + + } catch (e) { + + } } function hasEnoughSpaceToAppend() { const totalBufferedTime = getTotalBufferedTime(); - return (totalBufferedTime < criticalBufferLevel); + return (isNaN(totalBufferedTime) || totalBufferedTime < criticalBufferLevel); } - function triggerEvent(eventType, data) { + function setSeekTarget(value) { + seekTarget = value; + } + + function _triggerEvent(eventType, data) { let payload = data || {}; eventBus.trigger(eventType, payload, { streamId: streamInfo.id, mediaType: type }); } @@ -868,46 +1033,43 @@ function BufferController(config) { function resetInitialSettings(errored, keepBuffers) { criticalBufferLevel = Number.POSITIVE_INFINITY; bufferState = undefined; - requiredQuality = AbrController.QUALITY_DEFAULT; - lastIndex = Number.POSITIVE_INFINITY; + maximumIndex = Number.POSITIVE_INFINITY; maxAppendedIndex = 0; appendedBytesInfo = null; isBufferingCompleted = false; isPruningInProgress = false; isQuotaExceeded = false; - seekClearedBufferingCompleted = false; bufferLevel = 0; wallclockTicked = 0; pendingPruningRanges = []; seekTarget = NaN; - if (buffer) { - if (!errored) { - buffer.abort(); + if (sourceBufferSink) { + let tmpSourceBufferSinkToReset = sourceBufferSink; + sourceBufferSink = null; + if (!errored && !keepBuffers) { + tmpSourceBufferSinkToReset.abort() + .then(() => { + tmpSourceBufferSinkToReset.reset(keepBuffers); + tmpSourceBufferSinkToReset = null; + }); } - buffer.reset(keepBuffers); - buffer = null; } replacingBuffer = false; } function reset(errored, keepBuffers) { - eventBus.off(Events.DATA_UPDATE_COMPLETED, onDataUpdateCompleted, this); - eventBus.off(Events.INIT_FRAGMENT_LOADED, onInitFragmentLoaded, this); - eventBus.off(Events.MEDIA_FRAGMENT_LOADED, onMediaFragmentLoaded, this); - eventBus.off(Events.QUALITY_CHANGE_REQUESTED, onQualityChanged, this); - eventBus.off(Events.STREAM_COMPLETED, onStreamCompleted, this); - eventBus.off(Events.PLAYBACK_PLAYING, onPlaybackPlaying, this); - eventBus.off(Events.PLAYBACK_PROGRESS, onPlaybackProgression, this); - eventBus.off(Events.PLAYBACK_TIME_UPDATED, onPlaybackProgression, this); - eventBus.off(Events.PLAYBACK_RATE_CHANGED, onPlaybackRateChanged, this); - eventBus.off(Events.PLAYBACK_SEEKING, onPlaybackSeeking, this); - eventBus.off(Events.PLAYBACK_SEEKED, onPlaybackSeeked, this); - eventBus.off(Events.PLAYBACK_STALLED, onPlaybackStalled, this); - eventBus.off(Events.WALLCLOCK_TIME_UPDATED, onWallclockTimeUpdated, this); - eventBus.off(Events.CURRENT_TRACK_CHANGED, onCurrentTrackChanged, this); - eventBus.off(Events.SOURCEBUFFER_REMOVE_COMPLETED, onRemoved, this); + eventBus.off(Events.INIT_FRAGMENT_LOADED, _onInitFragmentLoaded, this); + eventBus.off(Events.MEDIA_FRAGMENT_LOADED, _onMediaFragmentLoaded, this); + eventBus.off(Events.WALLCLOCK_TIME_UPDATED, _onWallclockTimeUpdated, this); + + eventBus.off(MediaPlayerEvents.PLAYBACK_PLAYING, _onPlaybackPlaying, this); + eventBus.off(MediaPlayerEvents.PLAYBACK_PROGRESS, _onPlaybackProgression, this); + eventBus.off(MediaPlayerEvents.PLAYBACK_TIME_UPDATED, _onPlaybackProgression, this); + eventBus.off(MediaPlayerEvents.PLAYBACK_RATE_CHANGED, _onPlaybackRateChanged, this); + eventBus.off(MediaPlayerEvents.PLAYBACK_STALLED, _onPlaybackStalled, this); + resetInitialSettings(errored, keepBuffers); } @@ -917,21 +1079,29 @@ function BufferController(config) { getStreamId, getType, getBufferControllerType, - getRepresentationInfo, - createBuffer, - dischargePreBuffer, + createBufferSink, getBuffer, - setBuffer, getBufferLevel, getRangeAt, setMediaSource, getMediaSource, - appendInitSegment, - replaceBuffer, + appendInitSegmentFromCache, getIsBufferingCompleted, + setIsBufferingCompleted, getIsPruningInProgress, reset, - updateAppendWindow + prepareForPlaybackSeek, + prepareForReplacementTrackSwitch, + prepareForNonReplacementTrackSwitch, + prepareForReplacementQualitySwitch, + updateAppendWindow, + getAllRangesWithSafetyFactor, + getContinuousBufferTimeForTargetTime, + clearBuffers, + pruneAllSafely, + updateBufferTimestampOffset, + setSeekTarget, + segmentRequestingCompleted }; setup(); diff --git a/src/streaming/controllers/CatchupController.js b/src/streaming/controllers/CatchupController.js new file mode 100644 index 0000000000..260259d1d3 --- /dev/null +++ b/src/streaming/controllers/CatchupController.js @@ -0,0 +1,414 @@ +/** + * The copyright in this software is being made available under the BSD License, + * included below. This software may be subject to other third party and contributor + * rights, including patent rights, and no such rights are granted under this license. + * + * Copyright (c) 2013, Dash Industry Forum. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * * Neither the name of Dash Industry Forum nor the names of its + * contributors may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +import FactoryMaker from '../../core/FactoryMaker'; +import Debug from '../../core/Debug'; +import EventBus from '../../core/EventBus'; +import Constants from '../constants/Constants'; +import MediaPlayerEvents from '../MediaPlayerEvents'; +import Events from '../../core/events/Events'; +import MetricsConstants from '../constants/MetricsConstants'; +import Utils from '../../core/Utils'; + +function CatchupController() { + const context = this.context; + const eventBus = EventBus(context).getInstance(); + + let instance, + isCatchupSeekInProgress, + isSafari, + videoModel, + settings, + streamController, + playbackController, + mediaPlayerModel, + playbackStalled, + logger; + + function initialize() { + _registerEvents(); + } + + function setConfig(config) { + if (!config) { + return; + } + + if (config.settings) { + settings = config.settings; + } + + if (config.videoModel) { + videoModel = config.videoModel; + } + + if (config.streamController) { + streamController = config.streamController; + } + + if (config.playbackController) { + playbackController = config.playbackController; + } + + if (config.mediaPlayerModel) { + mediaPlayerModel = config.mediaPlayerModel; + } + } + + function _registerEvents() { + eventBus.on(MediaPlayerEvents.BUFFER_LEVEL_UPDATED, _onBufferLevelUpdated, instance); + eventBus.on(MediaPlayerEvents.BUFFER_LEVEL_STATE_CHANGED, _onBufferLevelStateChanged, instance); + eventBus.on(MediaPlayerEvents.PLAYBACK_PROGRESS, _onPlaybackProgression, instance); + eventBus.on(MediaPlayerEvents.PLAYBACK_TIME_UPDATED, _onPlaybackProgression, instance); + eventBus.on(MediaPlayerEvents.PLAYBACK_SEEKED, _onPlaybackSeeked, instance); + eventBus.on(Events.SETTING_UPDATED_CATCHUP_ENABLED, _onCatchupSettingUpdated, instance); + } + + function _unregisterEvents() { + eventBus.off(MediaPlayerEvents.BUFFER_LEVEL_UPDATED, _onBufferLevelUpdated, instance); + eventBus.off(MediaPlayerEvents.BUFFER_LEVEL_STATE_CHANGED, _onBufferLevelStateChanged, instance); + eventBus.off(MediaPlayerEvents.PLAYBACK_PROGRESS, _onPlaybackProgression, instance); + eventBus.off(MediaPlayerEvents.PLAYBACK_TIME_UPDATED, _onPlaybackProgression, instance); + eventBus.off(MediaPlayerEvents.PLAYBACK_SEEKED, _onPlaybackProgression, instance); + eventBus.off(Events.SETTING_UPDATED_CATCHUP_ENABLED, _onCatchupSettingUpdated, instance); + } + + function setup() { + logger = Debug(context).getInstance().getLogger(instance); + + _resetInitialSettings(); + } + + function reset() { + _unregisterEvents(); + _resetInitialSettings(); + videoModel.setPlaybackRate(1.0, true); + } + + function _resetInitialSettings() { + isCatchupSeekInProgress = false; + const ua = Utils.parseUserAgent(); + isSafari = ua && ua.browser && ua.browser.name && ua.browser.name.toLowerCase() === 'safari'; + } + + + function _onPlaybackSeeked() { + isCatchupSeekInProgress = false; + } + + /** + * When the buffer level updated we check if we can remove the stalled state + * @param {object} e + * @private + */ + function _onBufferLevelUpdated(e) { + // do not stop when getting an event from Stream that is not active + if (e.streamId !== streamController.getActiveStreamInfo().id || !playbackStalled) { + return; + } + + // we remove the stalled state once we reach a certain buffer level + const liveDelay = playbackController.getLiveDelay(); + const bufferLevel = playbackController.getBufferLevel(); + if (bufferLevel > liveDelay / 2) { + playbackStalled = false; + } + } + + /** + * When the buffer state changed to BUFFER_EMPTY we update the stalled state + * @param {object} e + * @private + */ + function _onBufferLevelStateChanged(e) { + // do not stop when getting an event from Stream that is not active + if (e.streamId !== streamController.getActiveStreamInfo().id) { + return; + } + + playbackStalled = e.state === MetricsConstants.BUFFER_EMPTY; + } + + /** + * If the catchup mode is disabled in the settings we reset playback rate to 1.0 + * @private + */ + function _onCatchupSettingUpdated() { + if (!mediaPlayerModel.getCatchupModeEnabled()) { + videoModel.setPlaybackRate(1.0); + } + } + + /** + * While playback is progressing we check if we need to start or stop the catchup mechanism to reach the target latency + * @private + */ + function _onPlaybackProgression() { + if (playbackController.getIsDynamic() && mediaPlayerModel.getCatchupModeEnabled() && mediaPlayerModel.getCatchupPlaybackRate() > 0 && !playbackController.isPaused() && !playbackController.isSeeking() && _shouldStartCatchUp()) { + _startPlaybackCatchUp(); + } + } + + /** + * Apply catchup mode. We either seek back to the target live edge or increase the playback rate. + */ + function _startPlaybackCatchUp() { + + // we are seeking dont do anything for now + if (isCatchupSeekInProgress) { + return; + } + + if (videoModel) { + let newRate; + const currentPlaybackRate = videoModel.getPlaybackRate(); + const liveCatchupPlaybackRate = mediaPlayerModel.getCatchupPlaybackRate(); + const bufferLevel = playbackController.getBufferLevel(); + const deltaLatency = _getLatencyDrift(); + + // we reached the maxDrift. Do a seek + const maxDrift = mediaPlayerModel.getCatchupMaxDrift(); + if (!isNaN(maxDrift) && maxDrift > 0 && + deltaLatency > maxDrift) { + logger.info('[CatchupController]: Low Latency catchup mechanism. Latency too high, doing a seek to live point'); + isCatchupSeekInProgress = true; + playbackController.seekToCurrentLive(true, false); + } + + // try to reach the target latency by adjusting the playback rate + else { + const currentLiveLatency = playbackController.getCurrentLiveLatency(); + const targetLiveDelay = playbackController.getLiveDelay(); + + if (_getCatchupMode() === Constants.LIVE_CATCHUP_MODE_LOLP) { + // Custom playback control: Based on buffer level + const playbackBufferMin = settings.get().streaming.liveCatchup.playbackBufferMin; + newRate = _calculateNewPlaybackRateLolP(liveCatchupPlaybackRate, currentLiveLatency, targetLiveDelay, playbackBufferMin, bufferLevel, currentPlaybackRate); + } else { + // Default playback control: Based on target and current latency + newRate = _calculateNewPlaybackRateDefault(liveCatchupPlaybackRate, currentLiveLatency, targetLiveDelay, bufferLevel, currentPlaybackRate); + } + + // We adjust the min change linear, depending on the maximum catchup rate. Default is 0.02 for rate 0.5. + // For Safari we stick to a fixed value because of https://bugs.webkit.org/show_bug.cgi?id=208142 + const minPlaybackRateChange = isSafari ? 0.25 : 0.02 / (0.5 / liveCatchupPlaybackRate); + + // Obtain newRate and apply to video model. Don't change playbackrate for small variations (don't overload element with playbackrate changes) + if (newRate && Math.abs(currentPlaybackRate - newRate) >= minPlaybackRateChange) { // non-null + logger.debug(`[CatchupController]: Setting playback rate to ${newRate}`); + videoModel.setPlaybackRate(newRate); + } + } + } + } + + /** + * Calculates the drift between the current latency and the target latency + * @return {number} + * @private + */ + function _getLatencyDrift() { + const currentLiveLatency = playbackController.getCurrentLiveLatency(); + const targetLiveDelay = playbackController.getLiveDelay(); + + return currentLiveLatency - targetLiveDelay; + } + + /** + * Checks whether the catchup mechanism should be enabled. We use different subfunctions here depending on the catchup mode. + * @return {boolean} + */ + function _shouldStartCatchUp() { + try { + if (!playbackController.getTime() > 0 || isCatchupSeekInProgress) { + return false; + } + + const catchupMode = _getCatchupMode(); + + if (catchupMode === Constants.LIVE_CATCHUP_MODE_LOLP) { + const currentBuffer = playbackController.getBufferLevel(); + const playbackBufferMin = settings.get().streaming.liveCatchup.playbackBufferMin; + + return _lolpNeedToCatchUpCustom(currentBuffer, playbackBufferMin); + } else { + return _defaultNeedToCatchUp(); + } + + } catch (e) { + return false; + } + } + + + /** + * Returns the mode for live playback catchup. + * @return {String} + * @private + */ + function _getCatchupMode() { + const playbackBufferMin = settings.get().streaming.liveCatchup.playbackBufferMin; + + return settings.get().streaming.liveCatchup.mode === Constants.LIVE_CATCHUP_MODE_LOLP && playbackBufferMin !== null && !isNaN(playbackBufferMin) ? Constants.LIVE_CATCHUP_MODE_LOLP : Constants.LIVE_CATCHUP_MODE_DEFAULT; + } + + /** + * Default algorithm to determine if catchup mode should be enabled + * @return {boolean} + * @private + */ + function _defaultNeedToCatchUp() { + try { + const latencyDrift = Math.abs(_getLatencyDrift()); + + return latencyDrift > 0; + } catch (e) { + return false; + } + } + + /** + * LoL+ logic to determine if catchup mode should be enabled + * @param {number} currentBuffer + * @param {number} playbackBufferMin + * @return {boolean} + * @private + */ + function _lolpNeedToCatchUpCustom(currentBuffer, playbackBufferMin) { + try { + const latencyDrift = Math.abs(_getLatencyDrift()); + + return latencyDrift > 0 || currentBuffer < playbackBufferMin; + } catch (e) { + return false; + } + } + + /** + * Default algorithm to calculate the new playback rate + * @param {number} liveCatchUpPlaybackRate + * @param {number} currentLiveLatency + * @param {number} liveDelay + * @param {number} bufferLevel + * @param {number} currentPlaybackRate + * @return {number} + * @private + */ + function _calculateNewPlaybackRateDefault(liveCatchUpPlaybackRate, currentLiveLatency, liveDelay, bufferLevel) { + + // if we recently ran into an empty buffer we wait for the buffer to recover before applying a new rate + if (playbackStalled) { + return 1.0; + } + + const cpr = liveCatchUpPlaybackRate; + const deltaLatency = currentLiveLatency - liveDelay; + const d = deltaLatency * 5; + + // Playback rate must be between (1 - cpr) - (1 + cpr) + // ex: if cpr is 0.5, it can have values between 0.5 - 1.5 + const s = (cpr * 2) / (1 + Math.pow(Math.E, -d)); + let newRate = (1 - cpr) + s; + // take into account situations in which there are buffer stalls, + // in which increasing playbackRate to reach target latency will + // just cause more and more stall situations + if (playbackController.getPlaybackStalled()) { + if (bufferLevel <= liveDelay / 2 && deltaLatency > 0) { + newRate = 1.0; + } + } + + return newRate; + } + + /** + * Lol+ algorithm to calculate the new playback rate + * @param {number} liveCatchUpPlaybackRate + * @param {number} currentLiveLatency + * @param {number} liveDelay + * @param {number} playbackBufferMin + * @param {number} bufferLevel + * @param {number} currentPlaybackRate + * @return {number} + * @private + */ + function _calculateNewPlaybackRateLolP(liveCatchUpPlaybackRate, currentLiveLatency, liveDelay, playbackBufferMin, bufferLevel) { + const cpr = liveCatchUpPlaybackRate; + let newRate; + + // Hybrid: Buffer-based + if (bufferLevel < playbackBufferMin) { + // Buffer in danger, slow down + const deltaBuffer = bufferLevel - playbackBufferMin; // -ve value + const d = deltaBuffer * 5; + + // Playback rate must be between (1 - cpr) - (1 + cpr) + // ex: if cpr is 0.5, it can have values between 0.5 - 1.5 + const s = (cpr * 2) / (1 + Math.pow(Math.E, -d)); + newRate = (1 - cpr) + s; + + logger.debug('[LoL+ playback control_buffer-based] bufferLevel: ' + bufferLevel + ', newRate: ' + newRate); + } else { + // Hybrid: Latency-based + // Buffer is safe, vary playback rate based on latency + + // Check if latency is within range of target latency + const minDifference = 0.02; + if (Math.abs(currentLiveLatency - liveDelay) <= (minDifference * liveDelay)) { + newRate = 1; + } else { + const deltaLatency = currentLiveLatency - liveDelay; + const d = deltaLatency * 5; + + // Playback rate must be between (1 - cpr) - (1 + cpr) + // ex: if cpr is 0.5, it can have values between 0.5 - 1.5 + const s = (cpr * 2) / (1 + Math.pow(Math.E, -d)); + newRate = (1 - cpr) + s; + } + + logger.debug('[LoL+ playback control_latency-based] latency: ' + currentLiveLatency + ', newRate: ' + newRate); + } + + return newRate + } + + instance = { + reset, + setConfig, + initialize + }; + + setup(); + + return instance; +} + +CatchupController.__dashjs_factory_name = 'CatchupController'; +export default FactoryMaker.getSingletonFactory(CatchupController); diff --git a/src/streaming/controllers/EventController.js b/src/streaming/controllers/EventController.js index be026e6501..3ea770a661 100644 --- a/src/streaming/controllers/EventController.js +++ b/src/streaming/controllers/EventController.js @@ -88,8 +88,8 @@ function EventController() { */ function _resetInitialSettings() { isStarted = false; - inlineEvents = {}; // Format inlineEvents[schemeIdUri] - inbandEvents = {}; // Format inlineEvents[schemeIdUri] + inlineEvents = {}; // Format inlineEvents[periodID][schemeIdUri] + inbandEvents = {}; // Format inbandEvents[periodID][schemeIdUri] eventInterval = null; eventHandlingInProgress = false; lastEventTimerCall = Date.now() / 1000; @@ -128,31 +128,112 @@ function EventController() { } } + /** + * Iterate through the eventList and trigger the events + */ + function _onEventTimer() { + try { + if (!eventHandlingInProgress) { + eventHandlingInProgress = true; + const currentVideoTime = playbackController.getTime(); + let presentationTimeThreshold = (currentVideoTime - lastEventTimerCall); + + // For dynamic streams lastEventTimeCall will be large in the first iteration. Avoid firing all events at once. + presentationTimeThreshold = lastEventTimerCall > 0 ? Math.max(0, presentationTimeThreshold) : 0; + + _triggerEvents(inbandEvents, presentationTimeThreshold, currentVideoTime); + _triggerEvents(inlineEvents, presentationTimeThreshold, currentVideoTime); + _removeOutdatedEventObjects(inbandEvents); + _removeOutdatedEventObjects(inlineEvents); + + lastEventTimerCall = currentVideoTime; + eventHandlingInProgress = false; + } + } catch (e) { + eventHandlingInProgress = false; + logger.error(e); + } + } + + /** + * Iterate over a list of events and trigger the ones for which the presentation time is within the current timing interval + * @param {object} events + * @param {number} presentationTimeThreshold + * @param {number} currentVideoTime + * @private + */ + function _triggerEvents(events, presentationTimeThreshold, currentVideoTime) { + try { + const callback = function (event) { + if (event !== undefined) { + const duration = !isNaN(event.duration) ? event.duration : 0; + // The event is either about to start or has already been started and we are within its duration + if ((event.calculatedPresentationTime <= currentVideoTime && event.calculatedPresentationTime + presentationTimeThreshold + duration >= currentVideoTime)) { + _startEvent(event, MediaPlayerEvents.EVENT_MODE_ON_START); + } else if (_eventHasExpired(currentVideoTime, duration + presentationTimeThreshold, event.calculatedPresentationTime) || _eventIsInvalid(event)) { + logger.debug(`Removing event ${event.id} from period ${event.eventStream.period.id} as it is expired or invalid`); + _removeEvent(events, event); + } + } + }; + + _iterateAndTriggerCallback(events, callback); + } catch (e) { + logger.error(e); + } + } + + /** + * Iterates over the entries of the events object and deletes the entries for which no events are present + * @param {object} events + * @private + */ + function _removeOutdatedEventObjects(events) { + try { + for (const key in events) { + if (events.hasOwnProperty(key)) { + if (Object.keys(events[key]).length === 0) + delete events[key]; + } + } + } catch (e) { + logger.error(e); + } + } + /** * Add MPD events to the list of events. * Events that are not in the MPD anymore but not triggered yet will still be deleted. * Existing events might get updated. * @param {Array.} values + * @param {string} periodId */ - function addInlineEvents(values) { + function addInlineEvents(values, periodId) { try { checkConfig(); + if (!inlineEvents[periodId]) { + inlineEvents[periodId] = {}; + } + if (values) { for (let i = 0; i < values.length; i++) { let event = values[i]; - let result = _addOrUpdateEvent(event, inlineEvents, true); - - if (result === EVENT_HANDLED_STATES.ADDED) { - logger.debug(`Added inline event with id ${event.id}`); - // If we see the event for the first time we trigger it in onReceive mode - _startEvent(event, values, MediaPlayerEvents.EVENT_MODE_ON_RECEIVE); - } else if (result === EVENT_HANDLED_STATES.UPDATED) { - logger.debug(`Updated inline event with id ${event.id}`); + const currentTime = playbackController.getTime(); + const duration = !isNaN(event.duration) ? event.duration : 0; + if (!_eventHasExpired(currentTime, duration, event.calculatedPresentationTime)) { + let result = _addOrUpdateEvent(event, inlineEvents[periodId], true); + + if (result === EVENT_HANDLED_STATES.ADDED) { + logger.debug(`Added inline event with id ${event.id} from period ${periodId}`); + // If we see the event for the first time we trigger it in onReceive mode + _startEvent(event, MediaPlayerEvents.EVENT_MODE_ON_RECEIVE); + } else if (result === EVENT_HANDLED_STATES.UPDATED) { + logger.debug(`Updated inline event with id ${event.id} from period ${periodId}`); + } } } } - logger.debug(`Added ${values.length} inline events`); } catch (e) { throw e; } @@ -162,23 +243,32 @@ function EventController() { * Add EMSG events to the list of events * Messages with the same id within the scope of the same scheme_id_uri and value pair are equivalent , i.e. processing of any one event message box with the same id is sufficient. * @param {Array.} values + * @param {string} periodId */ - function addInbandEvents(values) { + function addInbandEvents(values, periodId) { try { checkConfig(); + if (!inbandEvents[periodId]) { + inbandEvents[periodId] = {}; + } + for (let i = 0; i < values.length; i++) { let event = values[i]; - let result = _addOrUpdateEvent(event, inbandEvents, false); + const currentTime = playbackController.getTime(); + const duration = !isNaN(event.duration) ? event.duration : 0; + if (!_eventHasExpired(currentTime, duration, event.calculatedPresentationTime)) { + let result = _addOrUpdateEvent(event, inbandEvents[periodId], false); - if (result === EVENT_HANDLED_STATES.ADDED) { - if (event.eventStream.schemeIdUri === MPD_RELOAD_SCHEME && inbandEvents[event.id] === undefined) { - _handleManifestReloadEvent(event); + if (result === EVENT_HANDLED_STATES.ADDED) { + if (event.eventStream.schemeIdUri === MPD_RELOAD_SCHEME) { + _handleManifestReloadEvent(event); + } + logger.debug(`Added inband event with id ${event.id} from period ${periodId}`); + _startEvent(event, MediaPlayerEvents.EVENT_MODE_ON_RECEIVE); + } else { + logger.debug(`Inband event with scheme_id_uri ${event.eventStream.schemeIdUri}, value ${event.eventStream.value}, period id ${periodId} and event id ${event.id} was ignored because it has been added before.`); } - logger.debug('Added inband event with id ' + event.id); - _startEvent(event, values, MediaPlayerEvents.EVENT_MODE_ON_RECEIVE); - } else { - logger.debug(`Inband event with scheme_id_uri ${event.eventStream.schemeIdUri}, value ${event.eventStream.value} and id ${event.id} was ignored because it has been added before.`); } } _onEventTimer(); @@ -209,10 +299,19 @@ function EventController() { return ((!value || (e.eventStream.value && e.eventStream.value === value)) && (e.id === id)); }); + // New event, we add it to our list of events if (indexOfExistingEvent === -1) { events[schemeIdUri].push(event); + event.triggeredReceivedEvent = false; + event.triggeredStartEvent = false; eventState = EVENT_HANDLED_STATES.ADDED; - } else if (shouldOverwriteExistingEvents) { + } + + // We have a similar event for this period with the same schemeIdUri, value and id. Overwrite it or ignore it + else if (shouldOverwriteExistingEvents) { + const oldEvent = events[schemeIdUri][indexOfExistingEvent]; + event.triggeredReceivedEvent = oldEvent.triggeredReceivedEvent; + event.triggeredStartEvent = oldEvent.triggeredStartEvent; events[schemeIdUri][indexOfExistingEvent] = event; eventState = EVENT_HANDLED_STATES.UPDATED; } @@ -246,30 +345,7 @@ function EventController() { }); } } catch (e) { - } - } - - /** - * Iterate through the eventList and trigger the events - */ - function _onEventTimer() { - try { - if (!eventHandlingInProgress) { - eventHandlingInProgress = true; - const currentVideoTime = playbackController.getTime(); - let presentationTimeThreshold = (currentVideoTime - lastEventTimerCall); - - // For dynamic streams lastEventTimeCall will be large in the first iteration. Avoid firing all events at once. - presentationTimeThreshold = lastEventTimerCall > 0 ? Math.max(0, presentationTimeThreshold) : 0; - - _triggerEvents(inbandEvents, presentationTimeThreshold, currentVideoTime); - _triggerEvents(inlineEvents, presentationTimeThreshold, currentVideoTime); - - lastEventTimerCall = currentVideoTime; - eventHandlingInProgress = false; - } - } catch (e) { - eventHandlingInProgress = false; + logger.error(e); } } @@ -283,34 +359,7 @@ function EventController() { _triggerRemainingEvents(inbandEvents); _triggerRemainingEvents(inlineEvents); } catch (e) { - - } - } - - /** - * Iterate over a list of events and trigger the ones for which the presentation time is within the current timing interval - * @param {object} events - * @param {number} presentationTimeThreshold - * @param {number} currentVideoTime - * @private - */ - function _triggerEvents(events, presentationTimeThreshold, currentVideoTime) { - try { - const callback = function (event) { - if (event !== undefined) { - const duration = !isNaN(event.duration) ? event.duration : 0; - // The event is either about to start or has already been started and we are within its duration - if ((event.calculatedPresentationTime <= currentVideoTime && event.calculatedPresentationTime + presentationTimeThreshold + duration >= currentVideoTime)) { - _startEvent(event, events, MediaPlayerEvents.EVENT_MODE_ON_START); - } else if (_eventHasExpired(currentVideoTime, duration + presentationTimeThreshold, event.calculatedPresentationTime) || _eventIsInvalid(event)) { - logger.debug(`Deleting event ${event.id} as it is expired or invalid`); - _removeEvent(events, event); - } - } - }; - - _iterateAndTriggerCallback(events, callback); - } catch (e) { + logger.error(e); } } @@ -333,15 +382,15 @@ function EventController() { const calculatedPresentationTimeInSeconds = event.calculatedPresentationTime; if (Math.abs(calculatedPresentationTimeInSeconds - currentTime) < REMAINING_EVENTS_THRESHOLD) { - _startEvent(event, events, MediaPlayerEvents.EVENT_MODE_ON_START); + _startEvent(event, MediaPlayerEvents.EVENT_MODE_ON_START); } }; - _iterateAndTriggerCallback(events, callback()); + _iterateAndTriggerCallback(events, callback); } catch (e) { - + logger.error(e); } } @@ -354,18 +403,22 @@ function EventController() { function _iterateAndTriggerCallback(events, callback) { try { if (events) { - const schemeIdUris = Object.keys(events); - for (let i = 0; i < schemeIdUris.length; i++) { - const schemeIdEvents = events[schemeIdUris[i]]; - schemeIdEvents.forEach((event) => { - if (event !== undefined) { - callback(event); - } - }); + const periodIds = Object.keys(events); + for (let i = 0; i < periodIds.length; i++) { + const currentPeriod = events[periodIds[i]]; + const schemeIdUris = Object.keys(currentPeriod); + for (let j = 0; j < schemeIdUris.length; j++) { + const schemeIdEvents = currentPeriod[schemeIdUris[j]]; + schemeIdEvents.forEach((event) => { + if (event !== undefined) { + callback(event); + } + }); + } } } } catch (e) { - + logger.error(e); } } @@ -381,6 +434,7 @@ function EventController() { try { return currentVideoTime - threshold > calculatedPresentationTimeInSeconds; } catch (e) { + logger.error(e); return false; } } @@ -397,49 +451,50 @@ function EventController() { return event.calculatedPresentationTime > periodEndTime; } catch (e) { + logger.error(e); return false; } } /** - * Starts an event. Depending on the schemeIdUri we distinguis between + * Starts an event. Depending on the schemeIdUri we distinguish between * - MPD Reload events * - MPD Callback events * - Events to be dispatched to the application - * Events should be removed from the list before beeing triggered. Otherwise the event handler might cause an error and the remove function will not be called. * @param {object} event - * @param {object} events * @param {String} mode * @private */ - function _startEvent(event, events, mode) { + function _startEvent(event, mode) { try { const currentVideoTime = playbackController.getTime(); const eventId = event.id; - if (mode === MediaPlayerEvents.EVENT_MODE_ON_RECEIVE) { + if (mode === MediaPlayerEvents.EVENT_MODE_ON_RECEIVE && !event.triggeredReceivedEvent) { logger.debug(`Received event ${eventId}`); + event.triggeredReceivedEvent = true; eventBus.trigger(event.eventStream.schemeIdUri, { event: event }, { mode }); return; } - if (event.eventStream.schemeIdUri === MPD_RELOAD_SCHEME && event.eventStream.value === MPD_RELOAD_VALUE) { - if (event.duration !== 0 || event.presentationTimeDelta !== 0) { //If both are set to zero, it indicates the media is over at this point. Don't reload the manifest. - logger.debug(`Starting manifest refresh event ${eventId} at ${currentVideoTime}`); - _removeEvent(events, event); - _refreshManifest(); + if (!event.triggeredStartEvent) { + if (event.eventStream.schemeIdUri === MPD_RELOAD_SCHEME && event.eventStream.value == MPD_RELOAD_VALUE) { + //If both are set to zero, it indicates the media is over at this point. Don't reload the manifest. + if (event.duration !== 0 || event.presentationTimeDelta !== 0) { + logger.debug(`Starting manifest refresh event ${eventId} at ${currentVideoTime}`); + _refreshManifest(); + } + } else if (event.eventStream.schemeIdUri === MPD_CALLBACK_SCHEME && event.eventStream.value == MPD_CALLBACK_VALUE) { + logger.debug(`Starting callback event ${eventId} at ${currentVideoTime}`); + _sendCallbackRequest(event.messageData); + } else { + logger.debug(`Starting event ${eventId} from period ${event.eventStream.period.id} at ${currentVideoTime}`); + eventBus.trigger(event.eventStream.schemeIdUri, { event: event }, { mode }); } - } else if (event.eventStream.schemeIdUri === MPD_CALLBACK_SCHEME && event.eventStream.value === MPD_CALLBACK_VALUE) { - logger.debug(`Starting callback event ${eventId} at ${currentVideoTime}`); - _removeEvent(events, event); - _sendCallbackRequest(event.messageData); - } else { - logger.debug(`Starting event ${eventId} at ${currentVideoTime}`); - _removeEvent(events, event); - eventBus.trigger(event.eventStream.schemeIdUri, { event: event }, { mode }); + event.triggeredStartEvent = true; } - } catch (e) { + logger.error(e); } } @@ -450,18 +505,22 @@ function EventController() { * @private */ function _removeEvent(events, event) { - const schemeIdUri = event.eventStream.schemeIdUri; - const value = event.eventStream.value; - const id = event.id; + try { + const schemeIdUri = event.eventStream.schemeIdUri; + const periodId = event.eventStream.period.id; + const value = event.eventStream.value; + const id = event.id; - events[schemeIdUri] = events[schemeIdUri].filter((e) => { - return (value && e.eventStream.value && e.eventStream.value !== value) || (e.id !== id); - }); + events[periodId][schemeIdUri] = events[periodId][schemeIdUri].filter((e) => { + return (value && e.eventStream.value && e.eventStream.value !== value) || e.id !== id; + }); - if (events[schemeIdUri].length === 0) { - delete events[schemeIdUri]; + if (events[periodId][schemeIdUri].length === 0) { + delete events[periodId][schemeIdUri]; + } + } catch (e) { + logger.error(e); } - } /** @@ -473,6 +532,7 @@ function EventController() { checkConfig(); manifestUpdater.refreshManifest(); } catch (e) { + logger.error(e); } } @@ -492,7 +552,7 @@ function EventController() { } }); } catch (e) { - throw e; + logger.error(e); } } diff --git a/src/streaming/controllers/FragmentController.js b/src/streaming/controllers/FragmentController.js index e7baba01a5..a2412edf91 100644 --- a/src/streaming/controllers/FragmentController.js +++ b/src/streaming/controllers/FragmentController.js @@ -35,11 +35,12 @@ import FragmentLoader from '../FragmentLoader'; import RequestModifier from '../utils/RequestModifier'; import EventBus from '../../core/EventBus'; import Events from '../../core/events/Events'; +import MediaPlayerEvents from '../MediaPlayerEvents'; import Errors from '../../core/errors/Errors'; import FactoryMaker from '../../core/FactoryMaker'; import Debug from '../../core/Debug'; -function FragmentController( config ) { +function FragmentController(config) { config = config || {}; const context = this.context; @@ -58,8 +59,8 @@ function FragmentController( config ) { function setup() { logger = debug.getLogger(instance); resetInitialSettings(); - eventBus.on(Events.FRAGMENT_LOADING_COMPLETED, onFragmentLoadingCompleted, instance); - eventBus.on(Events.FRAGMENT_LOADING_PROGRESS, onFragmentLoadingCompleted, instance); + eventBus.on(MediaPlayerEvents.FRAGMENT_LOADING_COMPLETED, onFragmentLoadingCompleted, instance); + eventBus.on(MediaPlayerEvents.FRAGMENT_LOADING_PROGRESS, onFragmentLoadingCompleted, instance); } function getStreamId() { @@ -84,7 +85,8 @@ function FragmentController( config ) { events: Events, errors: Errors, dashConstants: config.dashConstants, - urlUtils: config.urlUtils + urlUtils: config.urlUtils, + streamId: getStreamId() }), debug: debug, eventBus: eventBus, @@ -105,8 +107,8 @@ function FragmentController( config ) { } function reset() { - eventBus.off(Events.FRAGMENT_LOADING_COMPLETED, onFragmentLoadingCompleted, this); - eventBus.off(Events.FRAGMENT_LOADING_PROGRESS, onFragmentLoadingCompleted, this); + eventBus.off(MediaPlayerEvents.FRAGMENT_LOADING_COMPLETED, onFragmentLoadingCompleted, this); + eventBus.off(MediaPlayerEvents.FRAGMENT_LOADING_PROGRESS, onFragmentLoadingCompleted, this); resetInitialSettings(); } @@ -138,7 +140,7 @@ function FragmentController( config ) { const strInfo = request.mediaInfo.streamInfo; if (e.error) { - if (request.mediaType === Constants.AUDIO || request.mediaType === Constants.VIDEO || request.mediaType === Constants.FRAGMENTED_TEXT) { + if (request.mediaType === Constants.AUDIO || request.mediaType === Constants.VIDEO || (request.mediaType === Constants.TEXT && request.mediaInfo.isFragmented)) { // add service location to blacklist controller - only for audio or video. text should not set errors eventBus.trigger(Events.SERVICE_LOCATION_BLACKLIST_ADD, { entry: e.request.serviceLocation }); } diff --git a/src/streaming/controllers/GapController.js b/src/streaming/controllers/GapController.js index ee05f0fd4e..d5930de4db 100644 --- a/src/streaming/controllers/GapController.js +++ b/src/streaming/controllers/GapController.js @@ -32,10 +32,11 @@ import FactoryMaker from '../../core/FactoryMaker'; import Debug from '../../core/Debug'; import Events from '../../core/events/Events'; import EventBus from '../../core/EventBus'; +import Constants from '../constants/Constants'; const GAP_HANDLER_INTERVAL = 100; -const THRESHOLD_TO_STALLS = 30; -const GAP_THRESHOLD = 0.1; +const THRESHOLD_TO_STALLS = 10; +const GAP_JUMP_WAITING_TIME_OFFSET = 0.1; function GapController() { const context = this.context; @@ -50,13 +51,12 @@ function GapController() { playbackController, streamController, videoModel, - timelineConverter, - adapter, jumpTimeoutHandler, + trackSwitchByMediaType, logger; function initialize() { - registerEvents(); + _registerEvents(); } function setup() { @@ -66,8 +66,8 @@ function GapController() { } function reset() { - stopGapHandler(); - unregisterEvents(); + _stopGapHandler(); + _unregisterEvents(); resetInitialSettings(); } @@ -76,6 +76,7 @@ function GapController() { lastGapJumpPosition = NaN; wallclockTicked = 0; jumpTimeoutHandler = null; + trackSwitchByMediaType = {}; } function setConfig(config) { @@ -94,32 +95,28 @@ function GapController() { if (config.videoModel) { videoModel = config.videoModel; } - if (config.timelineConverter) { - timelineConverter = config.timelineConverter; - } - if (config.adapter) { - adapter = config.adapter; - } } - function registerEvents() { + function _registerEvents() { eventBus.on(Events.WALLCLOCK_TIME_UPDATED, _onWallclockTimeUpdated, this); + eventBus.on(Events.INITIAL_STREAM_SWITCH, _onInitialStreamSwitch, this); eventBus.on(Events.PLAYBACK_SEEKING, _onPlaybackSeeking, this); - eventBus.on(Events.BYTES_APPENDED_END_FRAGMENT, onBytesAppended, instance); + eventBus.on(Events.BUFFER_REPLACEMENT_STARTED, _onBufferReplacementStarted, instance); + eventBus.on(Events.TRACK_CHANGE_RENDERED, _onBufferReplacementEnded, instance); } - function unregisterEvents() { + function _unregisterEvents() { eventBus.off(Events.WALLCLOCK_TIME_UPDATED, _onWallclockTimeUpdated, this); + eventBus.off(Events.INITIAL_STREAM_SWITCH, _onInitialStreamSwitch, this); eventBus.off(Events.PLAYBACK_SEEKING, _onPlaybackSeeking, this); - eventBus.off(Events.BYTES_APPENDED_END_FRAGMENT, onBytesAppended, instance); - } - - function onBytesAppended() { - if (!gapHandlerInterval) { - startGapHandler(); - } + eventBus.off(Events.BUFFER_REPLACEMENT_STARTED, _onBufferReplacementStarted, instance); + eventBus.off(Events.TRACK_CHANGE_RENDERED, _onBufferReplacementEnded, instance); } + /** + * Clear scheduled gap jump when seeking + * @private + */ function _onPlaybackSeeking() { if (jumpTimeoutHandler) { clearTimeout(jumpTimeoutHandler); @@ -127,8 +124,55 @@ function GapController() { } } + /** + * If the track was changed in the current active period and the player might aggressively replace segments the buffer will be empty for a short period of time. Avoid gap jumping at that time. + * We wait until the next media fragment of the target type has been appended before activating again + * @param {object} e + * @private + */ + function _onBufferReplacementStarted(e) { + try { + if (e.streamId !== streamController.getActiveStreamInfo().id || (e.mediaType !== Constants.VIDEO && e.mediaType !== Constants.AUDIO)) { + return; + } + + if (e.streamId === streamController.getActiveStreamInfo().id) { + trackSwitchByMediaType[e.mediaType] = true; + } + } catch (e) { + logger.error(e); + } + } + + /** + * Activate gap jumping again once segment of target type has been appended + * @param {object} e + * @private + */ + function _onBufferReplacementEnded(e) { + if (!e || !e.mediaType) { + return; + } + + trackSwitchByMediaType[e.mediaType] = false; + } + + /** + * Activate the gap handler after the first stream switch + * @private + */ + function _onInitialStreamSwitch() { + if (!gapHandlerInterval) { + _startGapHandler(); + } + } + + /** + * Callback handler for when the wallclock time has been updated + * @private + */ function _onWallclockTimeUpdated(/*e*/) { - if (!_shouldCheckForGaps()) { + if (!_shouldCheckForGaps(settings.get().streaming.gaps.enableSeekFix)) { return; } @@ -136,7 +180,7 @@ function GapController() { if (wallclockTicked >= THRESHOLD_TO_STALLS) { const currentTime = playbackController.getTime(); if (lastPlaybackTime === currentTime) { - jumpGap(currentTime, true); + _jumpGap(currentTime, true); } else { lastPlaybackTime = currentTime; lastGapJumpPosition = NaN; @@ -145,13 +189,45 @@ function GapController() { } } - function _shouldCheckForGaps() { - return settings.get().streaming.jumpGaps && streamController.getActiveStreamProcessors().length > 0 && - (!playbackController.isSeeking() || streamController.hasStreamFinishedBuffering(streamController.getActiveStream())) && !playbackController.isPaused() && !streamController.getIsStreamSwitchInProgress() && - !streamController.getHasMediaOrIntialisationError(); + /** + * Returns if we are supposed to check for gaps + * @param {boolean} checkSeekingState - Usually we are not checking for gaps in the videolement is in seeking state. If this flag is set to true we check for a potential exceptions of this rule. + * @return {boolean} + * @private + */ + function _shouldCheckForGaps(checkSeekingState = false) { + if (!streamController.getActiveStream()) { + return false; + } + const trackSwitchInProgress = Object.keys(trackSwitchByMediaType).some((key) => { + return trackSwitchByMediaType[key]; + }); + const shouldIgnoreSeekingState = checkSeekingState ? _shouldIgnoreSeekingState() : false; + + return !trackSwitchInProgress && settings.get().streaming.gaps.jumpGaps && streamController.getActiveStreamProcessors().length > 0 && (!playbackController.isSeeking() || shouldIgnoreSeekingState) && !playbackController.isPaused() && !streamController.getIsStreamSwitchInProgress() && + !streamController.getHasMediaOrInitialisationError(); + } + + /** + * There are cases in which we never transition out of the seeking state and still need to jump a gap. For instance if the user seeks right before a gap and video element will not transition out of the seeking state. + * For now limit this to period boundaries. In this case the current period is completely buffered and we are right before the end of the period. + * @private + */ + function _shouldIgnoreSeekingState() { + const activeStream = streamController.getActiveStream(); + const streamEnd = parseFloat((activeStream.getStartTime() + activeStream.getDuration()).toFixed(5)) + + return playbackController.getTime() + settings.get().streaming.gaps.threshold >= streamEnd; } - function getNextRangeIndex(ranges, currentTime) { + /** + * Returns the index of the range object that comes after the current time + * @param {object} ranges + * @param {number} currentTime + * @private + * @return {null|number} + */ + function _getNextRangeIndex(ranges, currentTime) { try { if (!ranges || (ranges.length <= 1 && currentTime > 0)) { @@ -162,7 +238,7 @@ function GapController() { while (isNaN(nextRangeIndex) && j < ranges.length) { const rangeEnd = j > 0 ? ranges.end(j - 1) : 0; - if (currentTime < ranges.start(j) && rangeEnd - currentTime < GAP_THRESHOLD) { + if (currentTime < ranges.start(j) && rangeEnd - currentTime < settings.get().streaming.gaps.threshold) { nextRangeIndex = j; } j += 1; @@ -174,8 +250,27 @@ function GapController() { } } + /** + * Check if the currentTime exist within the buffered range + * @param {object} ranges + * @param {number} currentTime + * @private + * @return {boolean} + */ + function _isTimeBuffered(ranges, currentTime) { + for(let i = 0, len = ranges.length; i < len; i++) { + if (currentTime >= ranges.start(i) && currentTime <= ranges.end(i)) { + return true; + } + } + return false; + } - function startGapHandler() { + /** + * Starts the interval that checks for gaps + * @private + */ + function _startGapHandler() { try { if (!gapHandlerInterval) { logger.debug('Starting the gap controller'); @@ -184,7 +279,7 @@ function GapController() { return; } const currentTime = playbackController.getTime(); - jumpGap(currentTime); + _jumpGap(currentTime); }, GAP_HANDLER_INTERVAL); } @@ -192,7 +287,11 @@ function GapController() { } } - function stopGapHandler() { + /** + * Clears the gap interval handler + * @private + */ + function _stopGapHandler() { logger.debug('Stopping the gap controller'); if (gapHandlerInterval) { clearInterval(gapHandlerInterval); @@ -200,9 +299,17 @@ function GapController() { } } - function jumpGap(currentTime, playbackStalled = false) { - const smallGapLimit = settings.get().streaming.smallGapLimit; - const jumpLargeGaps = settings.get().streaming.jumpLargeGaps; + /** + * Jump a gap + * @param {number} currentTime + * @param {boolean} playbackStalled + * @private + */ + function _jumpGap(currentTime, playbackStalled = false) { + const enableStallFix = settings.get().streaming.gaps.enableStallFix; + const stallSeek = settings.get().streaming.gaps.stallSeek; + const smallGapLimit = settings.get().streaming.gaps.smallGapLimit; + const jumpLargeGaps = settings.get().streaming.gaps.jumpLargeGaps; const ranges = videoModel.getBufferRange(); let nextRangeIndex; let seekToPosition = NaN; @@ -210,7 +317,7 @@ function GapController() { // Get the range just after current time position - nextRangeIndex = getNextRangeIndex(ranges, currentTime); + nextRangeIndex = _getNextRangeIndex(ranges, currentTime); if (!isNaN(nextRangeIndex)) { const start = ranges.start(nextRangeIndex); @@ -227,27 +334,34 @@ function GapController() { jumpToStreamEnd = true; } + if(enableStallFix && isNaN(seekToPosition) && playbackStalled && isNaN(nextRangeIndex) && _isTimeBuffered(ranges, currentTime)) { + if (stallSeek === 0) { + logger.warn(`Toggle play pause to break stall`); + videoModel.pause(); + videoModel.play(); + } else { + logger.warn(`Jumping ${stallSeek}s to break stall`); + seekToPosition = currentTime + stallSeek; + } + } + if (seekToPosition > 0 && lastGapJumpPosition !== seekToPosition && seekToPosition > currentTime && !jumpTimeoutHandler) { const timeUntilGapEnd = seekToPosition - currentTime; if (jumpToStreamEnd) { + const nextStream = streamController.getStreamForTime(seekToPosition); + const internalSeek = nextStream && !!nextStream.getPreloaded(); + logger.warn(`Jumping to end of stream because of gap from ${currentTime} to ${seekToPosition}. Gap duration: ${timeUntilGapEnd}`); - eventBus.trigger(Events.GAP_CAUSED_SEEK_TO_PERIOD_END, { - seekTime: seekToPosition, - duration: timeUntilGapEnd - }); + playbackController.seek(seekToPosition, true, internalSeek); } else { const isDynamic = playbackController.getIsDynamic(); const start = nextRangeIndex > 0 ? ranges.end(nextRangeIndex - 1) : currentTime; - const timeToWait = !isDynamic ? 0 : timeUntilGapEnd * 1000; + const timeToWait = !isDynamic ? 0 : Math.max(0, timeUntilGapEnd - GAP_JUMP_WAITING_TIME_OFFSET) * 1000; jumpTimeoutHandler = window.setTimeout(() => { playbackController.seek(seekToPosition, true, true); - logger.warn(`Jumping gap starting at ${start} and ending at ${seekToPosition}. Jumping by: ${timeUntilGapEnd}`); - eventBus.trigger(Events.GAP_CAUSED_INTERNAL_SEEK, { - seekTime: seekToPosition, - duration: timeUntilGapEnd - }); + logger.warn(`Jumping gap occuring in period ${streamController.getActiveStream().getStreamId()} starting at ${start} and ending at ${seekToPosition}. Jumping by: ${seekToPosition - start}`); jumpTimeoutHandler = null; }, timeToWait); } diff --git a/src/streaming/controllers/MediaController.js b/src/streaming/controllers/MediaController.js index c8fe4d7a07..c6ab38ebe2 100644 --- a/src/streaming/controllers/MediaController.js +++ b/src/streaming/controllers/MediaController.js @@ -33,6 +33,8 @@ import Events from '../../core/events/Events'; import EventBus from '../../core/EventBus'; import FactoryMaker from '../../core/FactoryMaker'; import Debug from '../../core/Debug'; +import bcp47Normalize from 'bcp-47-normalize'; +import {extendedFilter} from 'bcp-47-match'; function MediaController() { @@ -44,20 +46,10 @@ function MediaController() { tracks, settings, initialSettings, + lastSelectedTracks, + customParametersModel, domStorage; - const validTrackSwitchModes = [ - Constants.TRACK_SWITCH_MODE_ALWAYS_REPLACE, - Constants.TRACK_SWITCH_MODE_NEVER_REPLACE - ]; - - const validTrackSelectionModes = [ - Constants.TRACK_SELECTION_MODE_HIGHEST_BITRATE, - Constants.TRACK_SELECTION_MODE_FIRST_TRACK, - Constants.TRACK_SELECTION_MODE_HIGHEST_EFFICIENCY, - Constants.TRACK_SELECTION_MODE_WIDEST_RANGE - ]; - function setup() { logger = Debug(context).getInstance().getLogger(instance); reset(); @@ -68,9 +60,9 @@ function MediaController() { * @param {StreamInfo} streamInfo * @memberof MediaController# */ - function checkInitialMediaSettingsForType(type, streamInfo) { - let settings = getInitialSettings(type); - const tracksForType = getTracksFor(type, streamInfo); + function setInitialMediaSettingsForType(type, streamInfo) { + let settings = lastSelectedTracks[type] || getInitialSettings(type); + const tracksForType = getTracksFor(type, streamInfo.id); const tracks = []; if (!settings) { @@ -82,17 +74,17 @@ function MediaController() { if (settings) { tracksForType.forEach(function (track) { - if (matchSettings(settings, track)) { + if (matchSettings(settings, track, !!lastSelectedTracks[type])) { tracks.push(track); } }); } if (tracks.length === 0) { - setTrack(this.selectInitialTrack(type, tracksForType), true); + setTrack(selectInitialTrack(type, tracksForType), true); } else { if (tracks.length > 1) { - setTrack(this.selectInitialTrack(type, tracks)); + setTrack(selectInitialTrack(type, tracks, !!lastSelectedTracks[type])); } else { setTrack(tracks[0]); } @@ -107,7 +99,7 @@ function MediaController() { if (!track) return; const mediaType = track.type; - if (!isMultiTrackSupportedByType(mediaType)) return; + if (!_isMultiTrackSupportedByType(mediaType)) return; let streamId = track.streamInfo.id; if (!tracks[streamId]) { @@ -123,38 +115,31 @@ function MediaController() { } mediaTracks.push(track); - - let initSettings = getInitialSettings(mediaType); - if (initSettings && (matchSettings(initSettings, track)) && !getCurrentTrackFor(mediaType, track.streamInfo)) { - setTrack(track); - } } /** * @param {string} type - * @param {StreamInfo} streamInfo + * @param {string} streamId * @returns {Array} * @memberof MediaController# */ - function getTracksFor(type, streamInfo) { - if (!type || !streamInfo) return []; - - const id = streamInfo.id; + function getTracksFor(type, streamId) { + if (!type) return []; - if (!tracks[id] || !tracks[id][type]) return []; + if (!tracks[streamId] || !tracks[streamId][type]) return []; - return tracks[id][type].list; + return tracks[streamId][type].list; } /** * @param {string} type - * @param {StreamInfo} streamInfo + * @param {string} streamId * @returns {Object|null} * @memberof MediaController# */ - function getCurrentTrackFor(type, streamInfo) { - if (!type || !streamInfo || (streamInfo && !tracks[streamInfo.id])) return null; - return tracks[streamInfo.id][type].current; + function getCurrentTrackFor(type, streamId) { + if (!type || !tracks[streamId] || !tracks[streamId][type]) return null; + return tracks[streamId][type].current; } /** @@ -177,24 +162,24 @@ function MediaController() { * @param {boolean} noSettingsSave specify if settings must be not be saved * @memberof MediaController# */ - function setTrack(track, noSettingsSave) { + function setTrack(track, noSettingsSave = false) { if (!track || !track.streamInfo) return; const type = track.type; const streamInfo = track.streamInfo; const id = streamInfo.id; - const current = getCurrentTrackFor(type, streamInfo); + const current = getCurrentTrackFor(type, id); - if (!tracks[id] || !tracks[id][type] || isTracksEqual(track, current)) return; + if (!tracks[id] || !tracks[id][type]) return; tracks[id][type].current = track; - if (tracks[id][type].current && !(noSettingsSave && type === Constants.FRAGMENTED_TEXT)) { + if (tracks[id][type].current && ((type !== Constants.TEXT && !isTracksEqual(track, current)) || (type === Constants.TEXT && track.isFragmented))) { eventBus.trigger(Events.CURRENT_TRACK_CHANGED, { oldMediaInfo: current, newMediaInfo: track, - switchMode: getSwitchMode(type) - }); + switchMode: settings.get().streaming.trackSwitchMode[type] + }, { streamId: id }); } if (!noSettingsSave) { @@ -216,6 +201,7 @@ function MediaController() { settings.audioChannelConfiguration = settings.audioChannelConfiguration[0]; } + lastSelectedTracks[type] = settings; domStorage.setSavedMediaSettings(type, settings); } } @@ -246,70 +232,7 @@ function MediaController() { * @memberof MediaController# */ function saveTextSettingsDisabled() { - domStorage.setSavedMediaSettings(Constants.FRAGMENTED_TEXT, null); - } - - /** - * @param {string} type - * @param {string} mode - * @memberof MediaController# - * @deprecated Please use updateSettings({streaming: { trackSwitchMode: mode } }) instead - */ - function setSwitchMode(type, mode) { - logger.warn('deprecated: Please use updateSettings({streaming: { trackSwitchMode: mode } }) instead'); - const isModeSupported = (validTrackSwitchModes.indexOf(mode) !== -1); - - if (!isModeSupported) { - logger.warn('Track switch mode is not supported: ' + mode); - return; - } - - let switchMode = {}; - switchMode[type] = mode; - - settings.update({ - streaming: { - trackSwitchMode: switchMode - } - }); - } - - /** - * @param {string} type - * @returns {string} mode - * @memberof MediaController# - */ - function getSwitchMode(type) { - return settings.get().streaming.trackSwitchMode[type]; - } - - /** - * @param {string} mode - * @memberof MediaController# - * @deprecated Please use updateSettings({streaming: { selectionModeForInitialTrack: mode } }) instead - */ - function setSelectionModeForInitialTrack(mode) { - logger.warn('deprecated: Please use updateSettings({streaming: { selectionModeForInitialTrack: mode } }) instead'); - const isModeSupported = (validTrackSelectionModes.indexOf(mode) !== -1); - - if (!isModeSupported) { - logger.warn('Track selection mode is not supported: ' + mode); - return; - } - - settings.update({ - streaming: { - selectionModeForInitialTrack: mode - } - }); - } - - /** - * @returns {string} mode - * @memberof MediaController# - */ - function getSelectionModeForInitialTrack() { - return settings.get().streaming.selectionModeForInitialTrack; + domStorage.setSavedMediaSettings(Constants.TEXT, null); } /** @@ -317,9 +240,8 @@ function MediaController() { * @returns {boolean} * @memberof MediaController# */ - function isMultiTrackSupportedByType(type) { - return (type === Constants.AUDIO || type === Constants.VIDEO || type === Constants.TEXT || - type === Constants.FRAGMENTED_TEXT || type === Constants.IMAGE); + function _isMultiTrackSupportedByType(type) { + return (type === Constants.AUDIO || type === Constants.VIDEO || type === Constants.TEXT || type === Constants.IMAGE); } /** @@ -340,11 +262,12 @@ function MediaController() { const sameId = t1.id === t2.id; const sameViewpoint = t1.viewpoint === t2.viewpoint; const sameLang = t1.lang === t2.lang; + const sameCodec = t1.codec === t2.codec; const sameRoles = t1.roles.toString() === t2.roles.toString(); const sameAccessibility = t1.accessibility.toString() === t2.accessibility.toString(); const sameAudioChannelConfiguration = t1.audioChannelConfiguration.toString() === t2.audioChannelConfiguration.toString(); - return (sameId && sameViewpoint && sameLang && sameRoles && sameAccessibility && sameAudioChannelConfiguration); + return (sameId && sameCodec && sameViewpoint && sameLang && sameRoles && sameAccessibility && sameAudioChannelConfiguration); } function setConfig(config) { @@ -357,13 +280,19 @@ function MediaController() { if (config.settings) { settings = config.settings; } + + if (config.customParametersModel) { + customParametersModel = config.customParametersModel; + } } + /** * @memberof MediaController# */ function reset() { tracks = {}; + lastSelectedTracks = {}; resetInitialSettings(); } @@ -376,43 +305,92 @@ function MediaController() { audioChannelConfiguration: mediaInfo.audioChannelConfiguration }; let notEmpty = settings.lang || settings.viewpoint || (settings.role && settings.role.length > 0) || - (settings.accessibility && settings.accessibility.length > 0) || (settings.audioChannelConfiguration && settings.audioChannelConfiguration.length > 0); + (settings.accessibility && settings.accessibility.length > 0) || (settings.audioChannelConfiguration && settings.audioChannelConfiguration.length > 0); return notEmpty ? settings : null; } - function matchSettings(settings, track) { - const matchLang = !settings.lang || (track.lang.match(settings.lang)); - const matchIndex = (settings.index === undefined) || (settings.index === null) || (track.index === settings.index); - const matchViewPoint = !settings.viewpoint || (settings.viewpoint === track.viewpoint); - const matchRole = !settings.role || !!track.roles.filter(function (item) { - return item === settings.role; - })[0]; - let matchAccessibility = !settings.accessibility || !!track.accessibility.filter(function (item) { - return item === settings.accessibility; - })[0]; - let matchAudioChannelConfiguration = !settings.audioChannelConfiguration || !!track.audioChannelConfiguration.filter(function (item) { - return item === settings.audioChannelConfiguration; - })[0]; - - return (matchLang && matchIndex && matchViewPoint && matchRole && matchAccessibility && matchAudioChannelConfiguration); + function matchSettings(settings, track, isTrackActive = false) { + try { + let matchLang = false; + + // If there is no language defined in the target settings we got a match + if (!settings.lang) { + matchLang = true; + } + + // If the target language is provided as a RegExp apply match function + else if (settings.lang instanceof RegExp) { + matchLang = track.lang.match(settings.lang); + } + + // If the track has a language and we can normalize the target language check if we got a match + else if (track.lang !== '') { + const normalizedSettingsLang = bcp47Normalize(settings.lang); + if (normalizedSettingsLang) { + matchLang = extendedFilter(track.lang, normalizedSettingsLang).length > 0 + } + } + + const matchIndex = (settings.index === undefined) || (settings.index === null) || (track.index === settings.index); + const matchViewPoint = !settings.viewpoint || (settings.viewpoint === track.viewpoint); + const matchRole = !settings.role || !!track.roles.filter(function (item) { + return item === settings.role; + })[0]; + let matchAccessibility = !settings.accessibility || !!track.accessibility.filter(function (item) { + return item === settings.accessibility; + })[0]; + let matchAudioChannelConfiguration = !settings.audioChannelConfiguration || !!track.audioChannelConfiguration.filter(function (item) { + return item === settings.audioChannelConfiguration; + })[0]; + + + return (matchLang && matchIndex && matchViewPoint && (matchRole || (track.type === Constants.AUDIO && isTrackActive)) && matchAccessibility && matchAudioChannelConfiguration); + } catch (e) { + return false; + logger.error(e); + } } function resetInitialSettings() { initialSettings = { audio: null, video: null, - fragmentedText: null + text: null }; } - function getTracksWithHighestBitrate (trackArr) { + function getTracksWithHighestSelectionPriority(trackArr) { + let max = 0; + let result = []; + + trackArr.forEach((track) => { + if (!isNaN(track.selectionPriority)) { + // Higher max value. Reset list and add new entry + if (track.selectionPriority > max) { + max = track.selectionPriority; + result = [track]; + } + // Same max value add to list + else if (track.selectionPriority === max) { + result.push(track); + } + + } + }) + + return result; + } + + function getTracksWithHighestBitrate(trackArr) { let max = 0; let result = []; let tmp; trackArr.forEach(function (track) { - tmp = Math.max.apply(Math, track.bitrateList.map(function (obj) { return obj.bandwidth; })); + tmp = Math.max.apply(Math, track.bitrateList.map(function (obj) { + return obj.bandwidth; + })); if (tmp > max) { max = tmp; @@ -425,7 +403,7 @@ function MediaController() { return result; } - function getTracksWithHighestEfficiency (trackArr) { + function getTracksWithHighestEfficiency(trackArr) { let min = Infinity; let result = []; let tmp; @@ -449,7 +427,7 @@ function MediaController() { return result; } - function getTracksWithWidestRange (trackArr) { + function getTracksWithWidestRange(trackArr) { let max = 0; let result = []; let tmp; @@ -469,44 +447,91 @@ function MediaController() { } function selectInitialTrack(type, tracks) { - if (type === Constants.FRAGMENTED_TEXT) return tracks[0]; + if (type === Constants.TEXT) return tracks[0]; + + let mode = settings.get().streaming.selectionModeForInitialTrack; + let tmpArr; + const customInitialTrackSelectionFunction = customParametersModel.getCustomInitialTrackSelectionFunction(); + + if (customInitialTrackSelectionFunction && typeof customInitialTrackSelectionFunction === 'function') { + tmpArr = customInitialTrackSelectionFunction(tracks); + } else { + switch (mode) { + case Constants.TRACK_SELECTION_MODE_HIGHEST_SELECTION_PRIORITY: + tmpArr = _trackSelectionModeHighestSelectionPriority(tracks); + break; + case Constants.TRACK_SELECTION_MODE_HIGHEST_BITRATE: + tmpArr = _trackSelectionModeHighestBitrate(tracks); + break; + case Constants.TRACK_SELECTION_MODE_FIRST_TRACK: + tmpArr = _trackSelectionModeFirstTrack(tracks); + break; + case Constants.TRACK_SELECTION_MODE_HIGHEST_EFFICIENCY: + tmpArr = _trackSelectionModeHighestEfficiency(tracks); + break; + case Constants.TRACK_SELECTION_MODE_WIDEST_RANGE: + tmpArr = _trackSelectionModeWidestRange(tracks); + break; + default: + logger.warn(`Track selection mode ${mode} is not supported. Falling back to TRACK_SELECTION_MODE_FIRST_TRACK`); + tmpArr = _trackSelectionModeFirstTrack(tracks); + break; + } + } - let mode = getSelectionModeForInitialTrack(); - let tmpArr = []; + return tmpArr.length > 0 ? tmpArr[0] : tracks[0]; + } - switch (mode) { - case Constants.TRACK_SELECTION_MODE_HIGHEST_BITRATE: - tmpArr = getTracksWithHighestBitrate(tracks); - if (tmpArr.length > 1) { - tmpArr = getTracksWithWidestRange(tmpArr); - } - break; - case Constants.TRACK_SELECTION_MODE_FIRST_TRACK: - tmpArr.push(tracks[0]); - break; - case Constants.TRACK_SELECTION_MODE_HIGHEST_EFFICIENCY: - tmpArr = getTracksWithHighestEfficiency(tracks); - - if (tmpArr.length > 1) { - tmpArr = getTracksWithHighestBitrate(tmpArr); - } - break; - case Constants.TRACK_SELECTION_MODE_WIDEST_RANGE: - tmpArr = getTracksWithWidestRange(tracks); + function _trackSelectionModeHighestSelectionPriority(tracks) { + let tmpArr = getTracksWithHighestSelectionPriority(tracks); - if (tmpArr.length > 1) { - tmpArr = getTracksWithHighestBitrate(tracks); - } - break; - default: - logger.warn('Track selection mode is not supported: ' + mode); - break; + if (tmpArr.length > 1) { + tmpArr = getTracksWithHighestBitrate(tmpArr); } - return tmpArr[0]; + if (tmpArr.length > 1) { + tmpArr = getTracksWithWidestRange(tmpArr); + } + + return tmpArr; + } + + function _trackSelectionModeHighestBitrate(tracks) { + let tmpArr = getTracksWithHighestBitrate(tracks); + + if (tmpArr.length > 1) { + tmpArr = getTracksWithWidestRange(tmpArr); + } + + return tmpArr; + } + + function _trackSelectionModeFirstTrack(tracks) { + return tracks[0]; + } + + function _trackSelectionModeHighestEfficiency(tracks) { + let tmpArr = getTracksWithHighestEfficiency(tracks); + + if (tmpArr.length > 1) { + tmpArr = getTracksWithHighestBitrate(tmpArr); + } + + return tmpArr; + } + + function _trackSelectionModeWidestRange(tracks) { + let tmpArr = getTracksWithWidestRange(tracks); + + if (tmpArr.length > 1) { + tmpArr = getTracksWithHighestBitrate(tracks); + } + + return tmpArr; } + function createTrackInfo() { return { audio: { @@ -524,11 +549,6 @@ function MediaController() { storeLastSettings: true, current: null }, - fragmentedText: { - list: [], - storeLastSettings: true, - current: null - }, image: { list: [], storeLastSettings: true, @@ -538,28 +558,23 @@ function MediaController() { } instance = { - checkInitialMediaSettingsForType: checkInitialMediaSettingsForType, - addTrack: addTrack, - getTracksFor: getTracksFor, - getCurrentTrackFor: getCurrentTrackFor, - isCurrentTrack: isCurrentTrack, - setTrack: setTrack, - setInitialSettings: setInitialSettings, - getInitialSettings: getInitialSettings, - setSwitchMode: setSwitchMode, - getSwitchMode: getSwitchMode, - selectInitialTrack: selectInitialTrack, - getTracksWithHighestBitrate: getTracksWithHighestBitrate, - getTracksWithHighestEfficiency: getTracksWithHighestEfficiency, - getTracksWithWidestRange: getTracksWithWidestRange, - setSelectionModeForInitialTrack: setSelectionModeForInitialTrack, - getSelectionModeForInitialTrack: getSelectionModeForInitialTrack, - isMultiTrackSupportedByType: isMultiTrackSupportedByType, - isTracksEqual: isTracksEqual, - matchSettings: matchSettings, - saveTextSettingsDisabled: saveTextSettingsDisabled, - setConfig: setConfig, - reset: reset + setInitialMediaSettingsForType, + addTrack, + getTracksFor, + getCurrentTrackFor, + isCurrentTrack, + setTrack, + selectInitialTrack, + setInitialSettings, + getInitialSettings, + getTracksWithHighestBitrate, + getTracksWithHighestEfficiency, + getTracksWithWidestRange, + isTracksEqual, + matchSettings, + saveTextSettingsDisabled, + setConfig, + reset }; setup(); diff --git a/src/streaming/controllers/MediaSourceController.js b/src/streaming/controllers/MediaSourceController.js index d86de0643f..6cce7efa99 100644 --- a/src/streaming/controllers/MediaSourceController.js +++ b/src/streaming/controllers/MediaSourceController.js @@ -34,6 +34,7 @@ import Debug from '../../core/Debug'; function MediaSourceController() { let instance, + mediaSource, logger; const context = this.context; @@ -48,17 +49,17 @@ function MediaSourceController() { let hasMediaSource = ('MediaSource' in window); if (hasMediaSource) { - return new MediaSource(); + mediaSource = new MediaSource(); } else if (hasWebKit) { - return new WebKitMediaSource(); + mediaSource = new WebKitMediaSource(); } - return null; + return mediaSource; } - function attachMediaSource(source, videoModel) { + function attachMediaSource(videoModel) { - let objectURL = window.URL.createObjectURL(source); + let objectURL = window.URL.createObjectURL(mediaSource); videoModel.setSource(objectURL); @@ -69,27 +70,55 @@ function MediaSourceController() { videoModel.setSource(null); } - function setDuration(source, value) { - if (!source || source.readyState !== 'open') return; + function setDuration(value, log = true) { + if (!mediaSource || mediaSource.readyState !== 'open') return; if (value === null && isNaN(value)) return; - if (source.duration === value) return; + if (mediaSource.duration === value) return; - if (!isBufferUpdating(source)) { - logger.info('Set MediaSource duration:' + value); - source.duration = value; + if (!isBufferUpdating(mediaSource)) { + if (log) { + logger.info('Set MediaSource duration:' + value); + } + mediaSource.duration = value; } else { - setTimeout(setDuration.bind(null, source, value), 50); + setTimeout(setDuration.bind(null, value), 50); } } - function setSeekable(source, start, end) { - if (source && typeof source.setLiveSeekableRange === 'function' && typeof source.clearLiveSeekableRange === 'function' && - source.readyState === 'open' && start >= 0 && start < end) { - source.clearLiveSeekableRange(); - source.setLiveSeekableRange(start, end); + function setSeekable(start, end, enableLiveSeekableRangeFix) { + if (!mediaSource || mediaSource.readyState !== 'open') return; + if (start < 0 || end <= start) return; + + if (typeof mediaSource.setLiveSeekableRange === 'function' && typeof mediaSource.clearLiveSeekableRange === 'function') { + mediaSource.clearLiveSeekableRange(); + mediaSource.setLiveSeekableRange(start, end); + } else if (enableLiveSeekableRangeFix) { + try { + const bufferedRangeEnd = getBufferedRangeEnd(mediaSource); + const targetMediaSourceDuration = Math.max(end, bufferedRangeEnd); + if (!isFinite(mediaSource.duration) || mediaSource.duration < targetMediaSourceDuration) { + setDuration(targetMediaSourceDuration, false); + } + } catch (e) { + logger.error(`Failed to set MediaSource duration! ` + e.toString()); + } } } + function getBufferedRangeEnd(source) { + let max = 0; + const buffers = source.sourceBuffers; + + for (let i = 0; i < buffers.length; i++) { + if (buffers[i].buffered.length > 0) { + const end = buffers[i].buffered.end(buffers[i].buffered.length - 1); + max = Math.max(end, max); + } + } + + return max; + } + function signalEndOfStream(source) { if (!source || source.readyState !== 'open') { return; @@ -120,12 +149,12 @@ function MediaSourceController() { } instance = { - createMediaSource: createMediaSource, - attachMediaSource: attachMediaSource, - detachMediaSource: detachMediaSource, - setDuration: setDuration, - setSeekable: setSeekable, - signalEndOfStream: signalEndOfStream + createMediaSource, + attachMediaSource, + detachMediaSource, + setDuration, + setSeekable, + signalEndOfStream }; setup(); diff --git a/src/streaming/controllers/PlaybackController.js b/src/streaming/controllers/PlaybackController.js index 3a501056d0..21abbbc03d 100644 --- a/src/streaming/controllers/PlaybackController.js +++ b/src/streaming/controllers/PlaybackController.js @@ -29,11 +29,12 @@ * POSSIBILITY OF SUCH DAMAGE. */ import Constants from '../constants/Constants'; -import MetricsConstants from '../constants/MetricsConstants'; import EventBus from '../../core/EventBus'; import Events from '../../core/events/Events'; import FactoryMaker from '../../core/FactoryMaker'; import Debug from '../../core/Debug'; +import MediaPlayerEvents from '../../streaming/MediaPlayerEvents'; +import MetricsConstants from '../constants/MetricsConstants'; const LIVE_UPDATE_PLAYBACK_TIME_INTERVAL_MS = 500; @@ -45,26 +46,26 @@ function PlaybackController() { let instance, logger, streamController, + serviceDescriptionController, dashMetrics, adapter, videoModel, timelineConverter, - streamSwitch, - streamSeekTime, wallclockTimeIntervalId, liveDelay, + originalLiveDelay, streamInfo, isDynamic, - mediaPlayerModel, playOnceInitialized, lastLivePlaybackTime, availabilityStartTime, + availabilityTimeComplete, + lowLatencyModeEnabled, seekTarget, internalSeek, - isLowLatencySeekingInProgress, playbackStalled, - minPlaybackRateChange, - uriFragmentModel, + manifestUpdateInProgress, + initialCatchupModeActivated, settings; function setup() { @@ -73,30 +74,72 @@ function PlaybackController() { reset(); } - function initialize(sInfo, periodSwitch, seekTime) { + /** + * Reset all settings + */ + function reset() { + pause(); + playOnceInitialized = false; + liveDelay = 0; + originalLiveDelay = 0; + availabilityStartTime = 0; + manifestUpdateInProgress = false; + availabilityTimeComplete = true; + lowLatencyModeEnabled = false; + initialCatchupModeActivated = false; + seekTarget = NaN; + + if (videoModel) { + eventBus.off(Events.DATA_UPDATE_COMPLETED, _onDataUpdateCompleted, instance); + eventBus.off(Events.LOADING_PROGRESS, _onFragmentLoadProgress, instance); + eventBus.off(Events.MANIFEST_UPDATED, _onManifestUpdated, instance); + eventBus.off(Events.STREAMS_COMPOSED, _onStreamsComposed, instance); + eventBus.off(MediaPlayerEvents.PLAYBACK_ENDED, _onPlaybackEnded, instance); + eventBus.off(MediaPlayerEvents.STREAM_INITIALIZING, _onStreamInitializing, instance); + eventBus.off(MediaPlayerEvents.REPRESENTATION_SWITCH, _onRepresentationSwitch, instance); + eventBus.off(MediaPlayerEvents.BUFFER_LEVEL_STATE_CHANGED, _onBufferLevelStateChanged, instance); + stopUpdatingWallclockTime(); + removeAllListeners(); + } + + wallclockTimeIntervalId = null; + videoModel = null; + streamInfo = null; + isDynamic = null; + } + + /** + * Initializes the PlaybackController. This function is called whenever the stream is switched. + * @param {object} sInfo + * @param {boolean} periodSwitch + */ + function initialize(sInfo, periodSwitch) { streamInfo = sInfo; + + if (periodSwitch !== true) { + _initializeForFirstStream(); + } + } + + /** + * Initializes the PlaybackController when the first stream is to be played. + * @private + */ + function _initializeForFirstStream() { addAllListeners(); isDynamic = streamInfo.manifestInfo.isDynamic; - isLowLatencySeekingInProgress = false; + playbackStalled = false; - streamSwitch = periodSwitch === true; - streamSeekTime = seekTime; internalSeek = false; - const ua = typeof navigator !== 'undefined' ? navigator.userAgent.toLowerCase() : ''; - - // Detect safari browser (special behavior for low latency streams) - const isSafari = /safari/.test(ua) && !/chrome/.test(ua); - minPlaybackRateChange = isSafari ? 0.25 : 0.02; - - eventBus.on(Events.STREAM_INITIALIZED, onStreamInitialized, this); - eventBus.on(Events.DATA_UPDATE_COMPLETED, onDataUpdateCompleted, this); - eventBus.on(Events.LOADING_PROGRESS, onFragmentLoadProgress, this); - eventBus.on(Events.BUFFER_LEVEL_STATE_CHANGED, onBufferLevelStateChanged, this); - eventBus.on(Events.PLAYBACK_PROGRESS, onPlaybackProgression, this); - eventBus.on(Events.PLAYBACK_TIME_UPDATED, onPlaybackProgression, this); - eventBus.on(Events.PLAYBACK_ENDED, onPlaybackEnded, this, { priority: EventBus.EVENT_PRIORITY_HIGH }); - eventBus.on(Events.STREAM_INITIALIZING, onStreamInitializing, this); + eventBus.on(Events.DATA_UPDATE_COMPLETED, _onDataUpdateCompleted, instance); + eventBus.on(Events.LOADING_PROGRESS, _onFragmentLoadProgress, instance); + eventBus.on(Events.MANIFEST_UPDATED, _onManifestUpdated, instance); + eventBus.on(Events.STREAMS_COMPOSED, _onStreamsComposed, instance); + eventBus.on(MediaPlayerEvents.PLAYBACK_ENDED, _onPlaybackEnded, instance, { priority: EventBus.EVENT_PRIORITY_HIGH }); + eventBus.on(MediaPlayerEvents.STREAM_INITIALIZING, _onStreamInitializing, instance); + eventBus.on(MediaPlayerEvents.REPRESENTATION_SWITCH, _onRepresentationSwitch, instance); + eventBus.on(MediaPlayerEvents.BUFFER_LEVEL_STATE_CHANGED, _onBufferLevelStateChanged, instance); if (playOnceInitialized) { playOnceInitialized = false; @@ -104,81 +147,64 @@ function PlaybackController() { } } - function onStreamInitialized(e) { - // Seamless period switch - if (streamSwitch && isNaN(streamSeekTime)) return; - - // Seek new stream in priority order: - // - at seek time (streamSeekTime) when switching period - // - at start time provided in URI parameters - // - at stream/period start time (for static streams) or live start time (for dynamic streams) - let startTime = streamSeekTime; - if (isNaN(startTime)) { - if (isDynamic) { - // For dynamic stream, start by default at (live edge - live delay) - startTime = e.liveStartTime; - // If start time in URI, take min value between live edge time and time from URI (capped by DVR window range) - const dvrInfo = dashMetrics.getCurrentDVRInfo(); - const dvrWindow = dvrInfo ? dvrInfo.range : null; - if (dvrWindow) { - // #t shall be relative to period start - const startTimeFromUri = getStartTimeFromUriParameters(true); - if (!isNaN(startTimeFromUri)) { - logger.info('Start time from URI parameters: ' + startTimeFromUri); - startTime = Math.max(Math.min(startTime, startTimeFromUri), dvrWindow.start); - } - } - } else { - // For static stream, start by default at period start - startTime = streamInfo.start; - // If start time in URI, take max value between period start and time from URI (if in period range) - const startTimeFromUri = getStartTimeFromUriParameters(false); - if (!isNaN(startTimeFromUri) && startTimeFromUri < (startTime + streamInfo.duration)) { - logger.info('Start time from URI parameters: ' + startTimeFromUri); - startTime = Math.max(startTime, startTimeFromUri); - } - } - } - - if (!isNaN(startTime) && startTime !== videoModel.getTime()) { - // Trigger PLAYBACK_SEEKING event for controllers - eventBus.trigger(Events.PLAYBACK_SEEKING, { seekTime: startTime }); - // Seek video model - seek(startTime, false, true); - } + /** + * Returns stalled state + * @return {boolean} + */ + function getPlaybackStalled() { + return playbackStalled } - function getTimeToStreamEnd() { - return parseFloat((getStreamEndTime() - getTime()).toFixed(5)); + /** + * Returns remaining duration of a period + * @param {object} sInfo + * @return {number} + */ + function getTimeToStreamEnd(sInfo = null) { + return parseFloat((getStreamEndTime(sInfo) - getTime()).toFixed(5)); } - function getStreamEndTime() { - return streamInfo.start + streamInfo.duration; + /** + * Returns end time of a period + * @param {object} sInfo + * @return {number} + */ + function getStreamEndTime(sInfo) { + const refInfo = sInfo ? sInfo : streamInfo; + return refInfo.start + refInfo.duration; } - function play() { + /** + * Triggers play() on the video element + */ + function play(adjustLiveDelay = false) { if (streamInfo && videoModel && videoModel.getElement()) { + if (adjustLiveDelay && isDynamic) { + _adjustLiveDelayAfterUserInteraction(getTime()); + } videoModel.play(); } else { playOnceInitialized = true; } } - function isPaused() { - return streamInfo && videoModel ? videoModel.isPaused() : null; - } - + /** + * Triggers pause() on the video element + */ function pause() { if (streamInfo && videoModel) { videoModel.pause(); } } - function isSeeking() { - return streamInfo && videoModel ? videoModel.isSeeking() : null; - } - - function seek(time, stickToBuffered, internal) { + /** + * Triggers a seek to the specified media time. If internal is enabled there will be now "seeked" event dispatched + * @param {number} time + * @param {boolean} stickToBuffered + * @param {boolean} internal + * @param {boolean} adjustLiveDelay + */ + function seek(time, stickToBuffered = false, internal = false, adjustLiveDelay = false) { if (!streamInfo || !videoModel) return; let currentTime = !isNaN(seekTarget) ? seekTarget : videoModel.getTime(); @@ -188,156 +214,256 @@ function PlaybackController() { if (!internalSeek) { seekTarget = time; - eventBus.trigger(Events.PLAYBACK_SEEK_ASKED); } logger.info('Requesting seek to time: ' + time + (internalSeek ? ' (internal)' : '')); + + // We adjust the current latency. If catchup is enabled we will maintain this new latency + if (isDynamic && adjustLiveDelay) { + _adjustLiveDelayAfterUserInteraction(time); + } + videoModel.setCurrentTime(time, stickToBuffered); } - function seekToLive() { - const DVRMetrics = dashMetrics.getCurrentDVRInfo(); - const DVRWindow = DVRMetrics ? DVRMetrics.range : null; + /** + * Seeks back to the live edge as defined by the originally calculated live delay + * @param {boolean} stickToBuffered + * @param {boolean} internal + * @param {boolean} adjustLiveDelay + */ + function seekToOriginalLive(stickToBuffered = false, internal = false, adjustLiveDelay = false) { + const dvrWindowEnd = _getDvrWindowEnd(); + + if (dvrWindowEnd === 0) { + return; + } + + liveDelay = originalLiveDelay; + const seektime = dvrWindowEnd - liveDelay; + + seek(seektime, stickToBuffered, internal, adjustLiveDelay); + } + + /** + * Seeks to the live edge as currently defined by liveDelay + * @param {boolean} stickToBuffered + * @param {boolean} internal + * @param {boolean} adjustLiveDelay + */ + function seekToCurrentLive(stickToBuffered = false, internal = false, adjustLiveDelay = false) { + const dvrWindowEnd = _getDvrWindowEnd(); - seek(DVRWindow.end - mediaPlayerModel.getLiveDelay(), true, false); + if (dvrWindowEnd === 0) { + return; + } + + const seektime = dvrWindowEnd - liveDelay; + + seek(seektime, stickToBuffered, internal, adjustLiveDelay); } + function _getDvrWindowEnd() { + if (!streamInfo || !videoModel || !isDynamic) { + return; + } + + const type = streamController && streamController.hasVideoTrack() ? Constants.VIDEO : Constants.AUDIO; + const dvrInfo = dashMetrics.getCurrentDVRInfo(type); + + return dvrInfo && dvrInfo.range ? dvrInfo.range.end : 0; + } + + + function _adjustLiveDelayAfterUserInteraction(time) { + const now = new Date(timelineConverter.getClientReferenceTime()); + const period = adapter.getRegularPeriods()[0]; + const nowAsPresentationTime = timelineConverter.calcPresentationTimeFromWallTime(now, period); + + liveDelay = nowAsPresentationTime - time; + } + + /** + * Returns current time of video element + * @return {number|null} + */ function getTime() { return streamInfo && videoModel ? videoModel.getTime() : null; } - function getNormalizedTime() { - let t = getTime(); + /** + * Returns paused state of the video element + * @return {boolean|null} + */ + function isPaused() { + return streamInfo && videoModel ? videoModel.isPaused() : null; + } - if (isDynamic && !isNaN(availabilityStartTime)) { - const timeOffset = availabilityStartTime / 1000; - // Fix current time for firefox and safari (returned as an absolute time) - if (t > timeOffset) { - t -= timeOffset; - } - } - return t; + /** + * Returns seeking state of the video element + * @return {boolean|null} + */ + function isSeeking() { + return streamInfo && videoModel ? videoModel.isSeeking() : null; } + /** + * Returns stalled state of the video element + * @return {boolean|null} + */ + function isStalled() { + return streamInfo && videoModel ? videoModel.isStalled() : null; + } + + /** + * Returns current playback rate of the video element + * @return {number|null} + */ function getPlaybackRate() { return streamInfo && videoModel ? videoModel.getPlaybackRate() : null; } + /** + * Returns the played ranges of the video element + * @return {array} + */ function getPlayedRanges() { return streamInfo && videoModel ? videoModel.getPlayedRanges() : null; } + /** + * Returns ended attribute of the video element + * @return {boolean|null} + */ function getEnded() { return streamInfo && videoModel ? videoModel.getEnded() : null; } + /** + * Returns whether a stream is type dynamic or not + * @return {boolean} + */ function getIsDynamic() { return isDynamic; } + /** + * Returns the StreamController + * @return {object} + */ function getStreamController() { return streamController; } + /** + * Returns whether a manifest update is in progress + * @return {boolean} + */ + function getIsManifestUpdateInProgress() { + return manifestUpdateInProgress; + } + + /** + * Returns the availabilityStartTime + * @return {number} + */ + function getAvailabilityStartTime() { + return availabilityStartTime; + } + + /** + * Returns the current live delay. A seek triggered by the user adjusts this value. + * @return {number} + */ + function getLiveDelay() { + return liveDelay; + } + + /** + * Returns the original live delay as calculated at playback start + */ + function getOriginalLiveDelay() { + return originalLiveDelay; + } + + /** + * Returns the current live latency + * @return {number} + */ + function getCurrentLiveLatency() { + if (!isDynamic || isNaN(availabilityStartTime)) { + return NaN; + } + let currentTime = getTime(); + if (isNaN(currentTime) || currentTime === 0) { + return 0; + } + + const now = new Date().getTime() + timelineConverter.getClientTimeOffset() * 1000; + return Math.max(((now - availabilityStartTime - currentTime * 1000) / 1000).toFixed(3), 0); + } + /** * Computes the desirable delay for the live edge to avoid a risk of getting 404 when playing at the bleeding edge * @param {number} fragmentDuration - seconds? - * @param {number} dvrWindowSize - seconds? - * @param {number} minBufferTime - seconds? + * @param {object} manifestInfo * @returns {number} object * @memberof PlaybackController# */ - function computeAndSetLiveDelay(fragmentDuration, dvrWindowSize, minBufferTime) { + function computeAndSetLiveDelay(fragmentDuration, manifestInfo) { let delay, ret, startTime; - const END_OF_PLAYLIST_PADDING = 10; const MIN_BUFFER_TIME_FACTOR = 4; const FRAGMENT_DURATION_FACTOR = 4; const adjustedFragmentDuration = !isNaN(fragmentDuration) && isFinite(fragmentDuration) ? fragmentDuration : NaN; let suggestedPresentationDelay = adapter.getSuggestedPresentationDelay(); + const serviceDescriptionSettings = serviceDescriptionController.getServiceDescriptionSettings(); + + // Live delay specified by the user + if (!isNaN(settings.get().streaming.delay.liveDelay)) { + delay = settings.get().streaming.delay.liveDelay; + } - if (settings.get().streaming.lowLatencyEnabled) { - delay = 0; - } else if (mediaPlayerModel.getLiveDelay()) { - delay = mediaPlayerModel.getLiveDelay(); // If set by user, this value takes precedence - } else if (settings.get().streaming.liveDelayFragmentCount !== null && !isNaN(settings.get().streaming.liveDelayFragmentCount) && !isNaN(adjustedFragmentDuration)) { - delay = adjustedFragmentDuration * settings.get().streaming.liveDelayFragmentCount; - } else if (settings.get().streaming.useSuggestedPresentationDelay === true && suggestedPresentationDelay !== null && !isNaN(suggestedPresentationDelay) && suggestedPresentationDelay > 0) { + // Live delay fragment count specified by the user + else if (settings.get().streaming.delay.liveDelayFragmentCount !== null && !isNaN(settings.get().streaming.delay.liveDelayFragmentCount) && !isNaN(adjustedFragmentDuration)) { + delay = adjustedFragmentDuration * settings.get().streaming.delay.liveDelayFragmentCount; + } + + // Live delay set via ServiceDescription element + else if (serviceDescriptionSettings && !isNaN(serviceDescriptionSettings.liveDelay) && serviceDescriptionSettings.liveDelay > 0) { + delay = serviceDescriptionSettings.liveDelay; + } + // Live delay set in the manifest using @suggestedPresentation Delay + else if (settings.get().streaming.delay.useSuggestedPresentationDelay === true && suggestedPresentationDelay !== null && !isNaN(suggestedPresentationDelay) && suggestedPresentationDelay > 0) { delay = suggestedPresentationDelay; - } else if (!isNaN(adjustedFragmentDuration)) { + } + + // We found a fragment duration, use that to calculcate live delay + else if (!isNaN(adjustedFragmentDuration)) { delay = adjustedFragmentDuration * FRAGMENT_DURATION_FACTOR; - } else { - delay = !isNaN(minBufferTime) ? minBufferTime * MIN_BUFFER_TIME_FACTOR : streamInfo.manifestInfo.minBufferTime * MIN_BUFFER_TIME_FACTOR; } - startTime = adapter.getAvailabilityStartTime(); + // Fall back to @minBufferTime to calculate the live delay + else { + delay = manifestInfo && !isNaN(manifestInfo.minBufferTime) ? manifestInfo.minBufferTime * MIN_BUFFER_TIME_FACTOR : streamInfo.manifestInfo.minBufferTime * MIN_BUFFER_TIME_FACTOR; + } + startTime = adapter.getAvailabilityStartTime(); if (startTime !== null) { availabilityStartTime = startTime; } - if (dvrWindowSize > 0) { - // cap target latency to: - // - dvrWindowSize / 2 for short playlists - // - dvrWindowSize - END_OF_PLAYLIST_PADDING for longer playlists - const targetDelayCapping = Math.max(dvrWindowSize - END_OF_PLAYLIST_PADDING, dvrWindowSize / 2); - ret = Math.min(delay, targetDelayCapping); + if (manifestInfo && manifestInfo.dvrWindowSize > 0) { + // Latency can not be higher than DVR window size + ret = Math.min(delay, manifestInfo.dvrWindowSize); } else { ret = delay; } liveDelay = ret; - return ret; - } - - function getLiveDelay() { - return liveDelay; - } - - function setLiveDelay(value, useMaxValue = false) { - if (useMaxValue && value < liveDelay) { - return; - } - - liveDelay = value; - } - - function getCurrentLiveLatency() { - if (!isDynamic || isNaN(availabilityStartTime)) { - return NaN; - } - let currentTime = getNormalizedTime(); - if (isNaN(currentTime) || currentTime === 0) { - return 0; - } + originalLiveDelay = ret; - const now = new Date().getTime() + timelineConverter.getClientTimeOffset() * 1000; - return Math.max(((now - availabilityStartTime - currentTime * 1000) / 1000).toFixed(3), 0); - } - - function reset() { - playOnceInitialized = false; - streamSwitch = false; - streamSeekTime = NaN; - liveDelay = 0; - availabilityStartTime = 0; - seekTarget = NaN; - if (videoModel) { - eventBus.off(Events.STREAM_INITIALIZED, onStreamInitialized, this); - eventBus.off(Events.DATA_UPDATE_COMPLETED, onDataUpdateCompleted, this); - eventBus.off(Events.BUFFER_LEVEL_STATE_CHANGED, onBufferLevelStateChanged, this); - eventBus.off(Events.LOADING_PROGRESS, onFragmentLoadProgress, this); - eventBus.off(Events.PLAYBACK_PROGRESS, onPlaybackProgression, this); - eventBus.off(Events.PLAYBACK_TIME_UPDATED, onPlaybackProgression, this); - eventBus.off(Events.PLAYBACK_ENDED, onPlaybackEnded, this); - eventBus.off(Events.STREAM_INITIALIZING, onStreamInitializing, this); - stopUpdatingWallclockTime(); - removeAllListeners(); - } - wallclockTimeIntervalId = null; - videoModel = null; - streamInfo = null; - isDynamic = null; + return ret; } function setConfig(config) { @@ -346,12 +472,12 @@ function PlaybackController() { if (config.streamController) { streamController = config.streamController; } + if (config.serviceDescriptionController) { + serviceDescriptionController = config.serviceDescriptionController; + } if (config.dashMetrics) { dashMetrics = config.dashMetrics; } - if (config.mediaPlayerModel) { - mediaPlayerModel = config.mediaPlayerModel; - } if (config.adapter) { adapter = config.adapter; } @@ -361,32 +487,41 @@ function PlaybackController() { if (config.timelineConverter) { timelineConverter = config.timelineConverter; } - if (config.uriFragmentModel) { - uriFragmentModel = config.uriFragmentModel; - } if (config.settings) { settings = config.settings; } } - function getStartTimeFromUriParameters(isDynamic) { - const fragData = uriFragmentModel.getURIFragmentData(); - if (!fragData || !fragData.t) { - return NaN; + /** + * Compare the current time of the video against the DVR window. If we are out of the DVR window we need to seek. + * @param {object} mediaType + */ + function updateCurrentTime(mediaType = null) { + if (isPaused() || !isDynamic || videoModel.getReadyState() === 0 || isSeeking() || manifestUpdateInProgress) return; + + // Note: In some cases we filter certain media types completely (for instance due to an unsupported video codec). This happens after the first entry to the DVR metric has been added. + // Now the DVR window for the filtered media type is not updated anymore. Consequently, always use a mediaType that is available to get a valid DVR window. + if (!mediaType) { + mediaType = streamController.hasVideoTrack() ? Constants.VIDEO : Constants.AUDIO; + } + // Compare the current time of the video element against the range defined in the DVR window. + const currentTime = getTime(); + const actualTime = _getAdjustedPresentationTime(currentTime, mediaType); + const timeChanged = (!isNaN(actualTime) && actualTime !== currentTime); + if (timeChanged && !isSeeking() && (isStalled() || playbackStalled || videoModel.getReadyState() === 1)) { + logger.debug(`UpdateCurrentTime: Seek to actual time: ${actualTime} from currentTime: ${currentTime}`); + seek(actualTime, false, false); } - const refStream = streamController.getStreams()[0]; - const refStreamStartTime = refStream.getStreamInfo().start; - // Consider only start time of MediaRange - // TODO: consider end time of MediaRange to stop playback at provided end time - fragData.t = fragData.t.split(',')[0]; - // "t=