├── .babelrc ├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .github ├── pull_request_template.md └── stale.yml ├── .gitignore ├── .hound.yml ├── .npmrc ├── .travis.yml ├── .yarnclean ├── .yarnrc ├── AUTHORS ├── LICENSE ├── README.md ├── bump ├── jest.config.js ├── package.json ├── public ├── i │ ├── clappr_logo_black.png │ ├── favico.png │ └── poster.jpg ├── index.hlsjs-external.html ├── index.html ├── j │ ├── add-external.js │ ├── clappr-config.js │ ├── editor │ │ ├── ace.js │ │ ├── mode-javascript.js │ │ ├── theme-katzenmilch.js │ │ └── worker-javascript.js │ └── main.js └── stylesheets │ ├── bootstrap-theme.min.css │ ├── bootstrap.min.css │ └── style.css ├── rollup.config.js ├── src ├── hls.js └── hls.test.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "modules": false }] 4 | ], 5 | "env": { 6 | "test": { 7 | "presets": [["@babel/preset-env"]] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | indent_style = space 12 | indent_size = 2 13 | 14 | trim_trailing_whitespace = true 15 | 16 | [*.md] 17 | # add Markdown specifics if needed 18 | 19 | [*json] 20 | # add JSON specifics if needed 21 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "globals": { 9 | "_gaq": false, 10 | "process": false, 11 | "ActiveXObject": false, 12 | "VERSION": false, 13 | "__dirname": false, 14 | "after": false, 15 | "afterEach": false, 16 | "assert": false, 17 | "before": false, 18 | "beforeEach": false, 19 | "describe": false, 20 | "expect": false, 21 | "it": false, 22 | "sinon": false, 23 | "xit": false, 24 | "jest": false, 25 | "test": false, 26 | "module": false, 27 | "require": false, 28 | "CLAPPR_CORE_VERSION": false 29 | }, 30 | "extends": "eslint:recommended", 31 | "parserOptions": { 32 | "sourceType": "module", 33 | "ecmaVersion": 2018 34 | }, 35 | "rules": { 36 | "indent": [ 37 | "error", 38 | 2 39 | ], 40 | "linebreak-style": [ 41 | "error", 42 | "unix" 43 | ], 44 | "quotes": [ 45 | "error", 46 | "single" 47 | ], 48 | "semi": [ 49 | "error", 50 | "never" 51 | ], 52 | "no-var": "error", 53 | "block-spacing": "error", 54 | "curly": ["error", "multi-or-nest", "consistent"], 55 | "object-curly-spacing": ["error", "always"], 56 | "brace-style": ["error", "1tbs", { "allowSingleLine": true }], 57 | "keyword-spacing": "error", 58 | "space-before-blocks": "error", 59 | "arrow-spacing": "error", 60 | "max-len": 0, 61 | "max-statements": 0 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | dist/* -diff 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | This PR implements / updates / fixes... 4 | 5 | 9 | 10 | ## Changes 11 | 12 | - `archive_name1.js`: 13 | - ... 14 | - ... 15 | - `archive_name2.js`: ... 16 | 17 | ## How to test 18 | 19 | 1. ... 20 | 1. ... 21 | 1. ... 22 | 23 | ## Images 24 | 25 | ### Before this PR 26 | 27 | 31 | 32 | ### After this PR 33 | 34 | 35 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - backlog 8 | - bug 9 | - feature 10 | - high-priority 11 | - in-progress 12 | - enhancement 13 | # Label to use when marking an issue as stale 14 | staleLabel: stale 15 | # Comment to post when marking an issue as stale. Set to `false` to disable 16 | markComment: > 17 | This issue has been automatically marked as stale because it has not had 18 | recent activity. It will be closed if no further activity occurs. Thank you 19 | for your contributions. 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: false 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules 3 | .DS_Store 4 | .env 5 | coverage 6 | build/ 7 | docs/ 8 | src/base/jst.js 9 | *.cache 10 | aws.json 11 | npm-debug.log 12 | yarn-error.log 13 | package-lock.json 14 | 15 | # bump 16 | *.bkp 17 | 18 | # Vim 19 | *~ 20 | *.swp 21 | *.swo 22 | 23 | # PhpStorm 24 | .idea 25 | 26 | -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | eslint: 2 | enabled: true 3 | config_file: .eslintrc.json 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" 2 | message="chore(package): bump version" 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: "10" 3 | cache: yarn 4 | os: linux 5 | dist: xenial 6 | 7 | services: 8 | - xvfb 9 | 10 | addons: 11 | chrome: "stable" 12 | firefox: "latest" 13 | 14 | notifications: 15 | email: 16 | - player-web@g.globo 17 | 18 | before_script: "npm run lint" 19 | 20 | after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" 21 | -------------------------------------------------------------------------------- /.yarnclean: -------------------------------------------------------------------------------- 1 | # test directories 2 | __tests__ 3 | test 4 | tests 5 | powered-test 6 | 7 | # asset directories 8 | docs 9 | doc 10 | website 11 | images 12 | 13 | # examples 14 | example 15 | examples 16 | 17 | # code coverage directories 18 | coverage 19 | .nyc_output 20 | 21 | # build scripts 22 | Makefile 23 | Gulpfile.js 24 | Gruntfile.js 25 | 26 | # configs 27 | appveyor.yml 28 | circle.yml 29 | codeship-services.yml 30 | codeship-steps.yml 31 | wercker.yml 32 | .tern-project 33 | .gitattributes 34 | .editorconfig 35 | .*ignore 36 | .flowconfig 37 | .documentup.json 38 | .yarn-metadata.json 39 | .travis.yml 40 | 41 | # misc 42 | *.md 43 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | --install.ignore-engines true -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of Globo.com Player authors for copyright purposes. 2 | # This file is distinct from the CONTRIBUTORS files. 3 | # See the latter for an explanation. 4 | 5 | Globo.com 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Globo.com Player authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the Globo.com nor the names of its contributors 12 | may be used to endorse or promote products derived from this software without 13 | specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 19 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 21 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HlsjsPlayback 2 | 3 | A [Clappr](https://github.com/clappr/clappr) playback to play HTTP Live Streaming (HLS) based on the [hls.js](https://github.com/video-dev/hls.js). 4 | 5 |

6 | 7 | 8 | 9 | 10 | 11 | jsDelivr hits (npm) 12 |

13 | 14 | 15 | ## Usage 16 | 17 | You can use it from JSDelivr: 18 | 19 | `https://cdn.jsdelivr.net/npm/@clappr/hlsjs-playback@latest/dist/hlsjs-playback.min.js` 20 | 21 | or as an npm package: 22 | 23 | `yarn add @clappr/hlsjs-playback` 24 | 25 | Then just add `HlsjsPlayback` into the list of plugins of your player instance: 26 | 27 | ```javascript 28 | var player = new Clappr.Player( 29 | { 30 | source: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8', 31 | plugins: [HlsjsPlayback], 32 | }); 33 | ``` 34 | 35 | ## Configuration 36 | 37 | The options for this playback are shown below: 38 | 39 | ```javascript 40 | var player = new Clappr.Player( 41 | { 42 | source: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8', 43 | plugins: [HlsjsPlayback], 44 | hlsUseNextLevel: false, 45 | hlsMinimumDvrSize: 60, 46 | hlsRecoverAttempts: 16, 47 | hlsPlayback: { 48 | preload: true, 49 | customListeners: [], 50 | }, 51 | playback: { 52 | extrapolatedWindowNumSegments: 2, 53 | triggerFatalErrorOnResourceDenied: false, 54 | hlsjsConfig: { 55 | // hls.js specific options 56 | }, 57 | }, 58 | }); 59 | ``` 60 | 61 | #### hlsUseNextLevel 62 | > Default value: `false` 63 | 64 | The default behavior for the HLS playback is to use [hls.currentLevel](https://github.com/video-dev/hls.js/blob/master/docs/API.md#hlscurrentlevel) to switch current level. To change this behaviour and force HLS playback to use [hls.nextLevel](https://github.com/video-dev/hls.js/blob/master/docs/API.md#hlsnextlevel), add `hlsUseNextLevel: true` to embed parameters. 65 | 66 | #### hlsMinimumDvrSize 67 | > Default value: `60 (seconds)` 68 | 69 | Option to define the minimum DVR size to active seek on Clappr live mode. 70 | 71 | #### extrapolatedWindowNumSegments 72 | > Default value: `2` 73 | 74 | Configure the size of the start time extrapolation window measured as a multiple of segments. 75 | 76 | Should be 2 or higher, or 0 to disable. It should only need to be increased above 2 if more than one segment is removed from the start of the playlist at a time. 77 | 78 | E.g.: If the playlist is cached for 10 seconds and new chunks are added/removed every 5. 79 | 80 | #### hlsRecoverAttempts 81 | > Default value: `16` 82 | 83 | The `hls.js` have recover approaches for some fatal errors. This option sets the max recovery attempts number for those errors. 84 | 85 | #### triggerFatalErrorOnResourceDenied 86 | > Default value: `false` 87 | 88 | If this option is set to true, the playback will triggers fatal error event if decrypt key http response code is greater than or equal to 400. This option is used to attempt to reproduce iOS devices behaviour which internally use html5 video playback. 89 | 90 | #### hlsPlayback 91 | > Soon (in a new breaking change version), all options related to this playback that are declared in the scope of the `options` object will have to be declared necessarily within this new scope! 92 | 93 | Groups all options related directly to `HlsjsPlayback` configs. 94 | 95 | ```javascript 96 | var player = new Clappr.Player( 97 | { 98 | ... 99 | hlsPlayback: { 100 | preload: true, 101 | customListeners: [], 102 | }, 103 | }); 104 | ``` 105 | 106 | #### `hlsPlayback.preload` 107 | > Default value: `true` 108 | 109 | Configures whether the source should be loaded as soon as the `HLS.JS` internal reference is setup or only after the first play. 110 | 111 | #### `hlsPlayback.customListeners` 112 | 113 | An array of listeners object with specific parameters to add on `HLS.JS` instance. 114 | 115 | ```javascript 116 | var player = new Clappr.Player( 117 | { 118 | ... 119 | hlsPlayback: { 120 | ... 121 | customListeners: [ 122 | // "hlsFragLoaded" is the value of HlsjsPlayback.HLSJS.Events.FRAG_LOADED constant. 123 | { eventName: 'hlsFragLoaded', callback: (event, data) => { console.log('>>>>>> data: ', data) }, once: true } 124 | ], 125 | }, 126 | }); 127 | ``` 128 | 129 | The listener object parameters are: 130 | 131 | * `eventName`: A valid event name of `hls.js` [events API](https://github.com/video-dev/hls.js/blob/master/docs/API.md#runtime-events); 132 | * `callback`: The callback that should be called when the event listened happen. 133 | * `once`: Flag to configure if the listener needs to be valid just for one time. 134 | 135 | #### hlsjsConfig 136 | 137 | As `HlsjsPlayback` is based on `hls.js`, it's possible to use the available `hls.js` configs too. You can check them out [here](https://github.com/video-dev/hls.js/blob/master/docs/API.md#fine-tuning). 138 | 139 | To use these settings, use the `hlsjsConfig` object. 140 | 141 | Example: 142 | 143 | ```javascript 144 | var player = new Clappr.Player( 145 | { 146 | ... 147 | playback: { 148 | hlsjsConfig: { 149 | debug: true, // https://github.com/video-dev/hls.js/blob/master/docs/API.md#debug 150 | enableworker: false, // https://github.com/video-dev/hls.js/blob/master/docs/API.md#enableworker 151 | ... 152 | }, 153 | }, 154 | }); 155 | ``` 156 | 157 | ## Development 158 | 159 | Enter the project directory and install the dependencies: 160 | 161 | `yarn install` 162 | 163 | Make your changes and run the tests: 164 | 165 | `yarn test` 166 | 167 | Build your own version: 168 | 169 | `yarn build` 170 | 171 | Check the result on `dist/` folder. 172 | 173 | Starting a local server: 174 | 175 | `yarn start` 176 | 177 | This command will start an HTTP Server on port 8080. You can check a sample page with Clappr-core using the HlsjsPlayback on http://localhost:8080/ 178 | 179 | ## Release 180 | 181 | To release a new version, first create a new tag by running: 182 | 183 | `npm version [patch | minor | major]` 184 | 185 | Choose between `patch`, `minor`, or `major` according to the changes for the new version. 186 | 187 | After that, publish the new version to NPM by running: 188 | 189 | `npm publish` 190 | 191 | Check the new version on [npmjs @clappr/hlsjs-playback](https://www.npmjs.com/package/@clappr/hlsjs-playback). 192 | -------------------------------------------------------------------------------- /bump: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PROJECT_NAME='hlsjs-playback' 4 | CDN_PATH="npm/@clappr/hlsjs-playback@latest/dist/$PROJECT_NAME.min.js" 5 | 6 | update_dependencies() { 7 | echo 'updating dependencies' && 8 | yarn install 9 | } 10 | 11 | update_version() { 12 | current_tag=$(git describe --abbrev=0 --tags master) && 13 | echo 'bump from '$current_tag' to '$1 && 14 | sed -i ".bkp" "s/\(version\":[ ]*\"\)$current_tag/\1$1/" package.json 15 | } 16 | 17 | build() { 18 | echo "building $PROJECT_NAME.js" && 19 | yarn build && 20 | echo "building $PROJECT_NAME.min.js" && 21 | yarn release 22 | } 23 | 24 | run_tests() { 25 | yarn lint 26 | yarn test 27 | } 28 | 29 | make_release_commit() { 30 | git add package.json yarn.lock && 31 | git commit -m 'chore(package): bump to '$1 && 32 | git tag -m "$1" $1 33 | } 34 | 35 | git_push() { 36 | echo 'pushing to github' 37 | git push origin master --tags 38 | } 39 | 40 | npm_publish() { 41 | npm publish 42 | } 43 | 44 | purge_cdn_cache() { 45 | echo 'purging cdn cache' 46 | curl -q "http://purge.jsdelivr.net/$CDN_PATH" 47 | } 48 | 49 | main() { 50 | npm whoami 51 | if (("$?" != "0")); then 52 | echo "you are not logged into npm" 53 | exit 1 54 | fi 55 | update_dependencies && 56 | update_version $1 && 57 | build 58 | if (("$?" != "0")); then 59 | echo "something failed during dependency update, version update, or build" 60 | exit 1 61 | fi 62 | run_tests 63 | if (("$?" == "0")); then 64 | make_release_commit $1 && 65 | git_push && 66 | npm_publish && 67 | purge_cdn_cache && 68 | exit 0 69 | 70 | echo "something failed" 71 | exit 1 72 | else 73 | echo "you broke the tests. fix it before bumping another version." 74 | exit 1 75 | fi 76 | } 77 | 78 | if [ "$1" != "" ]; then 79 | main $1 80 | else 81 | echo "Usage: bump [new_version]" 82 | fi 83 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const ClapprCorePkg = require('@clappr/core/package.json') 2 | 3 | module.exports = { 4 | verbose: true, 5 | transform: { 6 | '^.+\\.js$': 'babel-jest', 7 | }, 8 | moduleNameMapper: { 9 | '^@/(.*)$': '/src/$1', 10 | '^clappr$': '/node_modules/@clappr/core/dist/clappr-core.js', 11 | '^clappr-zepto$': 'clappr-zepto/zepto.js', 12 | }, 13 | globals: { CLAPPR_CORE_VERSION: ClapprCorePkg.version }, 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@clappr/hlsjs-playback", 3 | "version": "1.3.0", 4 | "description": "HLS Playback based on hls.js", 5 | "main": "./dist/hlsjs-playback.js", 6 | "module": "./dist/hlsjs-playback.esm.js", 7 | "scripts": { 8 | "start": "SERVE=true rollup --config --watch", 9 | "start:with-reload": "SERVE=true RELOAD=true rollup --config --watch", 10 | "build": "rollup --config", 11 | "watch": "rollup --config --watch", 12 | "bundle-check": "ANALYZE_BUNDLE=true rollup --config", 13 | "release": "MINIMIZE=true rollup --config", 14 | "test": "jest ./src --coverage --silent", 15 | "test:debug": "node --inspect node_modules/.bin/jest ./src --runInBand", 16 | "test:watch": "jest ./src --watch", 17 | "lint": "eslint *.js ./src", 18 | "lint:fix": "yarn lint -- --fix", 19 | "prepublishOnly": "npm run release" 20 | }, 21 | "files": [ 22 | "/dist", 23 | "/src" 24 | ], 25 | "publishConfig": { 26 | "access": "public" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git@github.com:clappr/hlsjs-playback.git" 31 | }, 32 | "author": "Globo.com", 33 | "license": "BSD-3-Clause", 34 | "bugs": { 35 | "url": "https://github.com/clappr/hlsjs-playback/issues" 36 | }, 37 | "homepage": "https://github.com/clappr/hlsjs-playback", 38 | "peerDependencies": { 39 | "@clappr/core": "^0.4.27", 40 | "hls.js": "^1.2.4" 41 | }, 42 | "devDependencies": { 43 | "@babel/core": "^7.14.2", 44 | "@babel/preset-env": "^7.14.2", 45 | "@clappr/core": "^0.4.27", 46 | "@rollup/plugin-babel": "^5.3.0", 47 | "@rollup/plugin-commonjs": "^19.0.0", 48 | "@rollup/plugin-node-resolve": "^13.0.0", 49 | "@rollup/plugin-replace": "^2.4.2", 50 | "babel-jest": "^26.6.3", 51 | "coveralls": "^3.1.0", 52 | "cz-conventional-changelog": "^3.3.0", 53 | "eslint": "^7.26.0", 54 | "hls.js": "^1.2.4", 55 | "jest": "^26.6.3", 56 | "rollup": "^2.47.0", 57 | "rollup-plugin-filesize": "^9.1.1", 58 | "rollup-plugin-livereload": "^2.0.0", 59 | "rollup-plugin-serve": "^1.1.0", 60 | "rollup-plugin-sizes": "^1.0.4", 61 | "rollup-plugin-terser": "^7.0.2", 62 | "rollup-plugin-visualizer": "^5.5.0" 63 | }, 64 | "config": { 65 | "commitizen": { 66 | "path": "./node_modules/cz-conventional-changelog" 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /public/i/clappr_logo_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clappr/hlsjs-playback/fc1d45d5c1dc819d4546c4675b3d74e4bf791ad7/public/i/clappr_logo_black.png -------------------------------------------------------------------------------- /public/i/favico.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clappr/hlsjs-playback/fc1d45d5c1dc819d4546c4675b3d74e4bf791ad7/public/i/favico.png -------------------------------------------------------------------------------- /public/i/poster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clappr/hlsjs-playback/fc1d45d5c1dc819d4546c4675b3d74e4bf791ad7/public/i/poster.jpg -------------------------------------------------------------------------------- /public/index.hlsjs-external.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | Clappr 20 |
    21 |
  • 22 | docs 23 |
  • 24 |
25 |
26 |
27 |
28 |

29 | Add external plugins: 30 | 31 | 32 |

33 |
34 |
35 |
36 |
37 | 57 |

58 | Load hlsjs-playback.js version 59 |

60 |
61 |
62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | Clappr 19 |
    20 |
  • 21 | docs 22 |
  • 23 |
24 |
25 |
26 |
27 |

28 | Add external plugins: 29 | 30 | 31 |

32 |
33 |
34 |
35 |
36 | 56 |

57 | Load hlsjs-playback.external.js version 58 |

59 |
60 |
61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /public/j/add-external.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | window.clappr = window.clappr || {} 4 | window.clappr.externals = [] 5 | 6 | function addExternal() { 7 | let url = document.getElementById('js-link') 8 | window.clappr.externals.push(url.value) 9 | addTag(url.value) 10 | url.value = '' 11 | } 12 | 13 | function addTag(url) { 14 | let colors = ['aliceblue', 'antiquewhite', 'azure', 'black', 'blue', 'brown', 'yellow', 'teal'] 15 | let color = colors[Math.floor(Math.random() * colors.length)] 16 | let span = document.createElement('span') 17 | 18 | span.style.backgroundColor = color 19 | span.className = 'external-js' 20 | span.innerText = url.split(/\//).pop().split(/\./)[0] 21 | 22 | document.getElementById('external-js-panel').appendChild(span) 23 | } 24 | -------------------------------------------------------------------------------- /public/j/clappr-config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const playerElement = document.getElementById('player-wrapper'); 4 | 5 | player = new Clappr.Player({ 6 | source: urlParams.src || 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8', 7 | poster: urlParams.poster || '../i/poster.jpg', 8 | mute: true, 9 | autoPlay: false, 10 | height: 360, 11 | width: 640, 12 | playback: { 13 | controls: true, 14 | }, 15 | plugins: [HlsjsPlayback], 16 | }); 17 | 18 | player.attachTo(playerElement); 19 | !player.options.autoPlay && player.core && player.core.activePlayback && player.core.activePlayback.el.addEventListener('play', () => player.play(), { once: true }); 20 | -------------------------------------------------------------------------------- /public/j/editor/mode-javascript.js: -------------------------------------------------------------------------------- 1 | define("ace/mode/javascript",["require","exports","module","ace/lib/oop","ace/mode/text","ace/tokenizer","ace/mode/javascript_highlight_rules","ace/mode/matching_brace_outdent","ace/range","ace/worker/worker_client","ace/mode/behaviour/cstyle","ace/mode/folding/cstyle"],function(e,t,n){var r=e("../lib/oop"),i=e("./text").Mode,s=e("../tokenizer").Tokenizer,o=e("./javascript_highlight_rules").JavaScriptHighlightRules,u=e("./matching_brace_outdent").MatchingBraceOutdent,a=e("../range").Range,f=e("../worker/worker_client").WorkerClient,l=e("./behaviour/cstyle").CstyleBehaviour,c=e("./folding/cstyle").FoldMode,h=function(){this.HighlightRules=o,this.$outdent=new u,this.$behaviour=new l,this.foldingRules=new c};r.inherits(h,i),function(){this.lineCommentStart="//",this.blockComment={start:"/*",end:"*/"},this.getNextLineIndent=function(e,t,n){var r=this.$getIndent(t),i=this.getTokenizer().getLineTokens(t,e),s=i.tokens,o=i.state;if(s.length&&s[s.length-1].type=="comment")return r;if(e=="start"||e=="no_regex"){var u=t.match(/^.*(?:\bcase\b.*\:|[\{\(\[])\s*$/);u&&(r+=n)}else if(e=="doc-start"){if(o=="start"||o=="no_regex")return"";var u=t.match(/^\s*(\/?)\*/);u&&(u[1]&&(r+=" "),r+="* ")}return r},this.checkOutdent=function(e,t,n){return this.$outdent.checkOutdent(t,n)},this.autoOutdent=function(e,t,n){this.$outdent.autoOutdent(t,n)},this.createWorker=function(e){var t=new f(["ace"],"ace/mode/javascript_worker","JavaScriptWorker");return t.attachToDocument(e.getDocument()),t.on("jslint",function(t){e.setAnnotations(t.data)}),t.on("terminate",function(){e.clearAnnotations()}),t},this.$id="ace/mode/javascript"}.call(h.prototype),t.Mode=h}),define("ace/mode/javascript_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/doc_comment_highlight_rules","ace/mode/text_highlight_rules"],function(e,t,n){var r=e("../lib/oop"),i=e("./doc_comment_highlight_rules").DocCommentHighlightRules,s=e("./text_highlight_rules").TextHighlightRules,o=function(){var e=this.createKeywordMapper({"variable.language":"Array|Boolean|Date|Function|Iterator|Number|Object|RegExp|String|Proxy|Namespace|QName|XML|XMLList|ArrayBuffer|Float32Array|Float64Array|Int16Array|Int32Array|Int8Array|Uint16Array|Uint32Array|Uint8Array|Uint8ClampedArray|Error|EvalError|InternalError|RangeError|ReferenceError|StopIteration|SyntaxError|TypeError|URIError|decodeURI|decodeURIComponent|encodeURI|encodeURIComponent|eval|isFinite|isNaN|parseFloat|parseInt|JSON|Math|this|arguments|prototype|window|document",keyword:"const|yield|import|get|set|break|case|catch|continue|default|delete|do|else|finally|for|function|if|in|instanceof|new|return|switch|throw|try|typeof|let|var|while|with|debugger|__parent__|__count__|escape|unescape|with|__proto__|class|enum|extends|super|export|implements|private|public|interface|package|protected|static","storage.type":"const|let|var|function","constant.language":"null|Infinity|NaN|undefined","support.function":"alert","constant.language.boolean":"true|false"},"identifier"),t="case|do|else|finally|in|instanceof|return|throw|try|typeof|yield|void",n="[a-zA-Z\\$_\xa1-\uffff][a-zA-Z\\d\\$_\xa1-\uffff]*\\b",r="\\\\(?:x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|[0-2][0-7]{0,2}|3[0-6][0-7]?|37[0-7]?|[4-7][0-7]?|.)";this.$rules={no_regex:[{token:"comment",regex:"\\/\\/",next:"line_comment"},i.getStartRule("doc-start"),{token:"comment",regex:/\/\*/,next:"comment"},{token:"string",regex:"'(?=.)",next:"qstring"},{token:"string",regex:'"(?=.)',next:"qqstring"},{token:"constant.numeric",regex:/0[xX][0-9a-fA-F]+\b/},{token:"constant.numeric",regex:/[+-]?\d+(?:(?:\.\d*)?(?:[eE][+-]?\d+)?)?\b/},{token:["storage.type","punctuation.operator","support.function","punctuation.operator","entity.name.function","text","keyword.operator"],regex:"("+n+")(\\.)(prototype)(\\.)("+n+")(\\s*)(=)",next:"function_arguments"},{token:["storage.type","punctuation.operator","entity.name.function","text","keyword.operator","text","storage.type","text","paren.lparen"],regex:"("+n+")(\\.)("+n+")(\\s*)(=)(\\s*)(function)(\\s*)(\\()",next:"function_arguments"},{token:["entity.name.function","text","keyword.operator","text","storage.type","text","paren.lparen"],regex:"("+n+")(\\s*)(=)(\\s*)(function)(\\s*)(\\()",next:"function_arguments"},{token:["storage.type","punctuation.operator","entity.name.function","text","keyword.operator","text","storage.type","text","entity.name.function","text","paren.lparen"],regex:"("+n+")(\\.)("+n+")(\\s*)(=)(\\s*)(function)(\\s+)(\\w+)(\\s*)(\\()",next:"function_arguments"},{token:["storage.type","text","entity.name.function","text","paren.lparen"],regex:"(function)(\\s+)("+n+")(\\s*)(\\()",next:"function_arguments"},{token:["entity.name.function","text","punctuation.operator","text","storage.type","text","paren.lparen"],regex:"("+n+")(\\s*)(:)(\\s*)(function)(\\s*)(\\()",next:"function_arguments"},{token:["text","text","storage.type","text","paren.lparen"],regex:"(:)(\\s*)(function)(\\s*)(\\()",next:"function_arguments"},{token:"keyword",regex:"(?:"+t+")\\b",next:"start"},{token:["punctuation.operator","support.function"],regex:/(\.)(s(?:h(?:ift|ow(?:Mod(?:elessDialog|alDialog)|Help))|croll(?:X|By(?:Pages|Lines)?|Y|To)?|t(?:op|rike)|i(?:n|zeToContent|debar|gnText)|ort|u(?:p|b(?:str(?:ing)?)?)|pli(?:ce|t)|e(?:nd|t(?:Re(?:sizable|questHeader)|M(?:i(?:nutes|lliseconds)|onth)|Seconds|Ho(?:tKeys|urs)|Year|Cursor|Time(?:out)?|Interval|ZOptions|Date|UTC(?:M(?:i(?:nutes|lliseconds)|onth)|Seconds|Hours|Date|FullYear)|FullYear|Active)|arch)|qrt|lice|avePreferences|mall)|h(?:ome|andleEvent)|navigate|c(?:har(?:CodeAt|At)|o(?:s|n(?:cat|textual|firm)|mpile)|eil|lear(?:Timeout|Interval)?|a(?:ptureEvents|ll)|reate(?:StyleSheet|Popup|EventObject))|t(?:o(?:GMTString|S(?:tring|ource)|U(?:TCString|pperCase)|Lo(?:caleString|werCase))|est|a(?:n|int(?:Enabled)?))|i(?:s(?:NaN|Finite)|ndexOf|talics)|d(?:isableExternalCapture|ump|etachEvent)|u(?:n(?:shift|taint|escape|watch)|pdateCommands)|j(?:oin|avaEnabled)|p(?:o(?:p|w)|ush|lugins.refresh|a(?:ddings|rse(?:Int|Float)?)|r(?:int|ompt|eference))|e(?:scape|nableExternalCapture|val|lementFromPoint|x(?:p|ec(?:Script|Command)?))|valueOf|UTC|queryCommand(?:State|Indeterm|Enabled|Value)|f(?:i(?:nd|le(?:ModifiedDate|Size|CreatedDate|UpdatedDate)|xed)|o(?:nt(?:size|color)|rward)|loor|romCharCode)|watch|l(?:ink|o(?:ad|g)|astIndexOf)|a(?:sin|nchor|cos|t(?:tachEvent|ob|an(?:2)?)|pply|lert|b(?:s|ort))|r(?:ou(?:nd|teEvents)|e(?:size(?:By|To)|calc|turnValue|place|verse|l(?:oad|ease(?:Capture|Events)))|andom)|g(?:o|et(?:ResponseHeader|M(?:i(?:nutes|lliseconds)|onth)|Se(?:conds|lection)|Hours|Year|Time(?:zoneOffset)?|Da(?:y|te)|UTC(?:M(?:i(?:nutes|lliseconds)|onth)|Seconds|Hours|Da(?:y|te)|FullYear)|FullYear|A(?:ttention|llResponseHeaders)))|m(?:in|ove(?:B(?:y|elow)|To(?:Absolute)?|Above)|ergeAttributes|a(?:tch|rgins|x))|b(?:toa|ig|o(?:ld|rderWidths)|link|ack))\b(?=\()/},{token:["punctuation.operator","support.function.dom"],regex:/(\.)(s(?:ub(?:stringData|mit)|plitText|e(?:t(?:NamedItem|Attribute(?:Node)?)|lect))|has(?:ChildNodes|Feature)|namedItem|c(?:l(?:ick|o(?:se|neNode))|reate(?:C(?:omment|DATASection|aption)|T(?:Head|extNode|Foot)|DocumentFragment|ProcessingInstruction|E(?:ntityReference|lement)|Attribute))|tabIndex|i(?:nsert(?:Row|Before|Cell|Data)|tem)|open|delete(?:Row|C(?:ell|aption)|T(?:Head|Foot)|Data)|focus|write(?:ln)?|a(?:dd|ppend(?:Child|Data))|re(?:set|place(?:Child|Data)|move(?:NamedItem|Child|Attribute(?:Node)?)?)|get(?:NamedItem|Element(?:sBy(?:Name|TagName)|ById)|Attribute(?:Node)?)|blur)\b(?=\()/},{token:["punctuation.operator","support.constant"],regex:/(\.)(s(?:ystemLanguage|cr(?:ipts|ollbars|een(?:X|Y|Top|Left))|t(?:yle(?:Sheets)?|atus(?:Text|bar)?)|ibling(?:Below|Above)|ource|uffixes|e(?:curity(?:Policy)?|l(?:ection|f)))|h(?:istory|ost(?:name)?|as(?:h|Focus))|y|X(?:MLDocument|SLDocument)|n(?:ext|ame(?:space(?:s|URI)|Prop))|M(?:IN_VALUE|AX_VALUE)|c(?:haracterSet|o(?:n(?:structor|trollers)|okieEnabled|lorDepth|mp(?:onents|lete))|urrent|puClass|l(?:i(?:p(?:boardData)?|entInformation)|osed|asses)|alle(?:e|r)|rypto)|t(?:o(?:olbar|p)|ext(?:Transform|Indent|Decoration|Align)|ags)|SQRT(?:1_2|2)|i(?:n(?:ner(?:Height|Width)|put)|ds|gnoreCase)|zIndex|o(?:scpu|n(?:readystatechange|Line)|uter(?:Height|Width)|p(?:sProfile|ener)|ffscreenBuffering)|NEGATIVE_INFINITY|d(?:i(?:splay|alog(?:Height|Top|Width|Left|Arguments)|rectories)|e(?:scription|fault(?:Status|Ch(?:ecked|arset)|View)))|u(?:ser(?:Profile|Language|Agent)|n(?:iqueID|defined)|pdateInterval)|_content|p(?:ixelDepth|ort|ersonalbar|kcs11|l(?:ugins|atform)|a(?:thname|dding(?:Right|Bottom|Top|Left)|rent(?:Window|Layer)?|ge(?:X(?:Offset)?|Y(?:Offset)?))|r(?:o(?:to(?:col|type)|duct(?:Sub)?|mpter)|e(?:vious|fix)))|e(?:n(?:coding|abledPlugin)|x(?:ternal|pando)|mbeds)|v(?:isibility|endor(?:Sub)?|Linkcolor)|URLUnencoded|P(?:I|OSITIVE_INFINITY)|f(?:ilename|o(?:nt(?:Size|Family|Weight)|rmName)|rame(?:s|Element)|gColor)|E|whiteSpace|l(?:i(?:stStyleType|n(?:eHeight|kColor))|o(?:ca(?:tion(?:bar)?|lName)|wsrc)|e(?:ngth|ft(?:Context)?)|a(?:st(?:M(?:odified|atch)|Index|Paren)|yer(?:s|X)|nguage))|a(?:pp(?:MinorVersion|Name|Co(?:deName|re)|Version)|vail(?:Height|Top|Width|Left)|ll|r(?:ity|guments)|Linkcolor|bove)|r(?:ight(?:Context)?|e(?:sponse(?:XML|Text)|adyState))|global|x|m(?:imeTypes|ultiline|enubar|argin(?:Right|Bottom|Top|Left))|L(?:N(?:10|2)|OG(?:10E|2E))|b(?:o(?:ttom|rder(?:Width|RightWidth|BottomWidth|Style|Color|TopWidth|LeftWidth))|ufferDepth|elow|ackground(?:Color|Image)))\b/},{token:["storage.type","punctuation.operator","support.function.firebug"],regex:/(console)(\.)(warn|info|log|error|time|timeEnd|assert)\b/},{token:e,regex:n},{token:"keyword.operator",regex:/--|\+\+|[!$%&*+\-~]|===|==|=|!=|!==|<=|>=|<<=|>>=|>>>=|<>|<|>|!|&&|\|\||\?\:|\*=|%=|\+=|\-=|&=|\^=/,next:"start"},{token:"punctuation.operator",regex:/\?|\:|\,|\;|\./,next:"start"},{token:"paren.lparen",regex:/[\[({]/,next:"start"},{token:"paren.rparen",regex:/[\])}]/},{token:"keyword.operator",regex:/\/=?/,next:"start"},{token:"comment",regex:/^#!.*$/}],start:[i.getStartRule("doc-start"),{token:"comment",regex:"\\/\\*",next:"comment_regex_allowed"},{token:"comment",regex:"\\/\\/",next:"line_comment_regex_allowed"},{token:"string.regexp",regex:"\\/",next:"regex"},{token:"text",regex:"\\s+|^$",next:"start"},{token:"empty",regex:"",next:"no_regex"}],regex:[{token:"regexp.keyword.operator",regex:"\\\\(?:u[\\da-fA-F]{4}|x[\\da-fA-F]{2}|.)"},{token:"string.regexp",regex:"/[sxngimy]*",next:"no_regex"},{token:"invalid",regex:/\{\d+\b,?\d*\}[+*]|[+*$^?][+*]|[$^][?]|\?{3,}/},{token:"constant.language.escape",regex:/\(\?[:=!]|\)|\{\d+\b,?\d*\}|[+*]\?|[()$^+*?.]/},{token:"constant.language.delimiter",regex:/\|/},{token:"constant.language.escape",regex:/\[\^?/,next:"regex_character_class"},{token:"empty",regex:"$",next:"no_regex"},{defaultToken:"string.regexp"}],regex_character_class:[{token:"regexp.keyword.operator",regex:"\\\\(?:u[\\da-fA-F]{4}|x[\\da-fA-F]{2}|.)"},{token:"constant.language.escape",regex:"]",next:"regex"},{token:"constant.language.escape",regex:"-"},{token:"empty",regex:"$",next:"no_regex"},{defaultToken:"string.regexp.charachterclass"}],function_arguments:[{token:"variable.parameter",regex:n},{token:"punctuation.operator",regex:"[, ]+"},{token:"punctuation.operator",regex:"$"},{token:"empty",regex:"",next:"no_regex"}],comment_regex_allowed:[{token:"comment",regex:"\\*\\/",next:"start"},{defaultToken:"comment"}],comment:[{token:"comment",regex:"\\*\\/",next:"no_regex"},{defaultToken:"comment"}],line_comment_regex_allowed:[{token:"comment",regex:"$|^",next:"start"},{defaultToken:"comment"}],line_comment:[{token:"comment",regex:"$|^",next:"no_regex"},{defaultToken:"comment"}],qqstring:[{token:"constant.language.escape",regex:r},{token:"string",regex:"\\\\$",next:"qqstring"},{token:"string",regex:'"|$',next:"no_regex"},{defaultToken:"string"}],qstring:[{token:"constant.language.escape",regex:r},{token:"string",regex:"\\\\$",next:"qstring"},{token:"string",regex:"'|$",next:"no_regex"},{defaultToken:"string"}]},this.embedRules(i,"doc-",[i.getEndRule("no_regex")])};r.inherits(o,s),t.JavaScriptHighlightRules=o}),define("ace/mode/doc_comment_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"],function(e,t,n){var r=e("../lib/oop"),i=e("./text_highlight_rules").TextHighlightRules,s=function(){this.$rules={start:[{token:"comment.doc.tag",regex:"@[\\w\\d_]+"},{token:"comment.doc.tag",regex:"\\bTODO\\b"},{defaultToken:"comment.doc"}]}};r.inherits(s,i),s.getStartRule=function(e){return{token:"comment.doc",regex:"\\/\\*(?=\\*)",next:e}},s.getEndRule=function(e){return{token:"comment.doc",regex:"\\*\\/",next:e}},t.DocCommentHighlightRules=s}),define("ace/mode/matching_brace_outdent",["require","exports","module","ace/range"],function(e,t,n){var r=e("../range").Range,i=function(){};(function(){this.checkOutdent=function(e,t){return/^\s+$/.test(e)?/^\s*\}/.test(t):!1},this.autoOutdent=function(e,t){var n=e.getLine(t),i=n.match(/^(\s*\})/);if(!i)return 0;var s=i[1].length,o=e.findMatchingBracket({row:t,column:s});if(!o||o.row==t)return 0;var u=this.$getIndent(e.getLine(o.row));e.replace(new r(t,0,t,s-1),u)},this.$getIndent=function(e){return e.match(/^\s*/)[0]}}).call(i.prototype),t.MatchingBraceOutdent=i}),define("ace/mode/behaviour/cstyle",["require","exports","module","ace/lib/oop","ace/mode/behaviour","ace/token_iterator","ace/lib/lang"],function(e,t,n){var r=e("../../lib/oop"),i=e("../behaviour").Behaviour,s=e("../../token_iterator").TokenIterator,o=e("../../lib/lang"),u=["text","paren.rparen","punctuation.operator"],a=["text","paren.rparen","punctuation.operator","comment"],f,l={},c=function(e){var t=-1;e.multiSelect&&(t=e.selection.id,l.rangeCount!=e.multiSelect.rangeCount&&(l={rangeCount:e.multiSelect.rangeCount}));if(l[t])return f=l[t];f=l[t]={autoInsertedBrackets:0,autoInsertedRow:-1,autoInsertedLineEnd:"",maybeInsertedBrackets:0,maybeInsertedRow:-1,maybeInsertedLineStart:"",maybeInsertedLineEnd:""}},h=function(){this.add("braces","insertion",function(e,t,n,r,i){var s=n.getCursorPosition(),u=r.doc.getLine(s.row);if(i=="{"){c(n);var a=n.getSelectionRange(),l=r.doc.getTextRange(a);if(l!==""&&l!=="{"&&n.getWrapBehavioursEnabled())return{text:"{"+l+"}",selection:!1};if(h.isSaneInsertion(n,r))return/[\]\}\)]/.test(u[s.column])||n.inMultiSelectMode?(h.recordAutoInsert(n,r,"}"),{text:"{}",selection:[1,1]}):(h.recordMaybeInsert(n,r,"{"),{text:"{",selection:[1,1]})}else if(i=="}"){c(n);var p=u.substring(s.column,s.column+1);if(p=="}"){var d=r.$findOpeningBracket("}",{column:s.column+1,row:s.row});if(d!==null&&h.isAutoInsertedClosing(s,u,i))return h.popAutoInsertedClosing(),{text:"",selection:[1,1]}}}else{if(i=="\n"||i=="\r\n"){c(n);var v="";h.isMaybeInsertedClosing(s,u)&&(v=o.stringRepeat("}",f.maybeInsertedBrackets),h.clearMaybeInsertedClosing());var p=u.substring(s.column,s.column+1);if(p==="}"){var m=r.findMatchingBracket({row:s.row,column:s.column+1},"}");if(!m)return null;var g=this.$getIndent(r.getLine(m.row))}else{if(!v){h.clearMaybeInsertedClosing();return}var g=this.$getIndent(u)}var y=g+r.getTabString();return{text:"\n"+y+"\n"+g+v,selection:[1,y.length,1,y.length]}}h.clearMaybeInsertedClosing()}}),this.add("braces","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s=="{"){c(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.end.column,i.end.column+1);if(u=="}")return i.end.column++,i;f.maybeInsertedBrackets--}}),this.add("parens","insertion",function(e,t,n,r,i){if(i=="("){c(n);var s=n.getSelectionRange(),o=r.doc.getTextRange(s);if(o!==""&&n.getWrapBehavioursEnabled())return{text:"("+o+")",selection:!1};if(h.isSaneInsertion(n,r))return h.recordAutoInsert(n,r,")"),{text:"()",selection:[1,1]}}else if(i==")"){c(n);var u=n.getCursorPosition(),a=r.doc.getLine(u.row),f=a.substring(u.column,u.column+1);if(f==")"){var l=r.$findOpeningBracket(")",{column:u.column+1,row:u.row});if(l!==null&&h.isAutoInsertedClosing(u,a,i))return h.popAutoInsertedClosing(),{text:"",selection:[1,1]}}}}),this.add("parens","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s=="("){c(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.start.column+1,i.start.column+2);if(u==")")return i.end.column++,i}}),this.add("brackets","insertion",function(e,t,n,r,i){if(i=="["){c(n);var s=n.getSelectionRange(),o=r.doc.getTextRange(s);if(o!==""&&n.getWrapBehavioursEnabled())return{text:"["+o+"]",selection:!1};if(h.isSaneInsertion(n,r))return h.recordAutoInsert(n,r,"]"),{text:"[]",selection:[1,1]}}else if(i=="]"){c(n);var u=n.getCursorPosition(),a=r.doc.getLine(u.row),f=a.substring(u.column,u.column+1);if(f=="]"){var l=r.$findOpeningBracket("]",{column:u.column+1,row:u.row});if(l!==null&&h.isAutoInsertedClosing(u,a,i))return h.popAutoInsertedClosing(),{text:"",selection:[1,1]}}}}),this.add("brackets","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s=="["){c(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.start.column+1,i.start.column+2);if(u=="]")return i.end.column++,i}}),this.add("string_dquotes","insertion",function(e,t,n,r,i){if(i=='"'||i=="'"){c(n);var s=i,o=n.getSelectionRange(),u=r.doc.getTextRange(o);if(u!==""&&u!=="'"&&u!='"'&&n.getWrapBehavioursEnabled())return{text:s+u+s,selection:!1};var a=n.getCursorPosition(),f=r.doc.getLine(a.row),l=f.substring(a.column-1,a.column);if(l=="\\")return null;var p=r.getTokens(o.start.row),d=0,v,m=-1;for(var g=0;go.start.column)break;d+=p[g].value.length}if(!v||m<0&&v.type!=="comment"&&(v.type!=="string"||o.start.column!==v.value.length+d-1&&v.value.lastIndexOf(s)===v.value.length-1)){if(!h.isSaneInsertion(n,r))return;return{text:s+s,selection:[1,1]}}if(v&&v.type==="string"){var y=f.substring(a.column,a.column+1);if(y==s)return{text:"",selection:[1,1]}}}}),this.add("string_dquotes","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&(s=='"'||s=="'")){c(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.start.column+1,i.start.column+2);if(u==s)return i.end.column++,i}})};h.isSaneInsertion=function(e,t){var n=e.getCursorPosition(),r=new s(t,n.row,n.column);if(!this.$matchTokenType(r.getCurrentToken()||"text",u)){var i=new s(t,n.row,n.column+1);if(!this.$matchTokenType(i.getCurrentToken()||"text",u))return!1}return r.stepForward(),r.getCurrentTokenRow()!==n.row||this.$matchTokenType(r.getCurrentToken()||"text",a)},h.$matchTokenType=function(e,t){return t.indexOf(e.type||e)>-1},h.recordAutoInsert=function(e,t,n){var r=e.getCursorPosition(),i=t.doc.getLine(r.row);this.isAutoInsertedClosing(r,i,f.autoInsertedLineEnd[0])||(f.autoInsertedBrackets=0),f.autoInsertedRow=r.row,f.autoInsertedLineEnd=n+i.substr(r.column),f.autoInsertedBrackets++},h.recordMaybeInsert=function(e,t,n){var r=e.getCursorPosition(),i=t.doc.getLine(r.row);this.isMaybeInsertedClosing(r,i)||(f.maybeInsertedBrackets=0),f.maybeInsertedRow=r.row,f.maybeInsertedLineStart=i.substr(0,r.column)+n,f.maybeInsertedLineEnd=i.substr(r.column),f.maybeInsertedBrackets++},h.isAutoInsertedClosing=function(e,t,n){return f.autoInsertedBrackets>0&&e.row===f.autoInsertedRow&&n===f.autoInsertedLineEnd[0]&&t.substr(e.column)===f.autoInsertedLineEnd},h.isMaybeInsertedClosing=function(e,t){return f.maybeInsertedBrackets>0&&e.row===f.maybeInsertedRow&&t.substr(e.column)===f.maybeInsertedLineEnd&&t.substr(0,e.column)==f.maybeInsertedLineStart},h.popAutoInsertedClosing=function(){f.autoInsertedLineEnd=f.autoInsertedLineEnd.substr(1),f.autoInsertedBrackets--},h.clearMaybeInsertedClosing=function(){f&&(f.maybeInsertedBrackets=0,f.maybeInsertedRow=-1)},r.inherits(h,i),t.CstyleBehaviour=h}),define("ace/mode/folding/cstyle",["require","exports","module","ace/lib/oop","ace/range","ace/mode/folding/fold_mode"],function(e,t,n){var r=e("../../lib/oop"),i=e("../../range").Range,s=e("./fold_mode").FoldMode,o=t.FoldMode=function(e){e&&(this.foldingStartMarker=new RegExp(this.foldingStartMarker.source.replace(/\|[^|]*?$/,"|"+e.start)),this.foldingStopMarker=new RegExp(this.foldingStopMarker.source.replace(/\|[^|]*?$/,"|"+e.end)))};r.inherits(o,s),function(){this.foldingStartMarker=/(\{|\[)[^\}\]]*$|^\s*(\/\*)/,this.foldingStopMarker=/^[^\[\{]*(\}|\])|^[\s\*]*(\*\/)/,this.getFoldWidgetRange=function(e,t,n,r){var i=e.getLine(n),s=i.match(this.foldingStartMarker);if(s){var o=s.index;if(s[1])return this.openingBracketBlock(e,s[1],n,o);var u=e.getCommentFoldRange(n,o+s[0].length,1);return u&&!u.isMultiLine()&&(r?u=this.getSectionRange(e,n):t!="all"&&(u=null)),u}if(t==="markbegin")return;var s=i.match(this.foldingStopMarker);if(s){var o=s.index+s[0].length;return s[1]?this.closingBracketBlock(e,s[1],n,o):e.getCommentFoldRange(n,o,-1)}},this.getSectionRange=function(e,t){var n=e.getLine(t),r=n.search(/\S/),s=t,o=n.length;t+=1;var u=t,a=e.getLength();while(++tf)break;var l=this.getFoldWidgetRange(e,"all",t);if(l){if(l.start.row<=s)break;if(l.isMultiLine())t=l.end.row;else if(r==f)break}u=t}return new i(s,o,u,e.getLine(u).length)}}.call(o.prototype)}) -------------------------------------------------------------------------------- /public/j/editor/theme-katzenmilch.js: -------------------------------------------------------------------------------- 1 | define("ace/theme/katzenmilch",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!1,t.cssClass="ace-katzenmilch",t.cssText=".ace-katzenmilch .ace_gutter,/* THIS THEME WAS AUTOGENERATED BY Theme.tmpl.css (UUID: ) */.ace-katzenmilch .ace_gutter {background: #e8e8e8;color: #333}.ace-katzenmilch .ace_print-margin {width: 1px;background: #e8e8e8}.ace-katzenmilch {background-color: #f3f2f3;color: rgba(15, 0, 9, 1.0)}.ace-katzenmilch .ace_cursor {border-left: 2px solid #100011}.ace-katzenmilch .ace_overwrite-cursors .ace_cursor {border-left: 0px;border-bottom: 1px solid #100011}.ace-katzenmilch .ace_marker-layer .ace_selection {background: rgba(100, 5, 208, 0.27)}.ace-katzenmilch.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #f3f2f3;border-radius: 2px}.ace-katzenmilch .ace_marker-layer .ace_step {background: rgb(198, 219, 174)}.ace-katzenmilch .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #000000}.ace-katzenmilch .ace_marker-layer .ace_active-line {background: rgb(232, 242, 254)}.ace-katzenmilch .ace_gutter-active-line {background-color: rgb(232, 242, 254)}.ace-katzenmilch .ace_marker-layer .ace_selected-word {border: 1px solid rgba(100, 5, 208, 0.27)}.ace-katzenmilch .ace_fold {background-color: rgba(2, 95, 73, 0.97);border-color: rgba(15, 0, 9, 1.0)}.ace-katzenmilch .ace_keyword {color: #674Aa8;rbackground-color: rgba(163, 170, 216, 0.055)}.ace-katzenmilch .ace_constant.ace_language {color: #7D7e52;rbackground-color: rgba(189, 190, 130, 0.059)}.ace-katzenmilch .ace_constant.ace_numeric {color: rgba(79, 130, 123, 0.93);rbackground-color: rgba(119, 194, 187, 0.059)}.ace-katzenmilch .ace_constant.ace_character,.ace-katzenmilch .ace_constant.ace_other {color: rgba(2, 95, 105, 1.0);rbackground-color: rgba(127, 34, 153, 0.063)}.ace-katzenmilch .ace_support.ace_function {color: #9D7e62;rbackground-color: rgba(189, 190, 130, 0.039)}.ace-katzenmilch .ace_support.ace_class {color: rgba(239, 106, 167, 1.0);rbackground-color: rgba(239, 106, 167, 0.063)}.ace-katzenmilch .ace_storage {color: rgba(123, 92, 191, 1.0);rbackground-color: rgba(139, 93, 223, 0.051)}.ace-katzenmilch .ace_invalid {color: #DFDFD5;rbackground-color: #CC1B27}.ace-katzenmilch .ace_string {color: #5a5f9b;rbackground-color: rgba(170, 175, 219, 0.035)}.ace-katzenmilch .ace_comment {font-style: italic;color: rgba(64, 79, 80, 0.67);rbackground-color: rgba(95, 15, 255, 0.0078)}.ace-katzenmilch .ace_entity.ace_name.ace_function,.ace-katzenmilch .ace_variable {color: rgba(2, 95, 73, 0.97);rbackground-color: rgba(34, 255, 73, 0.12)}.ace-katzenmilch .ace_variable.ace_language {color: #316fcf;rbackground-color: rgba(58, 175, 255, 0.039)}.ace-katzenmilch .ace_variable.ace_parameter {font-style: italic;color: rgba(51, 150, 159, 0.87);rbackground-color: rgba(5, 214, 249, 0.043)}.ace-katzenmilch .ace_entity.ace_other.ace_attribute-name {color: rgba(73, 70, 194, 0.93);rbackground-color: rgba(73, 134, 194, 0.035)}.ace-katzenmilch .ace_entity.ace_name.ace_tag {color: #3976a2;rbackground-color: rgba(73, 166, 210, 0.039)}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /public/j/main.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | // Query String 4 | let urlParams 5 | (function() { 6 | Clappr.Log.setLevel(Clappr.Log.LEVEL_WARN) 7 | window.onpopstate = function () { 8 | let match, 9 | pl = /\+/g, // Regex for replacing addition symbol with a space 10 | search = /([^&=]+)=?([^&]*)/g, 11 | decode = function (s) { return decodeURIComponent(s.replace(pl, ' ')) }, 12 | query = window.location.search.substring(1) 13 | 14 | urlParams = {} 15 | while (match = search.exec(query)) 16 | urlParams[decode(match[1])] = decode(match[2]) 17 | } 18 | window.onpopstate() 19 | })() 20 | 21 | // Parser 22 | const Parser = function(output) { 23 | this.output = output 24 | this.console = $('#console') 25 | this.context = document 26 | }; 27 | 28 | Parser.prototype = { 29 | parse: function(code) { 30 | try { 31 | let old = player 32 | eval(code) 33 | old && old.destroy() 34 | window.player = player 35 | this.console.empty() 36 | } catch(err) { 37 | this.console.html(err.message) 38 | } 39 | } 40 | }; 41 | 42 | $(document).ready(function() { 43 | let parser = new Parser($('#output')) 44 | let load = function (fn) { 45 | if (window.clappr.externals.length > 0) { 46 | let lastScript = window.clappr.externals.length 47 | window.clappr.externals.forEach(function (url, index) { 48 | let script = document.createElement('script') 49 | 50 | script.setAttribute('type', 'text/javascript') 51 | script.setAttribute('src', url) 52 | if (index === (lastScript - 1)) script.onload = fn 53 | script.onerror = function (e) { alert('we cant load ' + url + ': e' + e) } 54 | 55 | document.body.appendChild(script) 56 | }) 57 | } else { 58 | fn() 59 | } 60 | } 61 | $('.run').click(function() { 62 | const code = ace.edit('editor').getSession().getValue() 63 | load(function () { parser.parse(code) }) 64 | }) 65 | }) 66 | 67 | // Editor 68 | window.onload = function() { 69 | const editor = ace.edit('editor') 70 | const session = editor.getSession() 71 | 72 | editor.setTheme('ace/theme/katzenmilch') 73 | editor.$blockScrolling = Infinity 74 | session.setMode('ace/mode/javascript') 75 | session.setTabSize(2) 76 | session.setUseSoftTabs(true) 77 | editor.commands.addCommand({ 78 | name: 'run', 79 | bindKey: {mac: 'Command-Enter'}, 80 | exec: function(editor) { 81 | document.querySelector('.run').click() 82 | }, 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /public/stylesheets/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.1.1 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | .btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:active,.btn.active{background-image:none}.btn-default{background-image:-webkit-linear-gradient(top, #fff 0, #e0e0e0 100%);background-image:linear-gradient(to bottom, #fff 0, #e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#dbdbdb;text-shadow:0 1px 0 #fff;border-color:#ccc}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-primary{background-image:-webkit-linear-gradient(top, #428bca 0, #2d6ca2 100%);background-image:linear-gradient(to bottom, #428bca 0, #2d6ca2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#2b669a}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-success{background-image:-webkit-linear-gradient(top, #5cb85c 0, #419641 100%);background-image:linear-gradient(to bottom, #5cb85c 0, #419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-info{background-image:-webkit-linear-gradient(top, #5bc0de 0, #2aabd2 100%);background-image:linear-gradient(to bottom, #5bc0de 0, #2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.btn-warning{background-image:-webkit-linear-gradient(top, #f0ad4e 0, #eb9316 100%);background-image:linear-gradient(to bottom, #f0ad4e 0, #eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-danger{background-image:-webkit-linear-gradient(top, #d9534f 0, #c12e2a 100%);background-image:linear-gradient(to bottom, #d9534f 0, #c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-image:-webkit-linear-gradient(top, #f5f5f5 0, #e8e8e8 100%);background-image:linear-gradient(to bottom, #f5f5f5 0, #e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-color:#e8e8e8}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-image:-webkit-linear-gradient(top, #428bca 0, #357ebd 100%);background-image:linear-gradient(to bottom, #428bca 0, #357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-color:#357ebd}.navbar-default{background-image:-webkit-linear-gradient(top, #fff 0, #f8f8f8 100%);background-image:linear-gradient(to bottom, #fff 0, #f8f8f8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075)}.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top, #ebebeb 0, #f3f3f3 100%);background-image:linear-gradient(to bottom, #ebebeb 0, #f3f3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.075);box-shadow:inset 0 3px 9px rgba(0,0,0,0.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,0.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top, #3c3c3c 0, #222 100%);background-image:linear-gradient(to bottom, #3c3c3c 0, #222 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top, #222 0, #282828 100%);background-image:linear-gradient(to bottom, #222 0, #282828 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.25);box-shadow:inset 0 3px 9px rgba(0,0,0,0.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05)}.alert-success{background-image:-webkit-linear-gradient(top, #dff0d8 0, #c8e5bc 100%);background-image:linear-gradient(to bottom, #dff0d8 0, #c8e5bc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top, #d9edf7 0, #b9def0 100%);background-image:linear-gradient(to bottom, #d9edf7 0, #b9def0 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top, #fcf8e3 0, #f8efc0 100%);background-image:linear-gradient(to bottom, #fcf8e3 0, #f8efc0 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top, #f2dede 0, #e7c3c3 100%);background-image:linear-gradient(to bottom, #f2dede 0, #e7c3c3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top, #ebebeb 0, #f5f5f5 100%);background-image:linear-gradient(to bottom, #ebebeb 0, #f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0)}.progress-bar{background-image:-webkit-linear-gradient(top, #428bca 0, #3071a9 100%);background-image:linear-gradient(to bottom, #428bca 0, #3071a9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0)}.progress-bar-success{background-image:-webkit-linear-gradient(top, #5cb85c 0, #449d44 100%);background-image:linear-gradient(to bottom, #5cb85c 0, #449d44 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0)}.progress-bar-info{background-image:-webkit-linear-gradient(top, #5bc0de 0, #31b0d5 100%);background-image:linear-gradient(to bottom, #5bc0de 0, #31b0d5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0)}.progress-bar-warning{background-image:-webkit-linear-gradient(top, #f0ad4e 0, #ec971f 100%);background-image:linear-gradient(to bottom, #f0ad4e 0, #ec971f 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0)}.progress-bar-danger{background-image:-webkit-linear-gradient(top, #d9534f 0, #c9302c 100%);background-image:linear-gradient(to bottom, #d9534f 0, #c9302c 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-linear-gradient(top, #428bca 0, #3278b3 100%);background-image:linear-gradient(to bottom, #428bca 0, #3278b3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);border-color:#3278b3}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top, #f5f5f5 0, #e8e8e8 100%);background-image:linear-gradient(to bottom, #f5f5f5 0, #e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0)}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top, #428bca 0, #357ebd 100%);background-image:linear-gradient(to bottom, #428bca 0, #357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0)}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top, #dff0d8 0, #d0e9c6 100%);background-image:linear-gradient(to bottom, #dff0d8 0, #d0e9c6 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0)}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top, #d9edf7 0, #c4e3f3 100%);background-image:linear-gradient(to bottom, #d9edf7 0, #c4e3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0)}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top, #fcf8e3 0, #faf2cc 100%);background-image:linear-gradient(to bottom, #fcf8e3 0, #faf2cc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0)}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top, #f2dede 0, #ebcccc 100%);background-image:linear-gradient(to bottom, #f2dede 0, #ebcccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0)}.well{background-image:-webkit-linear-gradient(top, #e8e8e8 0, #f5f5f5 100%);background-image:linear-gradient(to bottom, #e8e8e8 0, #f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1)} -------------------------------------------------------------------------------- /public/stylesheets/bootstrap.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.1.1 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | /*! normalize.css v3.0.0 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;pointer-events:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{color:#333;background-color:#ebebeb;border-color:#adadad}.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{color:#fff;background-color:#3276b1;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{color:#fff;background-color:#47a447;border-color:#398439}.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{color:#fff;background-color:#39b3d7;border-color:#269abc}.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{color:#fff;background-color:#ed9c28;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{color:#fff;background-color:#d2322d;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#428bca;font-weight:normal;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%;padding-left:0;padding-right:0}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%} -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | .header { 7 | position: relative; 8 | margin: 0; 9 | right: 0; 10 | left: 0; 11 | padding: 0 4%; 12 | top: 0; 13 | background-color: rgba(116, 116, 116, 1); 14 | color: #eee; 15 | height: 50px; 16 | line-height: 50px; 17 | width: 100%; 18 | } 19 | 20 | .header span { 21 | font-size: 30px; 22 | font-weight: bold; 23 | } 24 | 25 | .header img { 26 | height: 45px; 27 | } 28 | 29 | .header ul { 30 | right: 7%; 31 | } 32 | 33 | .header ul, .header li, .header a { 34 | position: relative; 35 | display: inline; 36 | float: right; 37 | color: #ddd; 38 | margin: 0; 39 | padding: 0; 40 | outline: none; 41 | } 42 | 43 | a:hover { 44 | color: #aaa; 45 | text-decoration: none; 46 | } 47 | 48 | .header a:visited, a:active, a:link { 49 | color: #ddd; 50 | text-decoration: none; 51 | } 52 | 53 | .run { 54 | display: block; 55 | float: right; 56 | margin: 20px 0; 57 | } 58 | 59 | .container { 60 | text-align: center; 61 | } 62 | 63 | .main { 64 | display: inline-block; 65 | margin: 0; 66 | padding: 0px 20px 0 20px; 67 | border: 0; 68 | } 69 | 70 | .external-js { 71 | border-style: solid; 72 | border-width: 1px; 73 | color: #dcdcdc; 74 | font-size: 10px; 75 | padding: 2px; 76 | margin-right: 2px; 77 | font-family: monospace; 78 | } 79 | 80 | #player-wrapper { 81 | min-width: 320px; 82 | min-height: 180px; 83 | } 84 | 85 | .sidebar { 86 | display: inline-block; 87 | text-align: left; 88 | width: 680px; 89 | margin: 0; 90 | padding: 40px 20px 0 20px; 91 | border: 0; 92 | } 93 | 94 | #editor { 95 | border: 1px solid #c0c0EE; 96 | min-height: 360px; 97 | } 98 | 99 | #console { 100 | position: relative; 101 | color: red; 102 | left: 2%; 103 | top: 65px; 104 | } 105 | 106 | .btn:focus { outline: none; } 107 | 108 | .player { 109 | display: inline-block; 110 | margin: 0 auto; 111 | height: auto; 112 | width: auto; 113 | } 114 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs' 2 | import resolve from '@rollup/plugin-node-resolve' 3 | import replace from '@rollup/plugin-replace' 4 | import babel from '@rollup/plugin-babel' 5 | import filesize from 'rollup-plugin-filesize' 6 | import livereload from 'rollup-plugin-livereload' 7 | import serve from 'rollup-plugin-serve' 8 | import size from 'rollup-plugin-sizes' 9 | import { terser } from 'rollup-plugin-terser' 10 | import visualize from 'rollup-plugin-visualizer' 11 | import { version as clapprCoreVersion } from '@clappr/core/package.json' 12 | import pkg from './package.json' 13 | 14 | let rollupConfig 15 | 16 | const serveLocal = !!process.env.SERVE 17 | const reloadEnabled = !!process.env.RELOAD 18 | const analyzeBundle = !!process.env.ANALYZE_BUNDLE 19 | const minimize = !!process.env.MINIMIZE 20 | 21 | const babelOptionsPlugins = { exclude: 'node_modules/**', babelHelpers: 'bundled' } 22 | const servePluginOptions = { contentBase: ['dist', 'public'], host: '0.0.0.0', port: '8080' } 23 | const livereloadPluginOptions = { watch: ['dist', 'public'] } 24 | const replacePluginOptions = { CLAPPR_CORE_VERSION: JSON.stringify(clapprCoreVersion), preventAssignment: false } 25 | 26 | let plugins = [ 27 | replace(replacePluginOptions), 28 | resolve(), 29 | commonjs(), 30 | babel(babelOptionsPlugins), 31 | size(), 32 | filesize(), 33 | ] 34 | 35 | serveLocal && (plugins = [...plugins, replace({ ...replacePluginOptions, 'process.env.NODE_ENV': JSON.stringify('development') }), serve(servePluginOptions)]) 36 | reloadEnabled && (plugins = [...plugins, livereload(livereloadPluginOptions)]) 37 | analyzeBundle && plugins.push(visualize({ open: true })) 38 | 39 | const mainBundle = { 40 | external: ['@clappr/core'], 41 | input: 'src/hls.js', 42 | output: { 43 | name: 'HlsjsPlayback', 44 | file: pkg.main, 45 | format: 'umd', 46 | globals: { '@clappr/core': 'Clappr' }, 47 | }, 48 | plugins, 49 | } 50 | 51 | const mainBundleWithoutHLS = { 52 | external: ['@clappr/core', 'hls.js'], 53 | input: 'src/hls.js', 54 | output: { 55 | name: 'HlsjsPlayback', 56 | file: 'dist/hlsjs-playback.external.js', 57 | format: 'umd', 58 | globals: { '@clappr/core': 'Clappr', 'hls.js': 'Hls' }, 59 | }, 60 | plugins, 61 | } 62 | 63 | const mainBundleMinified = { 64 | input: 'src/hls.js', 65 | output: { 66 | name: 'HlsjsPlayback', 67 | file: 'dist/hlsjs-playback.min.js', 68 | format: 'iife', 69 | sourcemap: true, 70 | plugins: terser(), 71 | }, 72 | plugins, 73 | } 74 | 75 | const mainBundleWithoutHLSMinified = { 76 | external: ['@clappr/core', 'hls.js'], 77 | input: 'src/hls.js', 78 | output: { 79 | name: 'HlsjsPlayback', 80 | file: 'dist/hlsjs-playback.external.min.js', 81 | globals: { '@clappr/core': 'Clappr', 'hls.js': 'Hls' }, 82 | format: 'iife', 83 | sourcemap: true, 84 | plugins: terser(), 85 | }, 86 | plugins, 87 | } 88 | 89 | const moduleBundle = { 90 | external: ['@clappr/core'], 91 | input: 'src/hls.js', 92 | output: { 93 | name: 'HlsjsPlayback', 94 | file: pkg.module, 95 | format: 'esm', 96 | globals: { '@clappr/core': 'Clappr' }, 97 | }, 98 | plugins, 99 | } 100 | 101 | rollupConfig = [mainBundle, mainBundleWithoutHLS, moduleBundle] 102 | serveLocal && (rollupConfig = [mainBundle, mainBundleWithoutHLS]) 103 | minimize && rollupConfig.push(mainBundleMinified, mainBundleWithoutHLSMinified) 104 | 105 | export default rollupConfig 106 | -------------------------------------------------------------------------------- /src/hls.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Globo.com Player authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | import { Events, HTML5Video, Log, Playback, PlayerError, Utils } from '@clappr/core' 6 | import HLSJS from 'hls.js' 7 | 8 | const { now, listContainsIgnoreCase } = Utils 9 | const AUTO = -1 10 | const DEFAULT_RECOVER_ATTEMPTS = 16 11 | 12 | Events.register('PLAYBACK_FRAGMENT_CHANGED') 13 | Events.register('PLAYBACK_FRAGMENT_PARSING_METADATA') 14 | 15 | export default class HlsjsPlayback extends HTML5Video { 16 | get name() { return 'hls' } 17 | 18 | get supportedVersion() { return { min: CLAPPR_CORE_VERSION } } 19 | 20 | get levels() { return this._levels || [] } 21 | 22 | get currentLevel() { 23 | if (this._currentLevel === null || this._currentLevel === undefined) 24 | return AUTO 25 | else 26 | return this._currentLevel //0 is a valid level ID 27 | 28 | } 29 | 30 | get isReady() { 31 | return this._isReadyState 32 | } 33 | 34 | set currentLevel(id) { 35 | this._currentLevel = id 36 | this.trigger(Events.PLAYBACK_LEVEL_SWITCH_START) 37 | if (this.options.playback.hlsUseNextLevel) 38 | this._hls.nextLevel = this._currentLevel 39 | else 40 | this._hls.currentLevel = this._currentLevel 41 | } 42 | 43 | get latency() { 44 | return this._hls.latency 45 | } 46 | 47 | get currentProgramDateTime() { 48 | return this._hls.playingDate 49 | } 50 | 51 | get _startTime() { 52 | if (this._playbackType === Playback.LIVE && this._playlistType !== 'EVENT') 53 | return this._extrapolatedStartTime 54 | 55 | return this._playableRegionStartTime 56 | } 57 | 58 | get _now() { 59 | return now() 60 | } 61 | 62 | // the time in the video element which should represent the start of the sliding window 63 | // extrapolated to increase in real time (instead of jumping as the early segments are removed) 64 | get _extrapolatedStartTime() { 65 | if (!this._localStartTimeCorrelation) 66 | return this._playableRegionStartTime 67 | 68 | let corr = this._localStartTimeCorrelation 69 | let timePassed = this._now - corr.local 70 | let extrapolatedWindowStartTime = (corr.remote + timePassed) / 1000 71 | // cap at the end of the extrapolated window duration 72 | return Math.min(extrapolatedWindowStartTime, this._playableRegionStartTime + this._extrapolatedWindowDuration) 73 | } 74 | 75 | // the time in the video element which should represent the end of the content 76 | // extrapolated to increase in real time (instead of jumping as segments are added) 77 | get _extrapolatedEndTime() { 78 | let actualEndTime = this._playableRegionStartTime + this._playableRegionDuration 79 | if (!this._localEndTimeCorrelation) return actualEndTime 80 | const correlation = this._localEndTimeCorrelation 81 | const timePassed = this._now - correlation.local 82 | const extrapolatedEndTime = (correlation.remote + timePassed) / 1000 83 | return Math.max(actualEndTime - this._extrapolatedWindowDuration, Math.min(extrapolatedEndTime, actualEndTime)) 84 | } 85 | 86 | get _duration() { 87 | return this._extrapolatedEndTime - this._startTime 88 | } 89 | 90 | // Returns the duration (seconds) of the window that the extrapolated start time is allowed 91 | // to move in before being capped. 92 | // The extrapolated start time should never reach the cap at the end of the window as the 93 | // window should slide as chunks are removed from the start. 94 | // This also applies to the extrapolated end time in the same way. 95 | // 96 | // If chunks aren't being removed for some reason that the start time will reach and remain fixed at 97 | // playableRegionStartTime + extrapolatedWindowDuration 98 | // 99 | // <-- window duration --> 100 | // I.e playableRegionStartTime |-----------------------| 101 | // | --> . . . 102 | // . --> | --> . . 103 | // . . --> | --> . 104 | // . . . --> | 105 | // . . . . 106 | // extrapolatedStartTime 107 | get _extrapolatedWindowDuration() { 108 | if (this._segmentTargetDuration === null) 109 | return 0 110 | 111 | return this._extrapolatedWindowNumSegments * this._segmentTargetDuration 112 | } 113 | 114 | get bandwidthEstimate() { 115 | return this._hls && this._hls.bandwidthEstimate 116 | } 117 | 118 | get defaultOptions() { 119 | return { preload: true } 120 | } 121 | 122 | get customListeners() { 123 | return this.options.hlsPlayback && this.options.hlsPlayback.customListeners || [] 124 | } 125 | 126 | get sourceMedia() { 127 | return this.options.src 128 | } 129 | 130 | get currentTimestamp() { 131 | if (!this._currentFragment) return null 132 | const startTime = this._currentFragment.programDateTime 133 | const playbackTime = this.el.currentTime 134 | const playTimeOffSet = playbackTime - this._currentFragment.start 135 | const currentTimestampInMs = startTime + playTimeOffSet * 1000 136 | return currentTimestampInMs / 1000 137 | } 138 | 139 | static get HLSJS() { 140 | return HLSJS 141 | } 142 | 143 | constructor(...args) { 144 | super(...args) 145 | this.options.hlsPlayback = { ...this.defaultOptions, ...this.options.hlsPlayback } 146 | this._setInitialState() 147 | } 148 | 149 | _setInitialState() { 150 | this._minDvrSize = typeof (this.options.hlsMinimumDvrSize) === 'undefined' ? 60 : this.options.hlsMinimumDvrSize 151 | // The size of the start time extrapolation window measured as a multiple of segments. 152 | // Should be 2 or higher, or 0 to disable. Should only need to be increased above 2 if more than one segment is 153 | // removed from the start of the playlist at a time. E.g if the playlist is cached for 10 seconds and new chunks are 154 | // added/removed every 5. 155 | this._extrapolatedWindowNumSegments = !this.options.playback || typeof (this.options.playback.extrapolatedWindowNumSegments) === 'undefined' ? 2 : this.options.playback.extrapolatedWindowNumSegments 156 | 157 | this._playbackType = Playback.VOD 158 | this._lastTimeUpdate = { current: 0, total: 0 } 159 | this._lastDuration = null 160 | // for hls streams which have dvr with a sliding window, 161 | // the content at the start of the playlist is removed as new 162 | // content is appended at the end. 163 | // this means the actual playable start time will increase as the 164 | // start content is deleted 165 | // For streams with dvr where the entire recording is kept from the 166 | // beginning this should stay as 0 167 | this._playableRegionStartTime = 0 168 | // {local, remote} remote is the time in the video element that should represent 0 169 | // local is the system time when the 'remote' measurment took place 170 | this._localStartTimeCorrelation = null 171 | // {local, remote} remote is the time in the video element that should represents the end 172 | // local is the system time when the 'remote' measurment took place 173 | this._localEndTimeCorrelation = null 174 | // if content is removed from the beginning then this empty area should 175 | // be ignored. "playableRegionDuration" excludes the empty area 176 | this._playableRegionDuration = 0 177 | // #EXT-X-PROGRAM-DATE-TIME 178 | this._programDateTime = 0 179 | // true when the actual duration is longer than hlsjs's live sync point 180 | // when this is false playableRegionDuration will be the actual duration 181 | // when this is true playableRegionDuration will exclude the time after the sync point 182 | this._durationExcludesAfterLiveSyncPoint = false 183 | // #EXT-X-TARGETDURATION 184 | this._segmentTargetDuration = null 185 | // #EXT-X-PLAYLIST-TYPE 186 | this._playlistType = null 187 | this._recoverAttemptsRemaining = this.options.hlsRecoverAttempts || DEFAULT_RECOVER_ATTEMPTS 188 | } 189 | 190 | _setup() { 191 | this._destroyHLSInstance() 192 | this._createHLSInstance() 193 | this._listenHLSEvents() 194 | this._attachHLSMedia() 195 | } 196 | 197 | _destroyHLSInstance() { 198 | if (!this._hls) return 199 | this._manifestParsed = false 200 | this._ccIsSetup = false 201 | this._ccTracksUpdated = false 202 | this._setInitialState() 203 | this._hls.destroy() 204 | this._hls = null 205 | } 206 | 207 | _createHLSInstance() { 208 | const config = { ...this.options.playback.hlsjsConfig } 209 | this._hls = new HLSJS(config) 210 | } 211 | 212 | _attachHLSMedia() { 213 | if (!this._hls) return 214 | this._hls.attachMedia(this.el) 215 | } 216 | 217 | _listenHLSEvents() { 218 | if (!this._hls) return 219 | this._hls.once(HLSJS.Events.MEDIA_ATTACHED, () => { this.options.hlsPlayback.preload && this._hls.loadSource(this.options.src) }) 220 | this._hls.on(HLSJS.Events.MANIFEST_PARSED, () => this._manifestParsed = true) 221 | this._hls.on(HLSJS.Events.LEVEL_LOADED, (evt, data) => this._updatePlaybackType(evt, data)) 222 | this._hls.on(HLSJS.Events.LEVEL_UPDATED, (evt, data) => this._onLevelUpdated(evt, data)) 223 | this._hls.on(HLSJS.Events.LEVEL_SWITCHING, (evt,data) => this._onLevelSwitch(evt, data)) 224 | this._hls.on(HLSJS.Events.FRAG_CHANGED, (evt, data) => this._onFragmentChanged(evt, data)) 225 | this._hls.on(HLSJS.Events.FRAG_LOADED, (evt, data) => this._onFragmentLoaded(evt, data)) 226 | this._hls.on(HLSJS.Events.FRAG_PARSING_METADATA, (evt, data) => this._onFragmentParsingMetadata(evt, data)) 227 | this._hls.on(HLSJS.Events.ERROR, (evt, data) => this._onHLSJSError(evt, data)) 228 | this._hls.on(HLSJS.Events.SUBTITLE_TRACK_LOADED, (evt, data) => this._onSubtitleLoaded(evt, data)) 229 | this._hls.on(HLSJS.Events.SUBTITLE_TRACKS_UPDATED, () => this._ccTracksUpdated = true) 230 | this.bindCustomListeners() 231 | } 232 | 233 | bindCustomListeners() { 234 | this.customListeners.forEach(item => { 235 | const requestedEventName = item.eventName 236 | const typeOfListener = item.once ? 'once': 'on' 237 | requestedEventName && this._hls[`${typeOfListener}`](requestedEventName, item.callback) 238 | }) 239 | } 240 | 241 | unbindCustomListeners() { 242 | this.customListeners.forEach(item => { 243 | const requestedEventName = item.eventName 244 | requestedEventName && this._hls.off(requestedEventName, item.callback) 245 | }) 246 | } 247 | 248 | _onFragmentParsingMetadata(evt, data) { 249 | this.trigger(Events.Custom.PLAYBACK_FRAGMENT_PARSING_METADATA, { evt, data }) 250 | } 251 | 252 | render() { 253 | this._ready() 254 | return super.render() 255 | } 256 | 257 | _ready() { 258 | if (this._isReadyState) return 259 | !this._hls && this._setup() 260 | this._isReadyState = true 261 | this.trigger(Events.PLAYBACK_READY, this.name) 262 | } 263 | 264 | _recover(evt, data, error) { 265 | if (!this._recoveredDecodingError) { 266 | this._recoveredDecodingError = true 267 | this._hls.recoverMediaError() 268 | } else if (!this._recoveredAudioCodecError) { 269 | this._recoveredAudioCodecError = true 270 | this._hls.swapAudioCodec() 271 | this._hls.recoverMediaError() 272 | } else { 273 | Log.error('hlsjs: failed to recover', { evt, data }) 274 | error.level = PlayerError.Levels.FATAL 275 | const formattedError = this.createError(error) 276 | this.trigger(Events.PLAYBACK_ERROR, formattedError) 277 | this.stop() 278 | } 279 | } 280 | 281 | // override 282 | // this playback manages the src on the video element itself 283 | _setupSrc(srcUrl) {} // eslint-disable-line no-unused-vars 284 | 285 | _startTimeUpdateTimer() { 286 | if (this._timeUpdateTimer) return 287 | this._timeUpdateTimer = setInterval(() => { 288 | this._onDurationChange() 289 | this._onTimeUpdate() 290 | }, 100) 291 | } 292 | 293 | _stopTimeUpdateTimer() { 294 | if (!this._timeUpdateTimer) return 295 | clearInterval(this._timeUpdateTimer) 296 | this._timeUpdateTimer = null 297 | } 298 | 299 | getProgramDateTime() { 300 | return this._programDateTime 301 | } 302 | 303 | // the duration on the video element itself should not be used 304 | // as this does not necesarily represent the duration of the stream 305 | // https://github.com/clappr/clappr/issues/668#issuecomment-157036678 306 | getDuration() { 307 | return this._duration 308 | } 309 | 310 | getCurrentTime() { 311 | // e.g. can be < 0 if user pauses near the start 312 | // eventually they will then be kicked to the end by hlsjs if they run out of buffer 313 | // before the official start time 314 | return Math.max(0, this.el.currentTime - this._startTime) 315 | } 316 | 317 | // the time that "0" now represents relative to when playback started 318 | // for a stream with a sliding window this will increase as content is 319 | // removed from the beginning 320 | getStartTimeOffset() { 321 | return this._startTime 322 | } 323 | 324 | seekPercentage(percentage) { 325 | const seekTo = (percentage > 0) 326 | ? this._duration * (percentage / 100) 327 | : this._duration 328 | this.seek(seekTo) 329 | } 330 | 331 | seek(time) { 332 | if (time < 0) { 333 | Log.warn('Attempt to seek to a negative time. Resetting to live point. Use seekToLivePoint() to seek to the live point.') 334 | time = this.getDuration() 335 | } 336 | // assume live if time within 3 seconds of end of stream 337 | this.dvrEnabled && this._updateDvr(time < this.getDuration()-3) 338 | time += this._startTime 339 | this.el.currentTime = time 340 | } 341 | 342 | seekToLivePoint() { 343 | this.seek(this.getDuration()) 344 | } 345 | 346 | _updateDvr(status) { 347 | this.trigger(Events.PLAYBACK_DVR, status) 348 | this.trigger(Events.PLAYBACK_STATS_ADD, { 'dvr': status }) 349 | } 350 | 351 | _updateSettings() { 352 | if (this._playbackType === Playback.VOD) 353 | this.settings.left = ['playpause', 'position', 'duration'] 354 | else if (this.dvrEnabled) 355 | this.settings.left = ['playpause'] 356 | else 357 | this.settings.left = ['playstop'] 358 | 359 | this.settings.seekEnabled = this.isSeekEnabled() 360 | this.trigger(Events.PLAYBACK_SETTINGSUPDATE) 361 | } 362 | 363 | _onHLSJSError(evt, data) { 364 | const error = { 365 | code: `${data.type}_${data.details}`, 366 | description: `${this.name} error: type: ${data.type}, details: ${data.details}`, 367 | raw: data, 368 | } 369 | let formattedError 370 | if (data.response) error.description += `, response: ${JSON.stringify(data.response)}` 371 | // only report/handle errors if they are fatal 372 | // hlsjs should automatically handle non fatal errors 373 | if (data.fatal) { 374 | if (this._recoverAttemptsRemaining > 0) { 375 | this._recoverAttemptsRemaining -= 1 376 | switch (data.type) { 377 | case HLSJS.ErrorTypes.NETWORK_ERROR: 378 | switch (data.details) { 379 | // The following network errors cannot be recovered with HLS.startLoad() 380 | // For more details, see https://github.com/video-dev/hls.js/blob/master/doc/design.md#error-detection-and-handling 381 | // For "level load" fatal errors, see https://github.com/video-dev/hls.js/issues/1138 382 | case HLSJS.ErrorDetails.MANIFEST_LOAD_ERROR: 383 | case HLSJS.ErrorDetails.MANIFEST_LOAD_TIMEOUT: 384 | case HLSJS.ErrorDetails.MANIFEST_PARSING_ERROR: 385 | case HLSJS.ErrorDetails.LEVEL_LOAD_ERROR: 386 | case HLSJS.ErrorDetails.LEVEL_LOAD_TIMEOUT: 387 | Log.error('hlsjs: unrecoverable network fatal error.', { evt, data }) 388 | formattedError = this.createError(error) 389 | this.trigger(Events.PLAYBACK_ERROR, formattedError) 390 | this.stop() 391 | break 392 | default: 393 | Log.warn('hlsjs: trying to recover from network error.', { evt, data }) 394 | error.level = PlayerError.Levels.WARN 395 | this._hls.startLoad() 396 | break 397 | } 398 | break 399 | case HLSJS.ErrorTypes.MEDIA_ERROR: 400 | Log.warn('hlsjs: trying to recover from media error.', { evt, data }) 401 | error.level = PlayerError.Levels.WARN 402 | this._recover(evt, data, error) 403 | break 404 | default: 405 | Log.error('hlsjs: could not recover from error.', { evt, data }) 406 | formattedError = this.createError(error) 407 | this.trigger(Events.PLAYBACK_ERROR, formattedError) 408 | this.stop() 409 | break 410 | } 411 | } else { 412 | Log.error('hlsjs: could not recover from error after maximum number of attempts.', { evt, data }) 413 | formattedError = this.createError(error) 414 | this.trigger(Events.PLAYBACK_ERROR, formattedError) 415 | this.stop() 416 | } 417 | } else { 418 | // Transforms HLSJS.ErrorDetails.KEY_LOAD_ERROR non-fatal error to 419 | // playback fatal error if triggerFatalErrorOnResourceDenied playback 420 | // option is set. HLSJS.ErrorTypes.KEY_SYSTEM_ERROR are fatal errors 421 | // and therefore already handled. 422 | if (this.options.playback.triggerFatalErrorOnResourceDenied && this._keyIsDenied(data)) { 423 | Log.error('hlsjs: could not load decrypt key.', { evt, data }) 424 | formattedError = this.createError(error) 425 | this.trigger(Events.PLAYBACK_ERROR, formattedError) 426 | this.stop() 427 | return 428 | } 429 | 430 | error.level = PlayerError.Levels.WARN 431 | Log.warn('hlsjs: non-fatal error occurred', { evt, data }) 432 | } 433 | } 434 | 435 | _keyIsDenied(data) { 436 | return data.type === HLSJS.ErrorTypes.NETWORK_ERROR 437 | && data.details === HLSJS.ErrorDetails.KEY_LOAD_ERROR 438 | && data.response 439 | && data.response.code >= 400 440 | } 441 | 442 | _onTimeUpdate() { 443 | const update = { current: this.getCurrentTime(), total: this.getDuration(), firstFragDateTime: this.getProgramDateTime() } 444 | const isSame = this._lastTimeUpdate && ( 445 | update.current === this._lastTimeUpdate.current && 446 | update.total === this._lastTimeUpdate.total) 447 | if (isSame) return 448 | this._lastTimeUpdate = update 449 | this.trigger(Events.PLAYBACK_TIMEUPDATE, update, this.name) 450 | } 451 | 452 | _onDurationChange() { 453 | const duration = this.getDuration() 454 | if (this._lastDuration === duration) return 455 | this._lastDuration = duration 456 | super._onDurationChange() 457 | } 458 | 459 | _onProgress() { 460 | if (!this.el.buffered.length) return 461 | let buffered = [] 462 | let bufferedPos = 0 463 | for (let i = 0; i < this.el.buffered.length; i++) { 464 | buffered = [...buffered, { 465 | // for a stream with sliding window dvr something that is buffered my slide off the start of the timeline 466 | start: Math.max(0, this.el.buffered.start(i) - this._playableRegionStartTime), 467 | end: Math.max(0, this.el.buffered.end(i) - this._playableRegionStartTime) 468 | }] 469 | if (this.el.currentTime >= buffered[i].start && this.el.currentTime <= buffered[i].end) 470 | bufferedPos = i 471 | 472 | } 473 | const progress = { 474 | start: buffered[bufferedPos].start, 475 | current: buffered[bufferedPos].end, 476 | total: this.getDuration() 477 | } 478 | this.trigger(Events.PLAYBACK_PROGRESS, progress, buffered) 479 | } 480 | 481 | load(url) { 482 | this._stopTimeUpdateTimer() 483 | this.options.src = url 484 | this._setup() 485 | } 486 | 487 | play() { 488 | !this._hls && this._setup() 489 | !this._manifestParsed && !this.options.hlsPlayback.preload && this._hls.loadSource(this.options.src) 490 | super.play() 491 | this._startTimeUpdateTimer() 492 | } 493 | 494 | pause() { 495 | if (!this._hls) return 496 | this.el.pause() 497 | if (this.dvrEnabled) this._updateDvr(true) 498 | } 499 | 500 | stop() { 501 | this._stopTimeUpdateTimer() 502 | if (this._hls) super.stop() 503 | this._destroyHLSInstance() 504 | } 505 | 506 | destroy() { 507 | this._stopTimeUpdateTimer() 508 | this._destroyHLSInstance() 509 | super.destroy() 510 | } 511 | 512 | _updatePlaybackType(evt, data) { 513 | this._playbackType = data.details.live ? Playback.LIVE : Playback.VOD 514 | this._onLevelUpdated(evt, data) 515 | // Live stream subtitle tracks detection hack (may not immediately available) 516 | if (this._ccTracksUpdated && this._playbackType === Playback.LIVE && this.hasClosedCaptionsTracks) 517 | this._onSubtitleLoaded() 518 | 519 | } 520 | 521 | _fillLevels() { 522 | this._levels = this._hls.levels.map((level, index) => { 523 | return { id: index, level: level, label: `${level.bitrate/1000}Kbps` } 524 | }) 525 | this.trigger(Events.PLAYBACK_LEVELS_AVAILABLE, this._levels) 526 | } 527 | 528 | _onLevelUpdated(evt, data) { 529 | this._segmentTargetDuration = data.details.targetduration 530 | this._playlistType = data.details.type || null 531 | let startTimeChanged = false 532 | let durationChanged = false 533 | let fragments = data.details.fragments 534 | let previousPlayableRegionStartTime = this._playableRegionStartTime 535 | let previousPlayableRegionDuration = this._playableRegionDuration 536 | if (fragments.length === 0) return 537 | // #EXT-X-PROGRAM-DATE-TIME 538 | if (fragments[0].rawProgramDateTime) 539 | this._programDateTime = fragments[0].rawProgramDateTime 540 | if (this._playableRegionStartTime !== fragments[0].start) { 541 | startTimeChanged = true 542 | this._playableRegionStartTime = fragments[0].start 543 | } 544 | 545 | if (startTimeChanged) { 546 | if (!this._localStartTimeCorrelation) { 547 | // set the correlation to map to middle of the extrapolation window 548 | this._localStartTimeCorrelation = { 549 | local: this._now, 550 | remote: (fragments[0].start + (this._extrapolatedWindowDuration/2)) * 1000 551 | } 552 | } else { 553 | // check if the correlation still works 554 | let corr = this._localStartTimeCorrelation 555 | let timePassed = this._now - corr.local 556 | // this should point to a time within the extrapolation window 557 | let startTime = (corr.remote + timePassed) / 1000 558 | if (startTime < fragments[0].start) { 559 | // our start time is now earlier than the first chunk 560 | // (maybe the chunk was removed early) 561 | // reset correlation so that it sits at the beginning of the first available chunk 562 | this._localStartTimeCorrelation = { 563 | local: this._now, 564 | remote: fragments[0].start * 1000 565 | } 566 | } else if (startTime > previousPlayableRegionStartTime + this._extrapolatedWindowDuration) { 567 | // start time was past the end of the old extrapolation window (so would have been capped) 568 | // see if now that time would be inside the window, and if it would be set the correlation 569 | // so that it resumes from the time it was at at the end of the old window 570 | // update the correlation so that the time starts counting again from the value it's on now 571 | this._localStartTimeCorrelation = { 572 | local: this._now, 573 | remote: Math.max(fragments[0].start, previousPlayableRegionStartTime + this._extrapolatedWindowDuration) * 1000 574 | } 575 | } 576 | } 577 | } 578 | 579 | let newDuration = data.details.totalduration 580 | // if it's a live stream then shorten the duration to remove access 581 | // to the area after hlsjs's live sync point 582 | // seeks to areas after this point sometimes have issues 583 | if (this._playbackType === Playback.LIVE) { 584 | let fragmentTargetDuration = data.details.targetduration 585 | let hlsjsConfig = this.options.playback.hlsjsConfig || {} 586 | let liveSyncDurationCount = hlsjsConfig.liveSyncDurationCount || HLSJS.DefaultConfig.liveSyncDurationCount 587 | let hiddenAreaDuration = fragmentTargetDuration * liveSyncDurationCount 588 | if (hiddenAreaDuration <= newDuration) { 589 | newDuration -= hiddenAreaDuration 590 | this._durationExcludesAfterLiveSyncPoint = true 591 | } else { this._durationExcludesAfterLiveSyncPoint = false } 592 | 593 | } 594 | if (newDuration !== this._playableRegionDuration) { 595 | durationChanged = true 596 | this._playableRegionDuration = newDuration 597 | } 598 | // Note the end time is not the playableRegionDuration 599 | // The end time will always increase even if content is removed from the beginning 600 | let endTime = fragments[0].start + newDuration 601 | let previousEndTime = previousPlayableRegionStartTime + previousPlayableRegionDuration 602 | let endTimeChanged = endTime !== previousEndTime 603 | if (endTimeChanged) { 604 | if (!this._localEndTimeCorrelation) { 605 | // set the correlation to map to the end 606 | this._localEndTimeCorrelation = { 607 | local: this._now, 608 | remote: endTime * 1000 609 | } 610 | } else { 611 | // check if the correlation still works 612 | let corr = this._localEndTimeCorrelation 613 | let timePassed = this._now - corr.local 614 | // this should point to a time within the extrapolation window from the end 615 | let extrapolatedEndTime = (corr.remote + timePassed) / 1000 616 | if (extrapolatedEndTime > endTime) { 617 | this._localEndTimeCorrelation = { 618 | local: this._now, 619 | remote: endTime * 1000 620 | } 621 | } else if (extrapolatedEndTime < endTime - this._extrapolatedWindowDuration) { 622 | // our extrapolated end time is now earlier than the extrapolation window from the actual end time 623 | // (maybe a chunk became available early) 624 | // reset correlation so that it sits at the beginning of the extrapolation window from the end time 625 | this._localEndTimeCorrelation = { 626 | local: this._now, 627 | remote: (endTime - this._extrapolatedWindowDuration) * 1000 628 | } 629 | } else if (extrapolatedEndTime > previousEndTime) { 630 | // end time was past the old end time (so would have been capped) 631 | // set the correlation so that it resumes from the time it was at at the end of the old window 632 | this._localEndTimeCorrelation = { 633 | local: this._now, 634 | remote: previousEndTime * 1000 635 | } 636 | } 637 | } 638 | } 639 | 640 | // now that the values have been updated call any methods that use on them so they get the updated values 641 | // immediately 642 | durationChanged && this._onDurationChange() 643 | startTimeChanged && this._onProgress() 644 | } 645 | 646 | _onFragmentChanged(evt, data) { 647 | this._currentFragment = data.frag 648 | this.trigger(Events.Custom.PLAYBACK_FRAGMENT_CHANGED, data) 649 | } 650 | 651 | _onFragmentLoaded(evt, data) { 652 | this.trigger(Events.PLAYBACK_FRAGMENT_LOADED, data) 653 | } 654 | 655 | _onSubtitleLoaded() { 656 | // This event may be triggered multiple times 657 | // Setup CC only once (disable CC by default) 658 | if (!this._ccIsSetup) { 659 | this.trigger(Events.PLAYBACK_SUBTITLE_AVAILABLE) 660 | const trackId = this._playbackType === Playback.LIVE ? -1 : this.closedCaptionsTrackId 661 | this.closedCaptionsTrackId = trackId 662 | this._ccIsSetup = true 663 | } 664 | } 665 | 666 | _onLevelSwitch(evt, data) { 667 | if (!this.levels.length) this._fillLevels() 668 | this.trigger(Events.PLAYBACK_LEVEL_SWITCH_END) 669 | this.trigger(Events.PLAYBACK_LEVEL_SWITCH, data) 670 | let currentLevel = this._hls.levels[data.level] 671 | if (currentLevel) { 672 | // TODO should highDefinition be private and maybe have a read only accessor if it's used somewhere 673 | this.highDefinition = (currentLevel.height >= 720 || (currentLevel.bitrate / 1000) >= 2000) 674 | this.trigger(Events.PLAYBACK_HIGHDEFINITIONUPDATE, this.highDefinition) 675 | this.trigger(Events.PLAYBACK_BITRATE, { 676 | height: currentLevel.height, 677 | width: currentLevel.width, 678 | bandwidth: currentLevel.bitrate, 679 | bitrate: currentLevel.bitrate, 680 | level: data.level 681 | }) 682 | } 683 | } 684 | 685 | get dvrEnabled() { 686 | // enabled when: 687 | // - the duration does not include content after hlsjs's live sync point 688 | // - the playable region duration is longer than the configured duration to enable dvr after 689 | // - the playback type is LIVE. 690 | return (this._durationExcludesAfterLiveSyncPoint && this._duration >= this._minDvrSize && this.getPlaybackType() === Playback.LIVE) 691 | } 692 | 693 | getPlaybackType() { 694 | return this._playbackType 695 | } 696 | 697 | isSeekEnabled() { 698 | return (this._playbackType === Playback.VOD || this.dvrEnabled) 699 | } 700 | } 701 | 702 | HlsjsPlayback.canPlay = function(resource, mimeType) { 703 | const resourceParts = resource.split('?')[0].match(/.*\.(.*)$/) || [] 704 | const isHls = ((resourceParts.length > 1 && resourceParts[1].toLowerCase() === 'm3u8') || listContainsIgnoreCase(mimeType, ['application/vnd.apple.mpegurl', 'application/x-mpegURL'])) 705 | return !!(HLSJS.isSupported() && isHls) 706 | } 707 | -------------------------------------------------------------------------------- /src/hls.test.js: -------------------------------------------------------------------------------- 1 | import { Core, Events } from '@clappr/core' 2 | import HlsjsPlayback from './hls.js' 3 | import HLSJS from 'hls.js' 4 | 5 | const simplePlaybackMock = new HlsjsPlayback({ src: 'http://clappr.io/video.m3u8' }) 6 | 7 | describe('HlsjsPlayback', () => { 8 | test('have a getter called defaultOptions', () => { 9 | expect(Object.getOwnPropertyDescriptor(Object.getPrototypeOf(simplePlaybackMock), 'defaultOptions').get).toBeTruthy() 10 | }) 11 | 12 | test('defaultOptions getter returns all the default options values into one object', () => { 13 | expect(simplePlaybackMock.defaultOptions).toEqual({ preload: true }) 14 | }) 15 | 16 | test('have a getter called customListeners', () => { 17 | expect(Object.getOwnPropertyDescriptor(Object.getPrototypeOf(simplePlaybackMock), 'customListeners').get).toBeTruthy() 18 | }) 19 | 20 | test('customListeners getter returns all configured custom listeners for each hls.js event', () => { 21 | const cb = () => {} 22 | const playback = new HlsjsPlayback({ 23 | src: 'http://clappr.io/foo.m3u8', 24 | hlsPlayback: { 25 | customListeners: [{ eventName: 'hlsMediaAttaching', callback: cb }] 26 | } 27 | }) 28 | expect(playback.customListeners).toEqual(playback.options.hlsPlayback.customListeners) 29 | }) 30 | 31 | test('should be able to identify it can play resources independently of the file extension case', () => { 32 | jest.spyOn(HLSJS, 'isSupported').mockImplementation(() => true) 33 | expect(HlsjsPlayback.canPlay('/relative/video.m3u8')).toBeTruthy() 34 | expect(HlsjsPlayback.canPlay('/relative/VIDEO.M3U8')).toBeTruthy() 35 | expect(HlsjsPlayback.canPlay('/relative/video.m3u8?foobarQuery=1234#somefragment')).toBeTruthy() 36 | expect(HlsjsPlayback.canPlay('whatever_no_extension?foobarQuery=1234#somefragment', 'application/x-mpegURL' )).toBeTruthy() 37 | expect(HlsjsPlayback.canPlay('//whatever_no_extension?foobarQuery=1234#somefragment', 'application/x-mpegURL' )).toBeTruthy() 38 | }) 39 | 40 | test('can play regardless of any mime type letter case', () => { 41 | jest.spyOn(HLSJS, 'isSupported').mockImplementation(() => true) 42 | expect(HlsjsPlayback.canPlay('/path/list.m3u8', 'APPLICATION/VND.APPLE.MPEGURL' )).toBeTruthy() 43 | expect(HlsjsPlayback.canPlay('whatever_no_extension?foobarQuery=1234#somefragment', 'application/x-mpegurl' )).toBeTruthy() 44 | }) 45 | 46 | test('should ensure it does not create an audio tag if audioOnly is not set', () => { 47 | let options = { src: 'http://clappr.io/video.m3u8' }, 48 | playback = new HlsjsPlayback(options) 49 | expect(playback.tagName).toEqual('video') 50 | options = { src: 'http://clappr.io/video.m3u8', mimeType: 'application/x-mpegurl' } 51 | playback = new HlsjsPlayback(options) 52 | expect(playback.tagName).toEqual('video') 53 | }) 54 | 55 | test('should play on an audio tag if audioOnly is set', () => { 56 | let options = { src: 'http://clappr.io/video.m3u8', playback: { audioOnly: true } }, 57 | playback = new HlsjsPlayback(options) 58 | expect(playback.tagName).toEqual('audio') 59 | }) 60 | 61 | test('should trigger a playback error if source load failed', () => { 62 | jest.spyOn(window.HTMLMediaElement.prototype, 'play').mockImplementation(() => {}) 63 | let resolveFn = undefined 64 | const promise = new Promise((resolve) => { 65 | resolveFn = resolve 66 | }) 67 | let options = { 68 | src: 'http://clappr.io/notfound.m3u8', 69 | hlsRecoverAttempts: 0, 70 | mute: true 71 | } 72 | 73 | const core = new Core({}) 74 | const playback = new HlsjsPlayback(options, null, core.playerError) 75 | playback.on(Events.PLAYBACK_ERROR, (e) => resolveFn(e)) 76 | playback.play() 77 | 78 | promise.then((e) => { 79 | expect(e.raw.type).toEqual(HLSJS.ErrorTypes.NETWORK_ERROR) 80 | expect(e.raw.details).toEqual(HLSJS.ErrorDetails.MANIFEST_LOAD_ERROR) 81 | }) 82 | }) 83 | 84 | test('registers PLAYBACK_FRAGMENT_CHANGED event', () => { 85 | expect(Events.Custom.PLAYBACK_FRAGMENT_CHANGED).toEqual('playbackFragmentChanged') 86 | }) 87 | 88 | test('registers PLAYBACK_FRAGMENT_PARSING_METADATA event', () => { 89 | expect(Events.Custom.PLAYBACK_FRAGMENT_PARSING_METADATA).toEqual('playbackFragmentParsingMetadata') 90 | }) 91 | 92 | test('levels supports specifying the level', () => { 93 | let playback 94 | const options = { src: 'http://clappr.io/foo.m3u8' } 95 | playback = new HlsjsPlayback(options) 96 | playback._setup() 97 | // NOTE: rather than trying to call playback.setupHls, we'll punch a new one in place 98 | playback._hls = { levels: [] } 99 | playback._fillLevels() 100 | 101 | // AUTO by default (-1) 102 | expect(playback.currentLevel).toEqual(-1) 103 | 104 | // Supports other level specification. Should keep track of it 105 | // on itself and by proxy on the HLS.js object. 106 | playback.currentLevel = 0 107 | expect(playback.currentLevel).toEqual(0) 108 | expect(playback._hls.currentLevel).toEqual(0) 109 | playback.currentLevel = 1 110 | expect(playback.currentLevel).toEqual(1) 111 | expect(playback._hls.currentLevel).toEqual(1) 112 | }) 113 | 114 | describe('constructor', () => { 115 | test('should use hlsjsConfig from playback options', () => { 116 | const options = { 117 | src: 'http://clappr.io/video.m3u8', 118 | playback: { 119 | hlsMinimumDvrSize: 1, 120 | hlsjsConfig: { 121 | someHlsjsOption: 'value' 122 | } 123 | } 124 | } 125 | const playback = new HlsjsPlayback(options) 126 | playback._setup() 127 | expect(playback._hls.config.someHlsjsOption).toEqual('value') 128 | }) 129 | 130 | test('should use hlsjsConfig from player options as fallback', () => { 131 | const options = { 132 | src: 'http://clappr.io/video.m3u8', 133 | hlsMinimumDvrSize: 1, 134 | hlsjsConfig: { 135 | someHlsjsOption: 'value' 136 | } 137 | } 138 | const playback = new HlsjsPlayback(options) 139 | playback._setup() 140 | expect(playback._hls.config.someHlsjsOption).toEqual('value') 141 | }) 142 | 143 | test('merges defaultOptions with received options.hlsPlayback', () => { 144 | const options = { 145 | src: 'http://clappr.io/foo.m3u8', 146 | hlsPlayback: { foo: 'bar' }, 147 | } 148 | const playback = new HlsjsPlayback(options) 149 | expect(playback.options.hlsPlayback).toEqual({ ...options.hlsPlayback, ...playback.defaultOptions }) 150 | }) 151 | }) 152 | 153 | describe('_setup method', () => { 154 | test('sets _manifestParsed flag to false', () => { 155 | const playback = new HlsjsPlayback({ src: 'http://clappr.io/foo.m3u8' }) 156 | expect(playback._manifestParsed).toBeUndefined() 157 | 158 | playback._setup() 159 | 160 | expect(playback._manifestParsed).toBeFalsy() 161 | }) 162 | 163 | test('calls this._hls.loadSource when MEDIA_ATTACHED event is triggered and hlsPlayback.preload is true', () => { 164 | const playback = new HlsjsPlayback({ src: 'http://clappr.io/foo.m3u8', hlsPlayback: { preload: false } }) 165 | playback._setup() 166 | jest.spyOn(playback._hls, 'loadSource') 167 | playback._hls.trigger(HLSJS.Events.MEDIA_ATTACHED, { media: playback.el }) 168 | 169 | expect(playback._hls.loadSource).not.toHaveBeenCalled() 170 | 171 | playback.options.hlsPlayback.preload = true 172 | playback._setup() 173 | jest.spyOn(playback._hls, 'loadSource') 174 | playback._hls.trigger(HLSJS.Events.MEDIA_ATTACHED, { media: playback.el }) 175 | 176 | expect(playback._hls.loadSource).toHaveBeenCalledTimes(1) 177 | }) 178 | 179 | test('updates _manifestParsed flag value to true if MANIFEST_PARSED event is triggered', () => { 180 | const playback = new HlsjsPlayback({ src: 'http://clappr.io/foo.m3u8' }) 181 | 182 | expect(playback._manifestParsed).toBeUndefined() 183 | 184 | playback._setup() 185 | playback._hls.trigger(HLSJS.Events.MANIFEST_PARSED, { levels: [] }) 186 | 187 | expect(playback._manifestParsed).toBeTruthy() 188 | }) 189 | 190 | test('calls bindCustomListeners method', () => { 191 | const playback = new HlsjsPlayback({ src: 'http://clappr.io/foo.m3u8' }) 192 | jest.spyOn(playback, 'bindCustomListeners') 193 | playback._setup() 194 | 195 | expect(playback.bindCustomListeners).toHaveBeenCalledTimes(1) 196 | }) 197 | }) 198 | 199 | describe('_ready method', () => { 200 | test('avoid to run internal logic if _isReadyState flag is true', () => { 201 | const playback = new HlsjsPlayback({ src: 'http://clappr.io/video.m3u8' }) 202 | playback._isReadyState = true 203 | jest.spyOn(playback, '_setup') 204 | playback._ready() 205 | 206 | expect(playback._setup).not.toHaveBeenCalled() 207 | }) 208 | 209 | test('call _setup method if HLS.JS internal don\'t exists', () => { 210 | const playback = new HlsjsPlayback({ src: 'http://clappr.io/video.m3u8' }) 211 | jest.spyOn(playback, '_setup') 212 | playback._ready() 213 | 214 | expect(playback._setup).toHaveBeenCalledTimes(1) 215 | 216 | playback._ready() 217 | expect(playback._setup).toHaveBeenCalledTimes(1) 218 | }) 219 | 220 | test('update _isReadyState flag value to true', () => { 221 | const playback = new HlsjsPlayback({ src: 'http://clappr.io/video.m3u8' }) 222 | 223 | expect(playback._isReadyState).toBeFalsy() 224 | 225 | playback._ready() 226 | 227 | expect(playback._isReadyState).toBeTruthy() 228 | }) 229 | 230 | test('triggers PLAYBACK_READY event', done => { 231 | const cb = jest.fn() 232 | const playback = new HlsjsPlayback({ src: 'http://clappr.io/video.m3u8' }) 233 | 234 | playback.listenTo(playback, Events.PLAYBACK_READY, cb) 235 | playback.listenTo(playback, Events.PLAYBACK_READY, () => { 236 | expect(cb).toHaveBeenCalledTimes(1) 237 | done() 238 | }) 239 | playback._ready() 240 | }) 241 | }) 242 | 243 | describe('play method', () => { 244 | test('calls this._hls.loadSource if _manifestParsed flag and options.hlsPlayback.preload are falsy', () => { 245 | const playback = new HlsjsPlayback({ src: 'http://clappr.io/foo.m3u8', hlsPlayback: { preload: true } }) 246 | playback._setup() 247 | jest.spyOn(playback._hls, 'loadSource') 248 | playback.play() 249 | 250 | expect(playback._hls.loadSource).not.toHaveBeenCalled() 251 | 252 | playback.options.hlsPlayback.preload = false 253 | playback._manifestParsed = true 254 | playback.play() 255 | 256 | expect(playback._hls.loadSource).not.toHaveBeenCalled() 257 | 258 | playback._manifestParsed = false 259 | playback.play() 260 | 261 | expect(playback._hls.loadSource).toHaveBeenCalledTimes(1) 262 | }) 263 | }) 264 | 265 | describe('load method', () => { 266 | test('loads a new source when called', () => { 267 | const playback = new HlsjsPlayback({ src: 'http://clappr.io/foo.m3u8', hlsPlayback: { preload: true } }) 268 | const url = 'http://clappr.io/foo2.m3u8' 269 | playback.load(url) 270 | jest.spyOn(playback._hls, 'loadSource') 271 | playback._hls.trigger(HLSJS.Events.MEDIA_ATTACHED, { media: playback.el }) 272 | expect(playback.options.src).toBe(url) 273 | expect(playback._hls.loadSource).toHaveBeenCalledWith(url) 274 | }) 275 | }) 276 | 277 | describe('bindCustomListeners method', () => { 278 | test('creates listeners for each item configured on customListeners array', () => { 279 | const cb = jest.fn() 280 | const playback = new HlsjsPlayback({ 281 | src: 'http://clappr.io/foo.m3u8', 282 | hlsPlayback: { 283 | customListeners: [{ eventName: HLSJS.Events.MEDIA_ATTACHING, callback: cb }] 284 | } 285 | }) 286 | playback._setup() 287 | 288 | expect(cb).toHaveBeenCalledTimes(1) 289 | 290 | playback._hls.trigger(HLSJS.Events.MEDIA_ATTACHING, { media: playback.el }) 291 | 292 | expect(cb).toHaveBeenCalledTimes(2) 293 | }) 294 | 295 | test('don\'t add one listener without a valid configuration', () => { 296 | const cb = jest.fn() 297 | const playback = new HlsjsPlayback({ src: 'http://clappr.io/foo.m3u8' }) 298 | playback._setup() 299 | 300 | expect(cb).not.toHaveBeenCalled() 301 | 302 | playback.options.hlsPlayback = {} 303 | 304 | expect(cb).not.toHaveBeenCalled() 305 | 306 | playback.options.hlsPlayback.customListeners = [] 307 | 308 | expect(cb).not.toHaveBeenCalled() 309 | 310 | playback.options.hlsPlayback.customListeners.push([{ eventName: 'invalid_name', callback: cb }]) 311 | 312 | expect(cb).not.toHaveBeenCalled() 313 | }) 314 | 315 | test('adds a listener for one time when the customListeners array item is configured with the "once" param', () => { 316 | const cb = jest.fn() 317 | const playback = new HlsjsPlayback({ 318 | src: 'http://clappr.io/foo.m3u8', 319 | hlsPlayback: { 320 | customListeners: [{ eventName: HLSJS.Events.MEDIA_ATTACHING, callback: cb, once: true }] 321 | } 322 | }) 323 | playback._setup() 324 | 325 | expect(cb).toHaveBeenCalledTimes(1) 326 | 327 | playback._hls.trigger(HLSJS.Events.MEDIA_ATTACHING) 328 | 329 | expect(cb).toHaveBeenCalledTimes(1) 330 | }) 331 | }) 332 | 333 | describe('unbindCustomListeners method', () => { 334 | test('remove listeners for each item configured on customListeners array', () => { 335 | const cb = jest.fn() 336 | const playback = new HlsjsPlayback({ 337 | src: 'http://clappr.io/foo.m3u8', 338 | hlsPlayback: { 339 | customListeners: [{ eventName: 'hlsFragLoaded', callback: cb }] 340 | } 341 | }) 342 | playback._setup() 343 | playback.unbindCustomListeners() 344 | playback._hls.trigger(HLSJS.Events.FRAG_LOADED) 345 | 346 | expect(cb).not.toHaveBeenCalled() 347 | }) 348 | }) 349 | 350 | describe('currentTimestamp', () => { 351 | it('returns the fragment time plus the current playback time', () => { 352 | const fragmentMock = { 353 | frag: { 354 | programDateTime: 1556663040000, // 'Tue Apr 30 2019 19:24:00' 355 | start: 0, 356 | } 357 | } 358 | const playback = new HlsjsPlayback({ src: 'http://clappr.io/foo.m3u8' }) 359 | playback.el.currentTime = 5 360 | playback._setup() 361 | playback.unbindCustomListeners() 362 | playback._hls.trigger(HLSJS.Events.FRAG_CHANGED, fragmentMock) 363 | expect(playback.currentTimestamp).toBe(1556663045) // 'Tue Apr 30 2019 19:24:05' 364 | }) 365 | 366 | it('returns null if the playback does not have a fragment', () => { 367 | const playback = new HlsjsPlayback({ src: 'http://clappr.io/foo.m3u8' }) 368 | playback._setup() 369 | expect(playback.currentTimestamp).toBe(null) 370 | }) 371 | }) 372 | }) 373 | --------------------------------------------------------------------------------