├── .eslintignore ├── package.cjs ├── .gitattributes ├── static ├── favicon.ico └── index.html ├── CONTRIBUTING.md ├── .browserslistrc ├── .editorconfig ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── deploy2GhPages.yml │ ├── nodeTestCI.yml │ └── release.yml └── PULL_REQUEST_TEMPLATE.md ├── .babelrc ├── release.config.cjs ├── tasks ├── copyFiles.js └── handlePackageJson.js ├── rollup ├── rollup.config.prod.mjs ├── rollup.config.dev.mjs └── rollup.common.mjs ├── .eslintrc ├── src ├── utils.js ├── theme │ └── style.scss ├── buttonView.js ├── lib │ ├── ajax.js │ ├── fetch.js │ ├── websocket.js │ ├── decoder.js │ ├── canvas2d.js │ ├── mp2-wasm.js │ ├── ajax-progressive.js │ ├── mpeg1-wasm.js │ ├── webaudio.js │ ├── buffer.js │ ├── wasm-module.js │ ├── ts.js │ ├── video-element.js │ ├── webgl.js │ ├── player.js │ ├── mp2.js │ └── wasm │ │ └── WASM_BINARY.js └── index.js ├── .gitignore ├── LICENSE ├── package.json ├── CODE_OF_CONDUCT.md ├── docs └── CHANGELOG.md └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | coverage/* 3 | .release/* 4 | -------------------------------------------------------------------------------- /package.cjs: -------------------------------------------------------------------------------- 1 | module.exports = require('./package.json'); 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycjimmy/jsmpeg-player/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | The repository is released under the MIT license, and follows a standard Github development process, using Github tracker for issues and merging pull requests into main. 3 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | [production staging] 2 | ie >= 10 3 | ie_mob >= 10 4 | ff >= 30 5 | chrome >= 40 6 | safari >= 8 7 | opera >= 23 8 | ios >= 8 9 | android >= 4.4 10 | 11 | [development] 12 | last 2 versions 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: daily 7 | 8 | - package-ecosystem: npm 9 | directory: / 10 | schedule: 11 | interval: daily 12 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "loose": true, 7 | "modules": false 8 | } 9 | ] 10 | ], 11 | "plugins": [ 12 | "@babel/plugin-syntax-dynamic-import", 13 | "@babel/plugin-transform-object-assign" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /release.config.cjs: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 2 | const makeConfig = require('@cycjimmy/config-lib/cjs/semanticRelease/19.x/makeConfigWithPgkRootForLibrary.cjs').default; 3 | const pkg = require('./package.json'); 4 | 5 | module.exports = makeConfig({ 6 | githubOptions: { 7 | assets: [pkg.browser], 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /tasks/copyFiles.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 2 | /* eslint no-console: 0 */ 3 | import path from 'path'; 4 | import fs from 'fs-extra'; 5 | 6 | const { copySync } = fs; 7 | 8 | copySync( 9 | path.resolve('dist'), 10 | path.resolve('.release', 'dist'), 11 | ); 12 | copySync( 13 | path.resolve('README.md'), 14 | path.resolve('.release', 'README.md'), 15 | ); 16 | copySync( 17 | path.resolve('LICENSE'), 18 | path.resolve('.release', 'LICENSE'), 19 | ); 20 | 21 | console.log('copyFiles success!'); 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /rollup/rollup.config.prod.mjs: -------------------------------------------------------------------------------- 1 | /* eslint import/extensions: ["error", "ignorePackages", {"mjs": off}] */ 2 | import pkg from '../package.cjs'; 3 | 4 | import { 5 | banner, input, name, plugins, terserPlugins, 6 | } from './rollup.common.mjs'; 7 | 8 | export default [ 9 | { 10 | input, 11 | output: [ 12 | { file: pkg.main, format: 'cjs', exports: 'default' }, 13 | { file: pkg.module, format: 'es', exports: 'default' }, 14 | ], 15 | plugins, 16 | }, 17 | { 18 | input, 19 | output: { 20 | name, 21 | file: pkg.browser, 22 | format: 'umd', 23 | banner, 24 | exports: 'default', 25 | }, 26 | plugins: [...plugins, terserPlugins], 27 | }, 28 | ]; 29 | -------------------------------------------------------------------------------- /.github/workflows/deploy2GhPages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy To gh-pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v6 14 | 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v5 17 | with: 18 | node-version: 18 19 | 20 | - name: Pre-built 21 | run: | 22 | npm ci 23 | npm run build:deployment 24 | 25 | - name: Deploy to GitHub Pages 26 | uses: JamesIves/github-pages-deploy-action@releases/v4 27 | with: 28 | branch: gh-pages 29 | folder: .publish 30 | token: ${{ secrets.ACCESS_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Type of Change** 2 | 3 | - [ ] feat: A new feature 4 | - [ ] fix: A bug fix 5 | - [ ] docs: Documentation only changes 6 | - [ ] style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 7 | - [ ] refactor: A code change that neither fixes a bug nor adds a feature 8 | - [ ] perf: A code change that improves performance 9 | - [ ] test: Adding missing or correcting existing tests 10 | - [ ] chore: Changes to the build process or auxiliary tools and libraries such as documentation generation 11 | 12 | **Resolves** 13 | - Fixes #[Add issue number here.] 14 | 15 | **Describe Changes** 16 | 17 | _Describe what this Pull Request does_ 18 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "airbnb-base", 4 | "env": { 5 | "browser": true, 6 | "node": true 7 | }, 8 | "rules": { 9 | "import/no-cycle": 0, 10 | "no-param-reassign": 0, 11 | "no-bitwise": [ 12 | "error", 13 | { 14 | "allow": [ 15 | "|", 16 | "|=", 17 | "&", 18 | "<<", 19 | "<<=", 20 | ">>", 21 | ">>=" 22 | ] 23 | } 24 | ], 25 | "no-console": [ 26 | "error", 27 | { 28 | "allow": [ 29 | "warn", 30 | "error" 31 | ] 32 | } 33 | ], 34 | // "line-comment-position": 0, 35 | "no-underscore-dangle": 0, 36 | "no-multi-assign": 0, 37 | "no-plusplus": 0, 38 | "camelcase": 0, 39 | "no-unused-expressions": 0 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import VideoElement from './lib/video-element'; 2 | 3 | export const Now = () => (window.performance ? window.performance.now() / 1000 : Date.now() / 1000); 4 | 5 | export const CreateVideoElements = () => { 6 | const elements = document.querySelectorAll('.jsmpeg'); 7 | for (let i = 0; i < elements.length; i++) { 8 | // eslint-disable-next-line no-new 9 | new VideoElement(elements[i]); 10 | } 11 | }; 12 | 13 | export const Fill = (array, value) => { 14 | if (array.fill) { 15 | array.fill(value); 16 | } else { 17 | for (let i = 0; i < array.length; i++) { 18 | array[i] = value; 19 | } 20 | } 21 | }; 22 | 23 | export const Base64ToArrayBuffer = (base64) => { 24 | const binary = window.atob(base64); 25 | const { length } = binary; 26 | const bytes = new Uint8Array(length); 27 | for (let i = 0; i < length; i++) { 28 | bytes[i] = binary.charCodeAt(i); 29 | } 30 | return bytes.buffer; 31 | }; 32 | -------------------------------------------------------------------------------- /tasks/handlePackageJson.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 2 | /* eslint no-console: 0 */ 3 | import path from 'path'; 4 | import fs from 'fs-extra'; 5 | 6 | const { readJsonSync, outputJsonSync } = fs; 7 | 8 | const jsonData = readJsonSync( 9 | path.resolve('package.json'), 10 | ); 11 | 12 | if (jsonData.scripts) { 13 | delete jsonData.scripts; 14 | } 15 | 16 | if (jsonData.dependencies) { 17 | delete jsonData.dependencies; 18 | } 19 | 20 | if (jsonData.devDependencies) { 21 | delete jsonData.devDependencies; 22 | } 23 | 24 | if (jsonData.publishConfig) { 25 | delete jsonData.publishConfig; 26 | } 27 | 28 | if (jsonData.config) { 29 | delete jsonData.config; 30 | } 31 | 32 | if (jsonData.engines) { 33 | delete jsonData.engines; 34 | } 35 | 36 | outputJsonSync( 37 | path.resolve('.release', 'package.json'), 38 | jsonData, 39 | { 40 | spaces: 2, 41 | }, 42 | ); 43 | 44 | console.log('handlePackageJson success!'); 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | 49 | # IDE 50 | .idea 51 | 52 | # node 53 | node_modules/ 54 | *.log 55 | 56 | # project 57 | .publish/ 58 | dist/ 59 | temp/ 60 | coverage/ 61 | .release/ 62 | -------------------------------------------------------------------------------- /src/theme/style.scss: -------------------------------------------------------------------------------- 1 | @use "~@cycjimmy/sass-lib" as *; 2 | 3 | .canvas, 4 | .poster { 5 | @extend %full-container; 6 | display: block; 7 | } 8 | 9 | .poster { 10 | &.hidden { 11 | display: none; 12 | } 13 | } 14 | 15 | // buttons 16 | %button-common { 17 | @extend %full-container; 18 | 19 | opacity: .7; 20 | cursor: pointer; 21 | -webkit-tap-highlight-color: rgba(255, 0, 0, 0); 22 | 23 | &.hidden { 24 | display: none; 25 | } 26 | } 27 | 28 | .playButton { 29 | @extend %button-common; 30 | @extend %flex-center; 31 | z-index: 10; 32 | 33 | > svg { 34 | width: 12vw; 35 | height: 12vw; 36 | max-width: 60px; 37 | max-height: 60px; 38 | fill: #fff; 39 | } 40 | } 41 | 42 | .unmuteButton { 43 | @extend %button-common; 44 | z-index: 10; 45 | display: flex; 46 | justify-content: flex-end; 47 | align-items: flex-end; 48 | 49 | > svg { 50 | margin: 0 15px 15px 0; 51 | width: 9vw; 52 | height: 9vw; 53 | max-width: 40px; 54 | max-height: 40px; 55 | fill: #fff; 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /src/buttonView.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PLAY_BUTTON HTML 3 | * @type {string} 4 | */ 5 | export const PLAY_BUTTON = ` 6 | 7 | 8 | 9 | `; 10 | 11 | /** 12 | * UNMUTE_BUTTON HTML 13 | * @type {string} 14 | */ 15 | export const UNMUTE_BUTTON = ` 16 | 17 | 18 | 19 | `; 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Online demo** 38 | If applicable, provide an online demo to help explain your problem. 39 | 40 | **Additional context** 41 | Add any other context about the problem here. 42 | -------------------------------------------------------------------------------- /rollup/rollup.config.dev.mjs: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 2 | /* eslint import/extensions: ["error", "ignorePackages", {"mjs": off}] */ 3 | import browsersync from 'rollup-plugin-browsersync'; 4 | import copy from 'rollup-plugin-copy'; 5 | 6 | import pkg from '../package.cjs'; 7 | 8 | import { 9 | input, IS_DEVELOPMENT, IS_DEPLOYMENT, name, plugins, 10 | } from './rollup.common.mjs'; 11 | 12 | export default [ 13 | { 14 | input, 15 | output: { 16 | name, 17 | file: pkg.browser.replace('.min.js', '.js'), 18 | format: 'umd', 19 | exports: 'default', 20 | }, 21 | plugins: [ 22 | ...plugins, 23 | 24 | IS_DEPLOYMENT 25 | && copy({ 26 | hook: 'writeBundle', 27 | targets: [ 28 | { 29 | src: ['static/**/*', 'dist/**.umd.js'], 30 | dest: '.publish', 31 | }, 32 | ], 33 | }), 34 | IS_DEVELOPMENT 35 | && browsersync({ 36 | server: ['static', 'dist'], 37 | watch: true, 38 | }), 39 | ], 40 | }, 41 | ]; 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2022 cycjimmy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/nodeTestCI.yml: -------------------------------------------------------------------------------- 1 | name: Test CI 2 | on: 3 | push: 4 | 5 | pull_request: 6 | branches: 7 | - master 8 | - main 9 | - next 10 | - next-major 11 | - alpha 12 | - beta 13 | 14 | schedule: 15 | - cron: 0 2 * * 0 16 | 17 | jobs: 18 | test: 19 | runs-on: ${{ matrix.os }} 20 | strategy: 21 | matrix: 22 | node: [ 18 ] 23 | os: [ubuntu-latest, windows-latest, macOS-latest] 24 | 25 | steps: 26 | - uses: actions/checkout@v6 27 | - uses: actions/setup-node@v5 28 | with: 29 | node-version: ${{ matrix.node }} 30 | - run: npm ci 31 | - run: npm run package 32 | 33 | - name: Semantic Release Test 34 | uses: cycjimmy/semantic-release-action@v5 35 | with: 36 | dry_run: true 37 | branches: | 38 | [ 39 | 'master', 40 | 'main', 41 | 'next', 42 | 'next-major', 43 | { 44 | name: 'beta', 45 | prerelease: true 46 | }, 47 | { 48 | name: 'alpha', 49 | prerelease: true 50 | } 51 | ] 52 | extra_plugins: | 53 | @semantic-release/git 54 | @semantic-release/changelog 55 | @semantic-release/exec 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 59 | -------------------------------------------------------------------------------- /rollup/rollup.common.mjs: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 2 | /* eslint import/extensions: ["error", "ignorePackages", {"js": off}] */ 3 | import eslint from '@rollup/plugin-eslint'; 4 | import json from '@rollup/plugin-json'; 5 | import resolve from '@rollup/plugin-node-resolve'; 6 | import { babel } from '@rollup/plugin-babel'; 7 | import terser from '@rollup/plugin-terser'; 8 | import postcss from 'rollup-plugin-postcss'; 9 | import autoprefixer from 'autoprefixer'; 10 | 11 | import myBanner from '@cycjimmy/config-lib/esm/chore/myBanner.js'; 12 | import terserOption from '@cycjimmy/config-lib/esm/terser/4.x/production.js'; 13 | 14 | import pkg from '../package.cjs'; 15 | 16 | export const IS_DEVELOPMENT = process.env.NODE_ENV === 'development'; 17 | export const IS_PRODUCTION = process.env.NODE_ENV === 'production'; 18 | export const IS_DEPLOYMENT = process.env.NODE_ENV === 'deployment'; 19 | 20 | export const input = './src/index.js'; 21 | export const name = 'JSMpeg'; 22 | export const banner = myBanner(pkg); 23 | 24 | export const plugins = [ 25 | json(), 26 | postcss({ 27 | modules: { 28 | generateScopedName: IS_PRODUCTION ? '[hash:base64:10]' : '[name]__[local]', 29 | }, 30 | autoModules: false, 31 | minimize: true, 32 | plugins: [autoprefixer], 33 | }), 34 | eslint({ 35 | fix: true, 36 | exclude: [ 37 | 'node_modules/**', 38 | '**/*.(css|scss)', 39 | ], 40 | }), 41 | resolve(), 42 | babel({ babelHelpers: 'bundled' }), 43 | ]; 44 | 45 | export const terserPlugins = IS_PRODUCTION && terser(terserOption); 46 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | JSMpeg Video Demo 7 | 8 | 35 | 36 | 37 | 38 |
39 |
40 | play 41 | pause 42 | stop 43 | destroy 44 |
45 | 46 | 47 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/lib/ajax.js: -------------------------------------------------------------------------------- 1 | /* eslint class-methods-use-this: ["error", { "exceptMethods": ["resume"] }] */ 2 | export default class AjaxSource { 3 | constructor(url, options) { 4 | this.url = url; 5 | this.destination = null; 6 | this.request = null; 7 | this.streaming = false; 8 | 9 | this.completed = false; 10 | this.established = false; 11 | this.progress = 0; 12 | 13 | this.onEstablishedCallback = options.onSourceEstablished; 14 | this.onCompletedCallback = options.onSourceCompleted; 15 | 16 | if (options.hookOnEstablished) { 17 | this.hookOnEstablished = options.hookOnEstablished; 18 | } 19 | } 20 | 21 | connect(destination) { 22 | this.destination = destination; 23 | } 24 | 25 | start() { 26 | this.request = new XMLHttpRequest(); 27 | 28 | // eslint-disable-next-line func-names 29 | this.request.onreadystatechange = function () { 30 | if (this.request.readyState === this.request.DONE && this.request.status === 200) { 31 | this.onLoad(this.request.response); 32 | } 33 | }.bind(this); 34 | 35 | this.request.onprogress = this.onProgress.bind(this); 36 | this.request.open('GET', this.url); 37 | this.request.responseType = 'arraybuffer'; 38 | this.request.send(); 39 | } 40 | 41 | resume() { 42 | // Nothing to do here 43 | } 44 | 45 | destroy() { 46 | this.request.abort(); 47 | } 48 | 49 | onProgress(ev) { 50 | this.progress = ev.loaded / ev.total; 51 | } 52 | 53 | onLoad(data) { 54 | this.established = true; 55 | this.completed = true; 56 | this.progress = 1; 57 | 58 | if (this.hookOnEstablished) { 59 | this.hookOnEstablished(); 60 | } 61 | 62 | if (this.onEstablishedCallback) { 63 | this.onEstablishedCallback(this); 64 | } 65 | if (this.onCompletedCallback) { 66 | this.onCompletedCallback(this); 67 | } 68 | 69 | if (this.destination) { 70 | this.destination.write(data); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/lib/fetch.js: -------------------------------------------------------------------------------- 1 | /* eslint class-methods-use-this: ["error", { "exceptMethods": ["resume"] }] */ 2 | export default class FetchSource { 3 | constructor(url, options) { 4 | this.url = url; 5 | this.destination = null; 6 | this.request = null; 7 | this.streaming = true; 8 | 9 | this.completed = false; 10 | this.established = false; 11 | this.progress = 0; 12 | this.aborted = false; 13 | 14 | this.onEstablishedCallback = options.onSourceEstablished; 15 | this.onCompletedCallback = options.onSourceCompleted; 16 | 17 | if (options.hookOnEstablished) { 18 | this.hookOnEstablished = options.hookOnEstablished; 19 | } 20 | } 21 | 22 | connect(destination) { 23 | this.destination = destination; 24 | } 25 | 26 | start() { 27 | const params = { 28 | method: 'GET', 29 | headers: new Headers(), 30 | cache: 'default', 31 | }; 32 | 33 | // eslint-disable-next-line no-restricted-globals 34 | self 35 | .fetch(this.url, params) 36 | // eslint-disable-next-line consistent-return 37 | .then((res) => { 38 | if (res.ok && res.status >= 200 && res.status <= 299) { 39 | this.progress = 1; 40 | this.established = true; 41 | return this.pump(res.body.getReader()); 42 | } 43 | // error 44 | }) 45 | .catch((err) => { 46 | throw err; 47 | }); 48 | } 49 | 50 | pump(reader) { 51 | return ( 52 | reader 53 | .read() 54 | // eslint-disable-next-line consistent-return 55 | .then((result) => { 56 | if (result.done) { 57 | this.completed = true; 58 | } else { 59 | if (this.aborted) { 60 | return reader.cancel(); 61 | } 62 | 63 | if (this.destination) { 64 | this.destination.write(result.value.buffer); 65 | } 66 | 67 | return this.pump(reader); 68 | } 69 | }) 70 | .catch((err) => { 71 | throw err; 72 | }) 73 | ); 74 | } 75 | 76 | resume() { 77 | // Nothing to do here 78 | } 79 | 80 | abort() { 81 | this.aborted = true; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/lib/websocket.js: -------------------------------------------------------------------------------- 1 | export default class WSSource { 2 | constructor(url, options) { 3 | this.url = url; 4 | this.options = options; 5 | this.socket = null; 6 | this.streaming = true; 7 | 8 | this.callbacks = { connect: [], data: [] }; 9 | this.destination = null; 10 | 11 | this.reconnectInterval = options.reconnectInterval !== undefined 12 | ? options.reconnectInterval 13 | : 5; 14 | this.shouldAttemptReconnect = !!this.reconnectInterval; 15 | 16 | this.completed = false; 17 | this.established = false; 18 | this.progress = 0; 19 | 20 | this.reconnectTimeoutId = 0; 21 | 22 | this.onEstablishedCallback = options.onSourceEstablished; 23 | this.onCompletedCallback = options.onSourceCompleted; // Never used 24 | 25 | if (options.hookOnEstablished) { 26 | this.hookOnEstablished = options.hookOnEstablished; 27 | } 28 | } 29 | 30 | connect(destination) { 31 | this.destination = destination; 32 | } 33 | 34 | destroy() { 35 | clearTimeout(this.reconnectTimeoutId); 36 | this.shouldAttemptReconnect = false; 37 | this.socket.close(); 38 | } 39 | 40 | start() { 41 | this.shouldAttemptReconnect = !!this.reconnectInterval; 42 | this.progress = 0; 43 | this.established = false; 44 | 45 | if (this.options.protocols) { 46 | this.socket = new WebSocket(this.url, this.options.protocols); 47 | } else { 48 | this.socket = new WebSocket(this.url); 49 | } 50 | this.socket.binaryType = 'arraybuffer'; 51 | this.socket.onmessage = this.onMessage.bind(this); 52 | this.socket.onopen = this.onOpen.bind(this); 53 | this.socket.onerror = this.onClose.bind(this); 54 | this.socket.onclose = this.onClose.bind(this); 55 | } 56 | 57 | // eslint-disable-next-line class-methods-use-this 58 | resume() { 59 | // Nothing to do here 60 | } 61 | 62 | onOpen() { 63 | this.progress = 1; 64 | } 65 | 66 | onClose() { 67 | if (this.shouldAttemptReconnect) { 68 | clearTimeout(this.reconnectTimeoutId); 69 | this.reconnectTimeoutId = setTimeout(() => { 70 | this.start(); 71 | }, this.reconnectInterval * 1000); 72 | } 73 | } 74 | 75 | onMessage(ev) { 76 | const isFirstChunk = !this.established; 77 | this.established = true; 78 | 79 | if (isFirstChunk && this.hookOnEstablished) { 80 | this.hookOnEstablished(); 81 | } 82 | 83 | if (isFirstChunk && this.onEstablishedCallback) { 84 | this.onEstablishedCallback(this); 85 | } 86 | 87 | if (this.destination) { 88 | this.destination.write(ev.data); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | - next 9 | - next-major 10 | - alpha 11 | - beta 12 | 13 | jobs: 14 | test: 15 | name: test 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v6 20 | 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v5 23 | with: 24 | node-version: '18' 25 | 26 | - name: Install dependencies 27 | run: npm ci 28 | 29 | - run: npm run package 30 | 31 | release: 32 | name: release 33 | runs-on: ubuntu-latest 34 | needs: test 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v6 38 | 39 | - name: Setup Node.js 40 | uses: actions/setup-node@v5 41 | with: 42 | node-version: 18 43 | 44 | - name: Install dependencies 45 | run: npm ci 46 | 47 | - name: Build package 48 | run: npm run package 49 | 50 | - name: Semantic Release 51 | uses: cycjimmy/semantic-release-action@v5 52 | id: semantic 53 | with: 54 | branches: | 55 | [ 56 | '+([0-9])?(.{+([0-9]),x}).x', 57 | 'master', 58 | 'main', 59 | 'next', 60 | 'next-major', 61 | { 62 | name: 'beta', 63 | prerelease: true 64 | }, 65 | { 66 | name: 'alpha', 67 | prerelease: true 68 | } 69 | ] 70 | extra_plugins: | 71 | @semantic-release/git 72 | @semantic-release/changelog 73 | @semantic-release/exec 74 | env: 75 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 76 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 77 | 78 | - name: Setup Node.js with GitHub Package Registry 79 | uses: actions/setup-node@v5 80 | with: 81 | node-version: 18 82 | registry-url: 'https://npm.pkg.github.com' 83 | scope: 'cycjimmy' 84 | 85 | - name: Publish To GitHub Package Registry 86 | if: steps.semantic.outputs.new_release_published == 'true' 87 | run: npm publish ./.release 88 | env: 89 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 90 | 91 | - name: Push updates to branch for major version 92 | if: steps.semantic.outputs.new_release_published == 'true' 93 | run: git push https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git HEAD:refs/heads/v${{steps.semantic.outputs.new_release_major_version}} 94 | env: 95 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cycjimmy/jsmpeg-player", 3 | "version": "6.1.2", 4 | "description": "MPEG1 Video Player Based On JSMpeg", 5 | "type": "module", 6 | "main": "dist/jsmpeg-player.cjs", 7 | "module": "dist/jsmpeg-player.esm.js", 8 | "browser": "dist/jsmpeg-player.umd.min.js", 9 | "exports": { 10 | "require": "./dist/jsmpeg-player.cjs", 11 | "import": "./dist/jsmpeg-player.esm.js" 12 | }, 13 | "scripts": { 14 | "start": "npm run dev", 15 | "lint": "eslint --ext .js,.cjs,.mjs -c .eslintrc .", 16 | "lint:fix": "eslint --ext .js,.cjs,.mjs --fix -c .eslintrc . --fix", 17 | "dev": "cross-env NODE_ENV=development rollup -c rollup/rollup.config.dev.mjs --watch", 18 | "build": "trash dist && npm run build:prod:umd && npm run build:prod", 19 | "build:prod": "cross-env NODE_ENV=production rollup -c rollup/rollup.config.prod.mjs", 20 | "build:prod:umd": "cross-env NODE_ENV=production rollup -c rollup/rollup.config.dev.mjs", 21 | "build:deployment": "cross-env NODE_ENV=deployment rollup -c rollup/rollup.config.dev.mjs", 22 | "package": "trash .release && npm run build && node tasks/copyFiles.js && node tasks/handlePackageJson.js" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/cycjimmy/jsmpeg-player.git" 27 | }, 28 | "keywords": [ 29 | "jsmpeg", 30 | "TS" 31 | ], 32 | "author": "cycjimmy (https://github.com/cycjimmy)", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/cycjimmy/jsmpeg-player/issues" 36 | }, 37 | "homepage": "https://github.com/cycjimmy/jsmpeg-player#readme", 38 | "engines": { 39 | "node": ">=16" 40 | }, 41 | "publishConfig": { 42 | "access": "public" 43 | }, 44 | "dependencies": { 45 | "@cycjimmy/awesome-js-funcs": "^4.0.9", 46 | "@cycjimmy/sass-lib": "^3.0.1" 47 | }, 48 | "devDependencies": { 49 | "@babel/core": "^7.24.9", 50 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 51 | "@babel/plugin-transform-object-assign": "^7.24.7", 52 | "@babel/preset-env": "^7.24.8", 53 | "@cycjimmy/config-lib": "^3.2.1", 54 | "@rollup/plugin-babel": "^6.0.4", 55 | "@rollup/plugin-eslint": "^9.0.5", 56 | "@rollup/plugin-json": "^6.1.0", 57 | "@rollup/plugin-node-resolve": "^16.0.0", 58 | "@rollup/plugin-terser": "^0.4.4", 59 | "autoprefixer": "^10.4.19", 60 | "cross-env": "^10.0.0", 61 | "eslint": "^8.57.0", 62 | "eslint-config-airbnb-base": "^15.0.0", 63 | "eslint-plugin-import": "^2.29.1", 64 | "fs-extra": "^11.2.0", 65 | "rollup": "^4.18.1", 66 | "rollup-plugin-browsersync": "^1.3.3", 67 | "rollup-plugin-copy": "^3.5.0", 68 | "rollup-plugin-postcss": "^4.0.2", 69 | "sass": "^1.77.8", 70 | "trash-cli": "^6.0.0" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/lib/decoder.js: -------------------------------------------------------------------------------- 1 | /* eslint class-methods-use-this: ["error", { "exceptMethods": ["destroy"] }] */ 2 | export default class BaseDecoder { 3 | constructor(options) { 4 | this.destination = null; 5 | this.canPlay = false; 6 | 7 | this.collectTimestamps = !options.streaming; 8 | this.bytesWritten = 0; 9 | this.timestamps = []; 10 | this.timestampIndex = 0; 11 | 12 | this.startTime = 0; 13 | this.decodedTime = 0; 14 | 15 | Object.defineProperty(this, 'currentTime', { get: this.getCurrentTime }); 16 | } 17 | 18 | destroy() {} 19 | 20 | connect(destination) { 21 | this.destination = destination; 22 | } 23 | 24 | bufferGetIndex() { 25 | return this.bits.index; 26 | } 27 | 28 | bufferSetIndex(index) { 29 | this.bits.index = index; 30 | } 31 | 32 | bufferWrite(buffers) { 33 | return this.bits.write(buffers); 34 | } 35 | 36 | write(pts, buffers) { 37 | if (this.collectTimestamps) { 38 | if (this.timestamps.length === 0) { 39 | this.startTime = pts; 40 | this.decodedTime = pts; 41 | } 42 | this.timestamps.push({ index: this.bytesWritten << 3, time: pts }); 43 | } 44 | 45 | this.bytesWritten += this.bufferWrite(buffers); 46 | this.canPlay = true; 47 | } 48 | 49 | seek(time) { 50 | if (!this.collectTimestamps) { 51 | return; 52 | } 53 | 54 | this.timestampIndex = 0; 55 | for (let i = 0; i < this.timestamps.length; i++) { 56 | if (this.timestamps[i].time > time) { 57 | break; 58 | } 59 | this.timestampIndex = i; 60 | } 61 | 62 | const ts = this.timestamps[this.timestampIndex]; 63 | if (ts) { 64 | this.bufferSetIndex(ts.index); 65 | this.decodedTime = ts.time; 66 | } else { 67 | this.bufferSetIndex(0); 68 | this.decodedTime = this.startTime; 69 | } 70 | } 71 | 72 | decode() { 73 | this.advanceDecodedTime(0); 74 | } 75 | 76 | advanceDecodedTime(seconds) { 77 | if (this.collectTimestamps) { 78 | let newTimestampIndex = -1; 79 | const currentIndex = this.bufferGetIndex(); 80 | for (let i = this.timestampIndex; i < this.timestamps.length; i++) { 81 | if (this.timestamps[i].index > currentIndex) { 82 | break; 83 | } 84 | newTimestampIndex = i; 85 | } 86 | 87 | // Did we find a new PTS, different from the last? If so, we don't have 88 | // to advance the decoded time manually and can instead sync it exactly 89 | // to the PTS. 90 | if (newTimestampIndex !== -1 && newTimestampIndex !== this.timestampIndex) { 91 | this.timestampIndex = newTimestampIndex; 92 | this.decodedTime = this.timestamps[this.timestampIndex].time; 93 | return; 94 | } 95 | } 96 | 97 | this.decodedTime += seconds; 98 | } 99 | 100 | getCurrentTime() { 101 | return this.decodedTime; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at cycjimmy@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/lib/canvas2d.js: -------------------------------------------------------------------------------- 1 | import { Fill } from '../utils'; 2 | 3 | /* eslint class-methods-use-this: ["error", { "exceptMethods": ["destroy"] }] */ 4 | export default class CanvasRenderer { 5 | constructor(options) { 6 | if (options.canvas) { 7 | this.canvas = options.canvas; 8 | this.ownsCanvasElement = false; 9 | } else { 10 | this.canvas = document.createElement('canvas'); 11 | this.ownsCanvasElement = true; 12 | } 13 | this.width = this.canvas.width; 14 | this.height = this.canvas.height; 15 | this.enabled = true; 16 | 17 | this.context = this.canvas.getContext('2d'); 18 | } 19 | 20 | destroy() { 21 | if (this.ownsCanvasElement) { 22 | this.canvas.remove(); 23 | } 24 | } 25 | 26 | resize(width, height) { 27 | this.width = width | 0; 28 | this.height = height | 0; 29 | 30 | this.canvas.width = this.width; 31 | this.canvas.height = this.height; 32 | 33 | this.imageData = this.context.getImageData(0, 0, this.width, this.height); 34 | Fill(this.imageData.data, 255); 35 | } 36 | 37 | renderProgress(progress) { 38 | const w = this.canvas.width; 39 | const h = this.canvas.height; 40 | const ctx = this.context; 41 | 42 | ctx.fillStyle = '#222'; 43 | ctx.fillRect(0, 0, w, h); 44 | ctx.fillStyle = '#fff'; 45 | ctx.fillRect(0, h - h * progress, w, h * progress); 46 | } 47 | 48 | render(y, cb, cr) { 49 | this.YCbCrToRGBA(y, cb, cr, this.imageData.data); 50 | this.context.putImageData(this.imageData, 0, 0); 51 | } 52 | 53 | YCbCrToRGBA(y, cb, cr, rgba) { 54 | if (!this.enabled) { 55 | return; 56 | } 57 | 58 | // Chroma values are the same for each block of 4 pixels, so we proccess 59 | // 2 lines at a time, 2 neighboring pixels each. 60 | // I wish we could use 32bit writes to the RGBA buffer instead of writing 61 | // each byte separately, but we need the automatic clamping of the RGBA 62 | // buffer. 63 | 64 | const w = ((this.width + 15) >> 4) << 4; 65 | const w2 = w >> 1; 66 | 67 | let yIndex1 = 0; 68 | let yIndex2 = w; 69 | const yNext2Lines = w + (w - this.width); 70 | 71 | let cIndex = 0; 72 | const cNextLine = w2 - (this.width >> 1); 73 | 74 | let rgbaIndex1 = 0; 75 | let rgbaIndex2 = this.width * 4; 76 | const rgbaNext2Lines = this.width * 4; 77 | 78 | const cols = this.width >> 1; 79 | const rows = this.height >> 1; 80 | 81 | let ccb; 82 | let ccr; 83 | let r; 84 | let g; 85 | let b; 86 | 87 | for (let row = 0; row < rows; row++) { 88 | for (let col = 0; col < cols; col++) { 89 | ccb = cb[cIndex]; 90 | ccr = cr[cIndex]; 91 | cIndex++; 92 | 93 | r = ccb + ((ccb * 103) >> 8) - 179; 94 | g = ((ccr * 88) >> 8) - 44 + ((ccb * 183) >> 8) - 91; 95 | b = ccr + ((ccr * 198) >> 8) - 227; 96 | 97 | // Line 1 98 | const y1 = y[yIndex1++]; 99 | const y2 = y[yIndex1++]; 100 | rgba[rgbaIndex1] = y1 + r; 101 | rgba[rgbaIndex1 + 1] = y1 - g; 102 | rgba[rgbaIndex1 + 2] = y1 + b; 103 | rgba[rgbaIndex1 + 4] = y2 + r; 104 | rgba[rgbaIndex1 + 5] = y2 - g; 105 | rgba[rgbaIndex1 + 6] = y2 + b; 106 | rgbaIndex1 += 8; 107 | 108 | // Line 2 109 | const y3 = y[yIndex2++]; 110 | const y4 = y[yIndex2++]; 111 | rgba[rgbaIndex2] = y3 + r; 112 | rgba[rgbaIndex2 + 1] = y3 - g; 113 | rgba[rgbaIndex2 + 2] = y3 + b; 114 | rgba[rgbaIndex2 + 4] = y4 + r; 115 | rgba[rgbaIndex2 + 5] = y4 - g; 116 | rgba[rgbaIndex2 + 6] = y4 + b; 117 | rgbaIndex2 += 8; 118 | } 119 | 120 | yIndex1 += yNext2Lines; 121 | yIndex2 += yNext2Lines; 122 | rgbaIndex1 += rgbaNext2Lines; 123 | rgbaIndex2 += rgbaNext2Lines; 124 | cIndex += cNextLine; 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/lib/mp2-wasm.js: -------------------------------------------------------------------------------- 1 | // Based on kjmp2 by Martin J. Fiedler 2 | // http://keyj.emphy.de/kjmp2/ 3 | 4 | import { Now } from '../utils'; 5 | 6 | import BaseDecoder from './decoder'; 7 | import BitBuffer from './buffer'; 8 | 9 | class MP2WASM extends BaseDecoder { 10 | constructor(options) { 11 | super(options); 12 | 13 | this.onDecodeCallback = options.onAudioDecode; 14 | this.module = options.wasmModule; 15 | 16 | this.bufferSize = options.audioBufferSize || 128 * 1024; 17 | this.bufferMode = options.streaming ? BitBuffer.MODE.EVICT : BitBuffer.MODE.EXPAND; 18 | 19 | this.sampleRate = 0; 20 | } 21 | 22 | initializeWasmDecoder() { 23 | if (!this.module.instance) { 24 | console.warn('JSMpeg: WASM module not compiled yet'); 25 | return; 26 | } 27 | this.instance = this.module.instance; 28 | this.functions = this.module.instance.exports; 29 | this.decoder = this.functions._mp2_decoder_create(this.bufferSize, this.bufferMode); 30 | } 31 | 32 | destroy() { 33 | if (!this.decoder) { 34 | return; 35 | } 36 | this.functions._mp2_decoder_destroy(this.decoder); 37 | } 38 | 39 | bufferGetIndex() { 40 | if (!this.decoder) { 41 | return; 42 | } 43 | // eslint-disable-next-line consistent-return 44 | return this.functions._mp2_decoder_get_index(this.decoder); 45 | } 46 | 47 | bufferSetIndex(index) { 48 | if (!this.decoder) { 49 | return; 50 | } 51 | this.functions._mp2_decoder_set_index(this.decoder, index); 52 | } 53 | 54 | bufferWrite(buffers) { 55 | if (!this.decoder) { 56 | this.initializeWasmDecoder(); 57 | } 58 | 59 | let totalLength = 0; 60 | for (let i = 0; i < buffers.length; i++) { 61 | totalLength += buffers[i].length; 62 | } 63 | 64 | let ptr = this.functions._mp2_decoder_get_write_ptr(this.decoder, totalLength); 65 | for (let i = 0; i < buffers.length; i++) { 66 | this.instance.heapU8.set(buffers[i], ptr); 67 | ptr += buffers[i].length; 68 | } 69 | 70 | this.functions._mp2_decoder_did_write(this.decoder, totalLength); 71 | return totalLength; 72 | } 73 | 74 | decode() { 75 | const startTime = Now(); 76 | 77 | if (!this.decoder) { 78 | return false; 79 | } 80 | 81 | const decodedBytes = this.functions._mp2_decoder_decode(this.decoder); 82 | if (decodedBytes === 0) { 83 | return false; 84 | } 85 | 86 | if (!this.sampleRate) { 87 | this.sampleRate = this.functions._mp2_decoder_get_sample_rate(this.decoder); 88 | } 89 | 90 | if (this.destination) { 91 | // Create a Float32 View into the modules output channel data 92 | const leftPtr = this.functions._mp2_decoder_get_left_channel_ptr(this.decoder); 93 | const rightPtr = this.functions._mp2_decoder_get_right_channel_ptr(this.decoder); 94 | 95 | const leftOffset = leftPtr / Float32Array.BYTES_PER_ELEMENT; 96 | const rightOffset = rightPtr / Float32Array.BYTES_PER_ELEMENT; 97 | 98 | const left = this.instance.heapF32.subarray( 99 | leftOffset, 100 | leftOffset + MP2WASM.SAMPLES_PER_FRAME, 101 | ); 102 | const right = this.instance.heapF32.subarray( 103 | rightOffset, 104 | rightOffset + MP2WASM.SAMPLES_PER_FRAME, 105 | ); 106 | 107 | this.destination.play(this.sampleRate, left, right); 108 | } 109 | 110 | this.advanceDecodedTime(MP2WASM.SAMPLES_PER_FRAME / this.sampleRate); 111 | 112 | const elapsedTime = Now() - startTime; 113 | if (this.onDecodeCallback) { 114 | this.onDecodeCallback(this, elapsedTime); 115 | } 116 | return true; 117 | } 118 | 119 | getCurrentTime() { 120 | const enqueuedTime = this.destination ? this.destination.enqueuedTime : 0; 121 | return this.decodedTime - enqueuedTime; 122 | } 123 | } 124 | 125 | MP2WASM.SAMPLES_PER_FRAME = 1152; 126 | 127 | export default MP2WASM; 128 | -------------------------------------------------------------------------------- /src/lib/ajax-progressive.js: -------------------------------------------------------------------------------- 1 | import { Now } from '../utils'; 2 | 3 | export default class AjaxProgressiveSource { 4 | constructor(url, options) { 5 | this.url = url; 6 | this.destination = null; 7 | this.request = null; 8 | this.streaming = false; 9 | 10 | this.completed = false; 11 | this.established = false; 12 | this.progress = 0; 13 | 14 | this.fileSize = 0; 15 | this.loadedSize = 0; 16 | this.chunkSize = options.chunkSize || 1024 * 1024; 17 | 18 | this.isLoading = false; 19 | this.loadStartTime = 0; 20 | this.throttled = options.throttled !== false; 21 | this.aborted = false; 22 | 23 | this.onEstablishedCallback = options.onSourceEstablished; 24 | this.onCompletedCallback = options.onSourceCompleted; 25 | 26 | if (options.hookOnEstablished) { 27 | this.hookOnEstablished = options.hookOnEstablished; 28 | } 29 | } 30 | 31 | connect(destination) { 32 | this.destination = destination; 33 | } 34 | 35 | start() { 36 | this.request = new XMLHttpRequest(); 37 | 38 | this.request.onreadystatechange = () => { 39 | if (this.request.readyState === this.request.DONE) { 40 | this.fileSize = parseInt(this.request.getResponseHeader('Content-Length'), 10); 41 | this.loadNextChunk(); 42 | } 43 | }; 44 | 45 | this.request.onprogress = this.onProgress.bind(this); 46 | this.request.open('HEAD', this.url); 47 | this.request.send(); 48 | } 49 | 50 | resume(secondsHeadroom) { 51 | if (this.isLoading || !this.throttled) { 52 | return; 53 | } 54 | 55 | // Guess the worst case loading time with lots of safety margin. This is 56 | // somewhat arbitrary... 57 | const worstCaseLoadingTime = this.loadTime * 8 + 2; 58 | if (worstCaseLoadingTime > secondsHeadroom) { 59 | this.loadNextChunk(); 60 | } 61 | } 62 | 63 | destroy() { 64 | this.request.abort(); 65 | this.aborted = true; 66 | } 67 | 68 | loadNextChunk() { 69 | const start = this.loadedSize; 70 | const end = Math.min(this.loadedSize + this.chunkSize - 1, this.fileSize - 1); 71 | 72 | if (start >= this.fileSize || this.aborted) { 73 | this.completed = true; 74 | if (this.onCompletedCallback) { 75 | this.onCompletedCallback(this); 76 | } 77 | return; 78 | } 79 | 80 | this.isLoading = true; 81 | this.loadStartTime = Now(); 82 | this.request = new XMLHttpRequest(); 83 | 84 | this.request.onreadystatechange = () => { 85 | if ( 86 | this.request.readyState === this.request.DONE 87 | && this.request.status >= 200 88 | && this.request.status < 300 89 | ) { 90 | this.onChunkLoad(this.request.response); 91 | } else if (this.request.readyState === this.request.DONE) { 92 | // Retry? 93 | if (this.loadFails++ < 3) { 94 | this.loadNextChunk(); 95 | } 96 | } 97 | }; 98 | 99 | if (start === 0) { 100 | this.request.onprogress = this.onProgress.bind(this); 101 | } 102 | 103 | this.request.open('GET', `${this.url}?${start}-${end}`); 104 | this.request.setRequestHeader('Range', `bytes=${start}-${end}`); 105 | this.request.responseType = 'arraybuffer'; 106 | this.request.send(); 107 | } 108 | 109 | onProgress(ev) { 110 | this.progress = ev.loaded / ev.total; 111 | } 112 | 113 | onChunkLoad(data) { 114 | const isFirstChunk = !this.established; 115 | this.established = true; 116 | this.progress = 1; 117 | 118 | this.loadedSize += data.byteLength; 119 | this.loadFails = 0; 120 | this.isLoading = false; 121 | 122 | if (isFirstChunk && this.hookOnEstablished) { 123 | this.hookOnEstablished(); 124 | } 125 | 126 | if (isFirstChunk && this.onEstablishedCallback) { 127 | this.onEstablishedCallback(this); 128 | } 129 | 130 | if (this.destination) { 131 | this.destination.write(data); 132 | } 133 | 134 | this.loadTime = Now() - this.loadStartTime; 135 | if (!this.throttled) { 136 | this.loadNextChunk(); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/lib/mpeg1-wasm.js: -------------------------------------------------------------------------------- 1 | import { Now } from '../utils'; 2 | 3 | import BaseDecoder from './decoder'; 4 | import BitBuffer from './buffer'; 5 | 6 | export default class MPEG1WASM extends BaseDecoder { 7 | constructor(options) { 8 | super(options); 9 | 10 | this.onDecodeCallback = options.onVideoDecode; 11 | this.module = options.wasmModule; 12 | 13 | this.bufferSize = options.videoBufferSize || 512 * 1024; 14 | this.bufferMode = options.streaming ? BitBuffer.MODE.EVICT : BitBuffer.MODE.EXPAND; 15 | 16 | this.decodeFirstFrame = options.decodeFirstFrame !== false; 17 | this.hasSequenceHeader = false; 18 | } 19 | 20 | initializeWasmDecoder() { 21 | if (!this.module.instance) { 22 | console.warn('JSMpeg: WASM module not compiled yet'); 23 | return; 24 | } 25 | this.instance = this.module.instance; 26 | this.functions = this.module.instance.exports; 27 | this.decoder = this.functions._mpeg1_decoder_create(this.bufferSize, this.bufferMode); 28 | } 29 | 30 | destroy() { 31 | if (!this.decoder) { 32 | return; 33 | } 34 | this.functions._mpeg1_decoder_destroy(this.decoder); 35 | } 36 | 37 | bufferGetIndex() { 38 | if (!this.decoder) { 39 | return; 40 | } 41 | // eslint-disable-next-line consistent-return 42 | return this.functions._mpeg1_decoder_get_index(this.decoder); 43 | } 44 | 45 | bufferSetIndex(index) { 46 | if (!this.decoder) { 47 | return; 48 | } 49 | this.functions._mpeg1_decoder_set_index(this.decoder, index); 50 | } 51 | 52 | bufferWrite(buffers) { 53 | if (!this.decoder) { 54 | this.initializeWasmDecoder(); 55 | } 56 | 57 | let totalLength = 0; 58 | for (let i = 0; i < buffers.length; i++) { 59 | totalLength += buffers[i].length; 60 | } 61 | 62 | let ptr = this.functions._mpeg1_decoder_get_write_ptr(this.decoder, totalLength); 63 | for (let i = 0; i < buffers.length; i++) { 64 | this.instance.heapU8.set(buffers[i], ptr); 65 | ptr += buffers[i].length; 66 | } 67 | 68 | this.functions._mpeg1_decoder_did_write(this.decoder, totalLength); 69 | return totalLength; 70 | } 71 | 72 | write(pts, buffers) { 73 | BaseDecoder.prototype.write.call(this, pts, buffers); 74 | 75 | if ( 76 | !this.hasSequenceHeader 77 | && this.functions._mpeg1_decoder_has_sequence_header(this.decoder) 78 | ) { 79 | this.loadSequenceHeader(); 80 | } 81 | } 82 | 83 | loadSequenceHeader() { 84 | this.hasSequenceHeader = true; 85 | this.frameRate = this.functions._mpeg1_decoder_get_frame_rate(this.decoder); 86 | this.codedSize = this.functions._mpeg1_decoder_get_coded_size(this.decoder); 87 | 88 | if (this.destination) { 89 | const w = this.functions._mpeg1_decoder_get_width(this.decoder); 90 | const h = this.functions._mpeg1_decoder_get_height(this.decoder); 91 | this.destination.resize(w, h); 92 | } 93 | 94 | if (this.decodeFirstFrame) { 95 | this.decode(); 96 | } 97 | } 98 | 99 | decode() { 100 | const startTime = Now(); 101 | 102 | if (!this.decoder) { 103 | return false; 104 | } 105 | 106 | const didDecode = this.functions._mpeg1_decoder_decode(this.decoder); 107 | if (!didDecode) { 108 | return false; 109 | } 110 | 111 | // Invoke decode callbacks 112 | if (this.destination) { 113 | const ptrY = this.functions._mpeg1_decoder_get_y_ptr(this.decoder); 114 | const ptrCr = this.functions._mpeg1_decoder_get_cr_ptr(this.decoder); 115 | const ptrCb = this.functions._mpeg1_decoder_get_cb_ptr(this.decoder); 116 | 117 | const dy = this.instance.heapU8.subarray(ptrY, ptrY + this.codedSize); 118 | const dcr = this.instance.heapU8.subarray(ptrCr, ptrCr + (this.codedSize >> 2)); 119 | const dcb = this.instance.heapU8.subarray(ptrCb, ptrCb + (this.codedSize >> 2)); 120 | 121 | this.destination.render(dy, dcr, dcb, false); 122 | } 123 | 124 | this.advanceDecodedTime(1 / this.frameRate); 125 | 126 | const elapsedTime = Now() - startTime; 127 | if (this.onDecodeCallback) { 128 | this.onDecodeCallback(this, elapsedTime); 129 | } 130 | return true; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/lib/webaudio.js: -------------------------------------------------------------------------------- 1 | import { Now } from '../utils'; 2 | 3 | class WebAudioOut { 4 | constructor() { 5 | this.context = WebAudioOut.CachedContext = WebAudioOut.CachedContext 6 | || new (window.AudioContext || window.webkitAudioContext)(); 7 | 8 | this.gain = this.context.createGain(); 9 | this.destination = this.gain; 10 | 11 | // Keep track of the number of connections to this AudioContext, so we 12 | // can safely close() it when we're the only one connected to it. 13 | this.gain.connect(this.context.destination); 14 | this.context._connections = (this.context._connections || 0) + 1; 15 | 16 | this.startTime = 0; 17 | this.buffer = null; 18 | this.wallclockStartTime = 0; 19 | this.volume = 1; 20 | this.enabled = true; 21 | 22 | this.unlocked = !WebAudioOut.NeedsUnlocking(); 23 | 24 | Object.defineProperty(this, 'enqueuedTime', { get: this.getEnqueuedTime }); 25 | } 26 | 27 | destroy() { 28 | this.gain.disconnect(); 29 | this.context._connections--; 30 | 31 | if (this.context._connections === 0) { 32 | this.context.close(); 33 | WebAudioOut.CachedContext = null; 34 | } 35 | } 36 | 37 | play(sampleRate, left, right) { 38 | if (!this.enabled) { 39 | return; 40 | } 41 | 42 | // If the context is not unlocked yet, we simply advance the start time 43 | // to "fake" actually playing audio. This will keep the video in sync. 44 | if (!this.unlocked) { 45 | const ts = Now(); 46 | if (this.wallclockStartTime < ts) { 47 | this.wallclockStartTime = ts; 48 | } 49 | this.wallclockStartTime += left.length / sampleRate; 50 | return; 51 | } 52 | 53 | this.gain.gain.value = this.volume; 54 | 55 | const buffer = this.context.createBuffer(2, left.length, sampleRate); 56 | buffer.getChannelData(0).set(left); 57 | buffer.getChannelData(1).set(right); 58 | 59 | const source = this.context.createBufferSource(); 60 | source.buffer = buffer; 61 | source.connect(this.destination); 62 | 63 | const now = this.context.currentTime; 64 | const { duration } = buffer; 65 | if (this.startTime < now) { 66 | this.startTime = now; 67 | this.wallclockStartTime = Now(); 68 | } 69 | 70 | source.start(this.startTime); 71 | this.startTime += duration; 72 | this.wallclockStartTime += duration; 73 | } 74 | 75 | stop() { 76 | // Meh; there seems to be no simple way to get a list of currently 77 | // active source nodes from the Audio Context, and maintaining this 78 | // list ourselfs would be a pain, so we just set the gain to 0 79 | // to cut off all enqueued audio instantly. 80 | this.gain.gain.value = 0; 81 | } 82 | 83 | getEnqueuedTime() { 84 | // The AudioContext.currentTime is only updated every so often, so if we 85 | // want to get exact timing, we need to rely on the system time. 86 | return Math.max(this.wallclockStartTime - Now(), 0); 87 | } 88 | 89 | resetEnqueuedTime() { 90 | this.startTime = this.context.currentTime; 91 | this.wallclockStartTime = Now(); 92 | } 93 | 94 | unlock(callback) { 95 | if (this.unlocked) { 96 | if (callback) { 97 | callback(); 98 | } 99 | return; 100 | } 101 | 102 | this.unlockCallback = callback; 103 | 104 | // Create empty buffer and play it 105 | const buffer = this.context.createBuffer(1, 1, 22050); 106 | const source = this.context.createBufferSource(); 107 | source.buffer = buffer; 108 | source.connect(this.destination); 109 | 110 | // polyfill source.start(0); 111 | if (source.start) { 112 | source.start(0); 113 | } else { 114 | source.noteOn(0); 115 | } 116 | 117 | setTimeout(this.checkIfUnlocked.bind(this, source, 0), 0); 118 | } 119 | 120 | checkIfUnlocked(source, attempt) { 121 | if ( 122 | source.playbackState === source.PLAYING_STATE 123 | || source.playbackState === source.FINISHED_STATE 124 | ) { 125 | this.unlocked = true; 126 | if (this.unlockCallback) { 127 | this.unlockCallback(); 128 | this.unlockCallback = null; 129 | } 130 | } else if (attempt < 10) { 131 | // Jeez, what a shit show. Thanks iOS! 132 | setTimeout(this.checkIfUnlocked.bind(this, source, attempt + 1), 100); 133 | } 134 | } 135 | 136 | static NeedsUnlocking() { 137 | return /iPhone|iPad|iPod/i.test(navigator.userAgent); 138 | } 139 | 140 | static IsSupported() { 141 | return window.AudioContext || window.webkitAudioContext; 142 | } 143 | } 144 | 145 | WebAudioOut.CachedContext = null; 146 | 147 | export default WebAudioOut; 148 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * According to jsmpeg project(https://github.com/phoboslab/jsmpeg) 3 | */ 4 | 5 | // ES6 modular 6 | import Player from './lib/player'; 7 | import VideoElement from './lib/video-element'; 8 | import BitBuffer from './lib/buffer'; 9 | import AjaxSource from './lib/ajax'; 10 | import FetchSource from './lib/fetch'; 11 | import AjaxProgressiveSource from './lib/ajax-progressive'; 12 | import WSSource from './lib/websocket'; 13 | import TS from './lib/ts'; 14 | import BaseDecoder from './lib/decoder'; 15 | import MPEG1 from './lib/mpeg1'; 16 | import MPEG1WASM from './lib/mpeg1-wasm'; 17 | import MP2 from './lib/mp2'; 18 | import MP2WASM from './lib/mp2-wasm'; 19 | import WebGLRenderer from './lib/webgl'; 20 | import CanvasRenderer from './lib/canvas2d'; 21 | import WebAudioOut from './lib/webaudio'; 22 | import { 23 | Now, CreateVideoElements, Fill, Base64ToArrayBuffer, 24 | } from './utils'; 25 | import WASMModule from './lib/wasm-module'; 26 | import WASM_BINARY from './lib/wasm/WASM_BINARY'; 27 | 28 | // This sets up the JSMpeg "Namespace". The object is empty apart from the Now() 29 | // utility function and the automatic CreateVideoElements() after DOMReady. 30 | const JSMpeg = { 31 | // The Player sets up the connections between source, demuxer, decoders, 32 | // renderer and audio output. It ties everything together, is responsible 33 | // of scheduling decoding and provides some convenience methods for 34 | // external users. 35 | Player, 36 | 37 | // A Video Element wraps the Player, shows HTML controls to start/pause 38 | // the video and handles Audio unlocking on iOS. VideoElements can be 39 | // created directly in HTML using the
tag. 40 | VideoElement, 41 | 42 | // The BitBuffer wraps a Uint8Array and allows reading an arbitrary number 43 | // of bits at a time. On writing, the BitBuffer either expands its 44 | // internal buffer (for static files) or deletes old data (for streaming). 45 | BitBuffer, 46 | 47 | // A Source provides raw data from HTTP, a WebSocket connection or any 48 | // other mean. Sources must support the following API: 49 | // .connect(destinationNode) 50 | // .write(buffer) 51 | // .start() - start reading 52 | // .resume(headroom) - continue reading; headroom to play pos in seconds 53 | // .established - boolean, true after connection is established 54 | // .completed - boolean, true if the source is completely loaded 55 | // .progress - float 0-1 56 | Source: { 57 | Ajax: AjaxSource, 58 | AjaxProgressive: AjaxProgressiveSource, 59 | WebSocket: WSSource, 60 | Fetch: FetchSource, 61 | }, 62 | 63 | // A Demuxer may sit between a Source and a Decoder. It separates the 64 | // incoming raw data into Video, Audio and other Streams. API: 65 | // .connect(streamId, destinationNode) 66 | // .write(buffer) 67 | // .currentTime – float, in seconds 68 | // .startTime - float, in seconds 69 | Demuxer: { 70 | TS, 71 | }, 72 | 73 | // A Decoder accepts an incoming Stream of raw Audio or Video data, buffers 74 | // it and upon `.decode()` decodes a single frame of data. Video decoders 75 | // call `destinationNode.render(Y, Cr, CB)` with the decoded pixel data; 76 | // Audio decoders call `destinationNode.play(left, right)` with the decoded 77 | // PCM data. API: 78 | // .connect(destinationNode) 79 | // .write(pts, buffer) 80 | // .decode() 81 | // .seek(time) 82 | // .currentTime - float, in seconds 83 | // .startTime - float, in seconds 84 | Decoder: { 85 | Base: BaseDecoder, 86 | MPEG1Video: MPEG1, 87 | MPEG1VideoWASM: MPEG1WASM, 88 | MP2Audio: MP2, 89 | MP2AudioWASM: MP2WASM, 90 | }, 91 | 92 | // A Renderer accepts raw YCrCb data in 3 separate buffers via the render() 93 | // method. Renderers typically convert the data into the RGBA color space 94 | // and draw it on a Canvas, but other output - such as writing PNGs - would 95 | // be conceivable. API: 96 | // .render(y, cr, cb) - pixel data as Uint8Arrays 97 | // .enabled - wether the renderer does anything upon receiving data 98 | Renderer: { 99 | WebGL: WebGLRenderer, 100 | Canvas2D: CanvasRenderer, 101 | }, 102 | 103 | // Audio Outputs accept raw Stero PCM data in 2 separate buffers via the 104 | // play() method. Outputs typically play the audio on the user's device. 105 | // API: 106 | // .play(sampleRate, left, right) - rate in herz; PCM data as Uint8Arrays 107 | // .stop() 108 | // .enqueuedTime - float, in seconds 109 | // .enabled - wether the output does anything upon receiving data 110 | AudioOutput: { 111 | WebAudio: WebAudioOut, 112 | }, 113 | 114 | WASMModule, 115 | 116 | // functions 117 | Now, 118 | CreateVideoElements, 119 | Fill, 120 | Base64ToArrayBuffer, 121 | 122 | // The build process may append `JSMpeg.WASM_BINARY_INLINED = base64data;` 123 | // to the minified source. 124 | // If this property is present, jsmpeg will use the inlined binary data 125 | // instead of trying to load a jsmpeg.wasm file via Ajax. 126 | WASM_BINARY_INLINED: WASM_BINARY, 127 | }; 128 | 129 | export default JSMpeg; 130 | -------------------------------------------------------------------------------- /src/lib/buffer.js: -------------------------------------------------------------------------------- 1 | class BitBuffer { 2 | constructor(bufferOrLength, mode) { 3 | if (typeof bufferOrLength === 'object') { 4 | this.bytes = bufferOrLength instanceof Uint8Array 5 | ? bufferOrLength 6 | : new Uint8Array(bufferOrLength); 7 | 8 | this.byteLength = this.bytes.length; 9 | } else { 10 | this.bytes = new Uint8Array(bufferOrLength || 1024 * 1024); 11 | this.byteLength = 0; 12 | } 13 | 14 | this.mode = mode || BitBuffer.MODE.EXPAND; 15 | this.index = 0; 16 | } 17 | 18 | resize(size) { 19 | const newBytes = new Uint8Array(size); 20 | if (this.byteLength !== 0) { 21 | this.byteLength = Math.min(this.byteLength, size); 22 | newBytes.set(this.bytes, 0, this.byteLength); 23 | } 24 | this.bytes = newBytes; 25 | this.index = Math.min(this.index, this.byteLength << 3); 26 | } 27 | 28 | evict(sizeNeeded) { 29 | const bytePos = this.index >> 3; 30 | const available = this.bytes.length - this.byteLength; 31 | 32 | // If the current index is the write position, we can simply reset both 33 | // to 0. Also reset (and throw away yet unread data) if we won't be able 34 | // to fit the new data in even after a normal eviction. 35 | if ( 36 | this.index === this.byteLength << 3 37 | || sizeNeeded > available + bytePos // emergency evac 38 | ) { 39 | this.byteLength = 0; 40 | this.index = 0; 41 | return; 42 | } if (bytePos === 0) { 43 | // Nothing read yet - we can't evict anything 44 | return; 45 | } 46 | 47 | // Some browsers don't support copyWithin() yet - we may have to do 48 | // it manually using set and a subarray 49 | if (this.bytes.copyWithin) { 50 | this.bytes.copyWithin(0, bytePos, this.byteLength); 51 | } else { 52 | this.bytes.set(this.bytes.subarray(bytePos, this.byteLength)); 53 | } 54 | 55 | this.byteLength -= bytePos; 56 | this.index -= bytePos << 3; 57 | } 58 | 59 | write(buffers) { 60 | const isArrayOfBuffers = typeof buffers[0] === 'object'; 61 | let totalLength = 0; 62 | const available = this.bytes.length - this.byteLength; 63 | // Calculate total byte length 64 | if (isArrayOfBuffers) { 65 | totalLength = 0; 66 | for (let i = 0; i < buffers.length; i++) { 67 | totalLength += buffers[i].byteLength; 68 | } 69 | } else { 70 | totalLength = buffers.byteLength; 71 | } 72 | 73 | // Do we need to resize or evict? 74 | if (totalLength > available) { 75 | if (this.mode === BitBuffer.MODE.EXPAND) { 76 | const newSize = Math.max(this.bytes.length * 2, totalLength - available); 77 | this.resize(newSize); 78 | } else { 79 | this.evict(totalLength); 80 | } 81 | } 82 | 83 | if (isArrayOfBuffers) { 84 | for (let i = 0; i < buffers.length; i++) { 85 | this.appendSingleBuffer(buffers[i]); 86 | } 87 | } else { 88 | this.appendSingleBuffer(buffers); 89 | } 90 | 91 | return totalLength; 92 | } 93 | 94 | appendSingleBuffer(buffer) { 95 | buffer = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer); 96 | 97 | this.bytes.set(buffer, this.byteLength); 98 | this.byteLength += buffer.length; 99 | } 100 | 101 | findNextStartCode() { 102 | for (let i = (this.index + 7) >> 3; i < this.byteLength; i++) { 103 | if (this.bytes[i] === 0x00 && this.bytes[i + 1] === 0x00 && this.bytes[i + 2] === 0x01) { 104 | this.index = (i + 4) << 3; 105 | return this.bytes[i + 3]; 106 | } 107 | } 108 | this.index = this.byteLength << 3; 109 | return -1; 110 | } 111 | 112 | findStartCode(code) { 113 | const current = this.findNextStartCode(); 114 | if (current === code || current === -1) { 115 | return current; 116 | } 117 | return -1; 118 | } 119 | 120 | nextBytesAreStartCode() { 121 | const i = (this.index + 7) >> 3; 122 | return ( 123 | i >= this.byteLength 124 | || (this.bytes[i] === 0x00 && this.bytes[i + 1] === 0x00 && this.bytes[i + 2] === 0x01) 125 | ); 126 | } 127 | 128 | peek(count) { 129 | let offset = this.index; 130 | let value = 0; 131 | while (count) { 132 | const currentByte = this.bytes[offset >> 3]; 133 | const remaining = 8 - (offset & 7); // remaining bits in byte 134 | const read = remaining < count ? remaining : count; // bits in this run 135 | const shift = remaining - read; 136 | const mask = 0xff >> (8 - read); 137 | 138 | value = (value << read) | ((currentByte & (mask << shift)) >> shift); 139 | 140 | offset += read; 141 | count -= read; 142 | } 143 | 144 | return value; 145 | } 146 | 147 | read(count) { 148 | const value = this.peek(count); 149 | this.index += count; 150 | return value; 151 | } 152 | 153 | skip(count) { 154 | // eslint-disable-next-line no-return-assign 155 | return (this.index += count); 156 | } 157 | 158 | rewind(count) { 159 | this.index = Math.max(this.index - count, 0); 160 | } 161 | 162 | has(count) { 163 | return (this.byteLength << 3) - this.index >= count; 164 | } 165 | } 166 | 167 | BitBuffer.MODE = { 168 | EVICT: 1, 169 | EXPAND: 2, 170 | }; 171 | 172 | export default BitBuffer; 173 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [6.1.2](https://github.com/cycjimmy/jsmpeg-player/compare/v6.1.1...v6.1.2) (2024-10-25) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * use @cycjimmy/sass-lib@3 ([5c3e8ba](https://github.com/cycjimmy/jsmpeg-player/commit/5c3e8bae89cde7d03f6cb0b9fd6d47335138198e)) 7 | 8 | ## [6.1.1](https://github.com/cycjimmy/jsmpeg-player/compare/v6.1.0...v6.1.1) (2024-07-16) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * upgrade rollup to v4 ([8de1ea0](https://github.com/cycjimmy/jsmpeg-player/commit/8de1ea0288dff53f39d4c443ac802e8fc6403b63)) 14 | 15 | # [6.1.0](https://github.com/cycjimmy/jsmpeg-player/compare/v6.0.5...v6.1.0) (2024-07-16) 16 | 17 | 18 | ### Features 19 | 20 | * use dart-sass instead of node sass ([cbc2457](https://github.com/cycjimmy/jsmpeg-player/commit/cbc2457eef46f20a83c998fe527f2d241171baad)) 21 | 22 | ## [6.0.5](https://github.com/cycjimmy/jsmpeg-player/compare/v6.0.4...v6.0.5) (2022-09-22) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * fix problems with destroying WebGL context ([558c02e](https://github.com/cycjimmy/jsmpeg-player/commit/558c02e5dd13ead15d7eabf9a130de2adaba1c05)) 28 | 29 | ## [6.0.4](https://github.com/cycjimmy/jsmpeg-player/compare/v6.0.3...v6.0.4) (2022-09-05) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * catch lost context when creating the WebGL renderer ([6a6fcc6](https://github.com/cycjimmy/jsmpeg-player/commit/6a6fcc6c75363f43f04ed2d3c980338c6095d9f6)) 35 | * fix WebSocket constructor chocking on empty protocol string ([4caa818](https://github.com/cycjimmy/jsmpeg-player/commit/4caa8181bdabd32565ce2b8c44b4282460bc0e60)) 36 | 37 | ## [6.0.3](https://github.com/cycjimmy/jsmpeg-player/compare/v6.0.2...v6.0.3) (2022-06-21) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * remove @rollup/plugin-commonjs ([15d5cfe](https://github.com/cycjimmy/jsmpeg-player/commit/15d5cfe8afb817b83541d44448f6d345e0f8acb8)) 43 | 44 | ## [6.0.2](https://github.com/cycjimmy/jsmpeg-player/compare/v6.0.1...v6.0.2) (2022-03-26) 45 | 46 | 47 | ### Bug Fixes 48 | 49 | * fix root version after releasing ([7cf298a](https://github.com/cycjimmy/jsmpeg-player/commit/7cf298abe67e664651a1122f70dac638187cfed1)) 50 | 51 | ## [6.0.1](https://github.com/cycjimmy/jsmpeg-player/compare/v6.0.0...v6.0.1) (2022-03-25) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * fix root version after releasing ([d586ed2](https://github.com/cycjimmy/jsmpeg-player/commit/d586ed2ff98784c5d686dbf82665ef61bb1f4899)) 57 | 58 | # [6.0.0](https://github.com/cycjimmy/jsmpeg-player/compare/v5.1.1...v6.0.0) (2022-03-25) 59 | 60 | 61 | ### Features 62 | 63 | * change to module mode ([cff7d05](https://github.com/cycjimmy/jsmpeg-player/commit/cff7d057d4a33cb6eeb35669638f2b17c8bec33f)) 64 | 65 | 66 | ### BREAKING CHANGES 67 | 68 | * change to module mode 69 | 70 | ## [5.1.1](https://github.com/cycjimmy/jsmpeg-player/compare/v5.1.0...v5.1.1) (2022-03-15) 71 | 72 | 73 | ### Bug Fixes 74 | 75 | * fix race condition where WASM-Module is instantiated twice; and rebuild ([490801a](https://github.com/cycjimmy/jsmpeg-player/commit/490801a9605c883944b5ff53c3900d9c56ea469c)) 76 | * fix typo (https://github.com/phoboslab/jsmpeg/commit/55886464d289623af9c9dd39e8080a29a0719591) ([d3cc3a5](https://github.com/cycjimmy/jsmpeg-player/commit/d3cc3a5a580170c0d3847dbc46a1904f59f77a8a)) 77 | * handle WebGL contextLost ([b04df7e](https://github.com/cycjimmy/jsmpeg-player/commit/b04df7e128be71c87fbc95b4e22acf8776f668fc)) 78 | 79 | # [5.1.0](https://github.com/cycjimmy/jsmpeg-player/compare/v5.0.1...v5.1.0) (2022-03-15) 80 | 81 | 82 | ### Features 83 | 84 | * **deps:** upgrade deps ([2cfdb8e](https://github.com/cycjimmy/jsmpeg-player/commit/2cfdb8e26a58c3c1f6e4f6b66a6b36bde388c1b9)) 85 | 86 | ## [5.0.1](https://github.com/cycjimmy/jsmpeg-player/compare/v5.0.0...v5.0.1) (2020-05-12) 87 | 88 | 89 | ### Bug Fixes 90 | 91 | * **mp2:** fix audio only using the right channel for volume ([628844f](https://github.com/cycjimmy/jsmpeg-player/commit/628844febcc75ed6857e421becfbf8fafe72216d)), closes [#24](https://github.com/cycjimmy/jsmpeg-player/issues/24) [#24](https://github.com/cycjimmy/jsmpeg-player/issues/24) 92 | 93 | # [5.0.0](https://github.com/cycjimmy/jsmpeg-player/compare/v4.0.4...v5.0.0) (2020-01-19) 94 | 95 | 96 | ### Features 97 | 98 | * use rollup refactor project ([1d04cd5](https://github.com/cycjimmy/jsmpeg-player/commit/1d04cd5b1589e7481207ca4c45d4a39eddbd673c)) 99 | 100 | 101 | ### BREAKING CHANGES 102 | 103 | * use rollup refactor project 104 | 105 | ## [4.0.4](https://github.com/cycjimmy/jsmpeg-player/compare/v4.0.3...v4.0.4) (2020-01-09) 106 | 107 | 108 | ### Bug Fixes 109 | 110 | * fix error `JSMpeg is not defined` ([556be62](https://github.com/cycjimmy/jsmpeg-player/commit/556be621890382d2cebdff89a15ace30af1bd364)) 111 | * **upgrade:** update from origin jsmpeg ([9dd8098](https://github.com/cycjimmy/jsmpeg-player/commit/9dd8098c46d88161efdf6334ddc81c621be02b93)) 112 | 113 | ## [4.0.3](https://github.com/cycjimmy/jsmpeg-player/compare/v4.0.2...v4.0.3) (2019-10-24) 114 | 115 | 116 | ### Bug Fixes 117 | 118 | * **build:** replace "uglifyjs" with "terser" ([b55be86](https://github.com/cycjimmy/jsmpeg-player/commit/b55be862c794d41ce5c88898f7f54406dc9bc3e3)) 119 | 120 | ## [4.0.2](https://github.com/cycjimmy/jsmpeg-player/compare/v4.0.1...v4.0.2) (2019-10-23) 121 | 122 | 123 | ### Bug Fixes 124 | 125 | * **release:** start to use semantic release ([fa9c554](https://github.com/cycjimmy/jsmpeg-player/commit/fa9c554cb9a0e4c2bb161e47c7267009387452ec)) 126 | -------------------------------------------------------------------------------- /src/lib/wasm-module.js: -------------------------------------------------------------------------------- 1 | import AjaxSource from './ajax'; 2 | 3 | export default class WASM { 4 | constructor() { 5 | this.stackSize = 5 * 1024 * 1024; // emscripten default 6 | this.pageSize = 64 * 1024; // wasm page size 7 | this.onInitCallbacks = []; 8 | this.ready = false; 9 | this.loadingFromFileStarted = false; 10 | this.loadingFromBufferStarted = false; 11 | } 12 | 13 | write(buffer) { 14 | this.loadFromBuffer(buffer); 15 | } 16 | 17 | loadFromFile(url, callback) { 18 | if (callback) { 19 | this.onInitCallbacks.push(callback); 20 | } 21 | 22 | // Make sure this WASM Module is only instantiated once. If loadFromFile() 23 | // was already called, bail out here. On instantiation all pending 24 | // onInitCallbacks will be called. 25 | if (this.loadingFromFileStarted) { 26 | return; 27 | } 28 | this.loadingFromFileStarted = true; 29 | 30 | this.onInitCallback = callback; 31 | const ajax = new AjaxSource(url, {}); 32 | ajax.connect(this); 33 | ajax.start(); 34 | } 35 | 36 | loadFromBuffer(buffer, callback) { 37 | if (callback) { 38 | this.onInitCallbacks.push(callback); 39 | } 40 | 41 | // Make sure this WASM Module is only instantiated once. If loadFromBuffer() 42 | // was already called, bail out here. On instantiation all pending 43 | // onInitCallbacks will be called. 44 | if (this.loadingFromBufferStarted) { 45 | return; 46 | } 47 | this.loadingFromBufferStarted = true; 48 | 49 | this.moduleInfo = this.readDylinkSection(buffer); 50 | if (!this.moduleInfo) { 51 | for (let i = 0; i < this.onInitCallbacks.length; i++) { 52 | this.onInitCallbacks[i](null); 53 | } 54 | return; 55 | } 56 | 57 | this.memory = new WebAssembly.Memory({ initial: 256 }); 58 | const env = { 59 | memory: this.memory, 60 | memoryBase: 0, 61 | __memory_base: 0, 62 | table: new WebAssembly.Table({ initial: this.moduleInfo.tableSize, element: 'anyfunc' }), 63 | tableBase: 0, 64 | __table_base: 0, 65 | abort: this.c_abort.bind(this), 66 | ___assert_fail: this.c_assertFail.bind(this), 67 | _sbrk: this.c_sbrk.bind(this), 68 | }; 69 | 70 | this.brk = this.align(this.moduleInfo.memorySize + this.stackSize); 71 | WebAssembly.instantiate(buffer, { env }).then((results) => { 72 | this.instance = results.instance; 73 | if (this.instance.exports.__post_instantiate) { 74 | this.instance.exports.__post_instantiate(); 75 | } 76 | this.createHeapViews(); 77 | this.ready = true; 78 | for (let i = 0; i < this.onInitCallbacks.length; i++) { 79 | this.onInitCallbacks[i](this); 80 | } 81 | }); 82 | } 83 | 84 | createHeapViews() { 85 | this.instance.heapU8 = new Uint8Array(this.memory.buffer); 86 | this.instance.heapU32 = new Uint32Array(this.memory.buffer); 87 | this.instance.heapF32 = new Float32Array(this.memory.buffer); 88 | } 89 | 90 | align(addr) { 91 | // eslint-disable-next-line no-restricted-properties 92 | const a = 2 ** this.moduleInfo.memoryAlignment; 93 | return Math.ceil(addr / a) * a; 94 | } 95 | 96 | c_sbrk(size) { 97 | const previousBrk = this.brk; 98 | this.brk += size; 99 | 100 | if (this.brk > this.memory.buffer.byteLength) { 101 | const bytesNeeded = this.brk - this.memory.buffer.byteLength; 102 | const pagesNeeded = Math.ceil(bytesNeeded / this.pageSize); 103 | this.memory.grow(pagesNeeded); 104 | this.createHeapViews(); 105 | } 106 | return previousBrk; 107 | } 108 | 109 | // eslint-disable-next-line no-unused-vars,class-methods-use-this 110 | c_abort(size) { 111 | // eslint-disable-next-line prefer-rest-params 112 | console.warn('JSMPeg: WASM abort', arguments); 113 | } 114 | 115 | // eslint-disable-next-line no-unused-vars,class-methods-use-this 116 | c_assertFail(size) { 117 | // eslint-disable-next-line prefer-rest-params 118 | console.warn('JSMPeg: WASM ___assert_fail', arguments); 119 | } 120 | 121 | // eslint-disable-next-line class-methods-use-this 122 | readDylinkSection(buffer) { 123 | // Read the WASM header and dylink section of the .wasm binary data 124 | // to get the needed table size and static data size. 125 | 126 | // https://github.com/WebAssembly/tool-conventions/blob/master/DynamicLinking.md 127 | // https://github.com/kripken/emscripten/blob/20602efb955a7c6c20865a495932427e205651d2/src/support.js 128 | 129 | const bytes = new Uint8Array(buffer); 130 | let next = 0; 131 | 132 | const readVarUint = () => { 133 | let ret = 0; 134 | let mul = 1; 135 | // eslint-disable-next-line no-constant-condition 136 | while (1) { 137 | const byte = bytes[next++]; 138 | ret += (byte & 0x7f) * mul; 139 | mul *= 0x80; 140 | if (!(byte & 0x80)) { 141 | return ret; 142 | } 143 | } 144 | }; 145 | 146 | const matchNextBytes = (expected) => { 147 | for (let i = 0; i < expected.length; i++) { 148 | const b = typeof expected[i] === 'string' ? expected[i].charCodeAt(0) : expected[i]; 149 | if (bytes[next++] !== b) { 150 | return false; 151 | } 152 | } 153 | return true; 154 | }; 155 | 156 | // Make sure we have a wasm header 157 | if (!matchNextBytes([0, 'a', 's', 'm'])) { 158 | console.warn('JSMpeg: WASM header not found'); 159 | return null; 160 | } 161 | 162 | // Make sure we have a dylink section 163 | next = 9; 164 | // eslint-disable-next-line no-unused-vars 165 | const sectionSize = readVarUint(); 166 | if (!matchNextBytes([6, 'd', 'y', 'l', 'i', 'n', 'k'])) { 167 | console.warn('JSMpeg: No dylink section found in WASM'); 168 | return null; 169 | } 170 | 171 | return { 172 | memorySize: readVarUint(), 173 | memoryAlignment: readVarUint(), 174 | tableSize: readVarUint(), 175 | tableAlignment: readVarUint(), 176 | }; 177 | } 178 | 179 | static IsSupported() { 180 | return !!window.WebAssembly; 181 | } 182 | 183 | static GetModule() { 184 | WASM.CACHED_MODULE = WASM.CACHED_MODULE || new WASM(); 185 | return WASM.CACHED_MODULE; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSMpeg Player(TS Player) 2 | 3 | ![][workflows-badge-image] 4 | [![libraries dependency status][libraries-status-image]][libraries-status-url] 5 | [![libraries sourcerank][libraries-sourcerank-image]][libraries-sourcerank-url] 6 | [![Release date][release-date-image]][release-url] 7 | [![rollup][rollup-image]][rollup-url] 8 | [![semantic-release][semantic-image]][semantic-url] 9 | [![npm license][license-image]][download-url] 10 | 11 | 12 | * **[jsmpeg-player](https://github.com/cycdpo/jsmpeg-player) has been renamed to @cycjimmy/jsmpeg-player for scoped NPM package.** 13 | * JSMpeg player is based on [jsmpeg](https://github.com/phoboslab/jsmpeg). 14 | * The video must be compressed into the TS format of MPEG1 / MP2. 15 | * Apple device automatically plays without sound, you need to guide the user to click on the video in the lower right corner of the video icon to unlock the sound. (no similar problem in non-autoplay mode) 16 | * [Demo][github-pages-url] 17 | 18 | ## How to use 19 | ### Install 20 | [![NPM version][npm-image]][npm-url] 21 | [![NPM bundle size][npm-bundle-size-image]][npm-url] 22 | [![npm download][download-image]][download-url] 23 | 24 | ```shell 25 | $ npm install @cycjimmy/jsmpeg-player --save 26 | # or 27 | $ yarn add @cycjimmy/jsmpeg-player 28 | ``` 29 | 30 | ### Usage 31 | ```javascript 32 | import JSMpeg from '@cycjimmy/jsmpeg-player'; 33 | # OR 34 | const JSMpeg = require('@cycjimmy/jsmpeg-player'); 35 | ``` 36 | 37 | ```javascript 38 | new JSMpeg.VideoElement(videoWrapper, videoUrl [, options] [, overlayOptions]) 39 | ``` 40 | 41 | * `JSMpeg.VideoElement` config: 42 | * `videoWrapper`: [String | Element] The wrapper of the video. The height and width of the wrapper are recommended to be initialized. 43 | * `videoUrl`: [String] A URL to an MPEG .ts file 44 | * `options`: [Object] support: 45 | * `canvas`: [String | Element] The HTML canvas element to use for video rendering. If none is given, the renderer will create its own canvas element. Default `''`. 46 | * `poster`: [String] URL to an image to use as the poster to show before the video plays. (Recommended to set it manually) 47 | * `autoplay`: [Boolean] Whether to start playing immediately. Default `false`. 48 | * `autoSetWrapperSize`: [Boolean] Whether to set the wrapper element size automatically when the video loaded. Default `false`. 49 | * `loop`: [Boolean] Whether to loop the video (static files only). Default `false`.**[overwrite]** 50 | * `control`: [Boolean] Whether the user can control. Default `true`. 51 | * `decodeFirstFrame`: [Boolean] Whether to decode and display the first frame of the video. Default `true`. 52 | * `picMode`: [Boolean] Picture mode (no playButton). Default `false`. 53 | * `progressive`: [Boolean] whether to load data in chunks (static files only). Default `true`. 54 | * `chunkSize` [Number] The chunk size in bytes to load at a time. Default `1024*1024` (1mb). 55 | * `hooks`: [Object] The hook function 56 | * `play`: [Function] The hook function when the video play. 57 | * `pause`: [Function] The hook function when the video pause. 58 | * `stop`: [Function] The hook function when the video stop. 59 | * `load`: [Function] The hook function when the video established. 60 | * `overlayOptions`: [Object] More options can view the [jsmpeg options](https://github.com/phoboslab/jsmpeg#usage) 61 | 62 | * `JSMpeg.VideoElement` instance supports the following methods: 63 | * `play()`: Start playback 64 | * `pause()`: Pause playback 65 | * `stop()`: Stop playback and seek to the beginning 66 | * `destroy()`: Stop playback and empty video wrapper 67 | * `JSMpeg.VideoElement.player` instance API can view the [JSMpeg.Player API](https://github.com/phoboslab/jsmpeg#jsmpegplayer-api) 68 | 69 | ### Use in browser 70 | [![jsdelivr][jsdelivr-image]][jsdelivr-url] 71 | 72 | ```html 73 |
74 | 75 | 79 | ``` 80 | 81 | ## CDN 82 | To use via a CDN include this in your HTML: 83 | ```text 84 | 85 | ``` 86 | 87 | ## Encoding Video/Audio for [jsmpeg](https://github.com/phoboslab/jsmpeg) by [ffmpeg](https://ffmpeg.org/). E.g: 88 | ```shell 89 | $ ffmpeg -i input.mp4 -f mpegts \ 90 | -codec:v mpeg1video -s 640x360 -b:v 700k -r 25 -bf 0 \ 91 | -codec:a mp2 -ar 44100 -ac 1 -b:a 64k \ 92 | output.ts 93 | ``` 94 | 95 | * options 96 | * `-s`: video size 97 | * `-b:v`: video bit rate 98 | * `-r`: frame rate 99 | * `-ar`: sampling rate 100 | * `-ac`: number of audio channels 101 | * `-b:a`: audio bit rate 102 | 103 | 104 | 105 | [npm-image]: https://img.shields.io/npm/v/@cycjimmy/jsmpeg-player 106 | [npm-url]: https://npmjs.org/package/@cycjimmy/jsmpeg-player 107 | [npm-bundle-size-image]: https://img.shields.io/bundlephobia/min/@cycjimmy/jsmpeg-player 108 | 109 | [download-image]: https://img.shields.io/npm/dt/@cycjimmy/jsmpeg-player 110 | [download-url]: https://npmjs.org/package/@cycjimmy/jsmpeg-player 111 | 112 | [jsdelivr-image]: https://img.shields.io/jsdelivr/npm/hy/@cycjimmy/jsmpeg-player 113 | [jsdelivr-url]: https://www.jsdelivr.com/package/npm/@cycjimmy/jsmpeg-player 114 | 115 | [workflows-badge-image]: https://github.com/cycjimmy/jsmpeg-player/workflows/Test%20CI/badge.svg 116 | 117 | [libraries-status-image]: https://img.shields.io/librariesio/release/npm/@cycjimmy/jsmpeg-player 118 | [libraries-sourcerank-image]: https://img.shields.io/librariesio/sourcerank/npm/@cycjimmy/jsmpeg-player 119 | [libraries-status-url]: https://libraries.io/github/cycjimmy/jsmpeg-player 120 | [libraries-sourcerank-url]: https://libraries.io/npm/@cycjimmy%2Fjsmpeg-player 121 | 122 | [release-date-image]: https://img.shields.io/github/release-date/cycjimmy/jsmpeg-player 123 | [release-url]: https://github.com/cycjimmy/jsmpeg-player/releases 124 | 125 | [rollup-image]: https://img.shields.io/github/package-json/dependency-version/cycjimmy/jsmpeg-player/dev/rollup 126 | [rollup-url]: https://github.com/rollup/rollup 127 | 128 | [semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg 129 | [semantic-url]: https://github.com/semantic-release/semantic-release 130 | 131 | [license-image]: https://img.shields.io/npm/l/@cycjimmy/jsmpeg-player 132 | 133 | [github-pages-url]: https://cycjimmy.github.io/jsmpeg-player/ 134 | -------------------------------------------------------------------------------- /src/lib/ts.js: -------------------------------------------------------------------------------- 1 | import BitBuffer from './buffer'; 2 | 3 | class TS { 4 | constructor() { 5 | this.bits = null; 6 | this.leftoverBytes = null; 7 | 8 | this.guessVideoFrameEnd = true; 9 | this.pidsToStreamIds = {}; 10 | 11 | this.pesPacketInfo = {}; 12 | this.startTime = 0; 13 | this.currentTime = 0; 14 | } 15 | 16 | connect(streamId, destination) { 17 | this.pesPacketInfo[streamId] = { 18 | destination, 19 | currentLength: 0, 20 | totalLength: 0, 21 | pts: 0, 22 | buffers: [], 23 | }; 24 | } 25 | 26 | write(buffer) { 27 | if (this.leftoverBytes) { 28 | const totalLength = buffer.byteLength + this.leftoverBytes.byteLength; 29 | this.bits = new BitBuffer(totalLength); 30 | this.bits.write([this.leftoverBytes, buffer]); 31 | } else { 32 | this.bits = new BitBuffer(buffer); 33 | } 34 | 35 | // eslint-disable-next-line no-empty 36 | while (this.bits.has(188 << 3) && this.parsePacket()) {} 37 | 38 | const leftoverCount = this.bits.byteLength - (this.bits.index >> 3); 39 | this.leftoverBytes = leftoverCount > 0 ? this.bits.bytes.subarray(this.bits.index >> 3) : null; 40 | } 41 | 42 | parsePacket() { 43 | // Check if we're in sync with packet boundaries; attempt to resync if not. 44 | if (this.bits.read(8) !== 0x47) { 45 | if (!this.resync()) { 46 | // Couldn't resync; maybe next time... 47 | return false; 48 | } 49 | } 50 | 51 | const end = (this.bits.index >> 3) + 187; 52 | // eslint-disable-next-line no-unused-vars 53 | const transportError = this.bits.read(1); 54 | const payloadStart = this.bits.read(1); 55 | // eslint-disable-next-line no-unused-vars 56 | const transportPriority = this.bits.read(1); 57 | const pid = this.bits.read(13); 58 | // eslint-disable-next-line no-unused-vars 59 | const transportScrambling = this.bits.read(2); 60 | const adaptationField = this.bits.read(2); 61 | // eslint-disable-next-line no-unused-vars 62 | const continuityCounter = this.bits.read(4); 63 | 64 | // If this is the start of a new payload; signal the end of the previous 65 | // frame, if we didn't do so already. 66 | let streamId = this.pidsToStreamIds[pid]; 67 | if (payloadStart && streamId) { 68 | const pi = this.pesPacketInfo[streamId]; 69 | if (pi && pi.currentLength) { 70 | this.packetComplete(pi); 71 | } 72 | } 73 | 74 | // Extract current payload 75 | if (adaptationField & 0x1) { 76 | if (adaptationField & 0x2) { 77 | const adaptationFieldLength = this.bits.read(8); 78 | this.bits.skip(adaptationFieldLength << 3); 79 | } 80 | 81 | if (payloadStart && this.bits.nextBytesAreStartCode()) { 82 | this.bits.skip(24); 83 | streamId = this.bits.read(8); 84 | this.pidsToStreamIds[pid] = streamId; 85 | 86 | const packetLength = this.bits.read(16); 87 | this.bits.skip(8); 88 | const ptsDtsFlag = this.bits.read(2); 89 | this.bits.skip(6); 90 | const headerLength = this.bits.read(8); 91 | const payloadBeginIndex = this.bits.index + (headerLength << 3); 92 | 93 | const pi = this.pesPacketInfo[streamId]; 94 | if (pi) { 95 | let pts = 0; 96 | if (ptsDtsFlag & 0x2) { 97 | // The Presentation Timestamp is encoded as 33(!) bit 98 | // integer, but has a "marker bit" inserted at weird places 99 | // in between, making the whole thing 5 bytes in size. 100 | // You can't make this shit up... 101 | this.bits.skip(4); 102 | const p32_30 = this.bits.read(3); 103 | this.bits.skip(1); 104 | const p29_15 = this.bits.read(15); 105 | this.bits.skip(1); 106 | const p14_0 = this.bits.read(15); 107 | this.bits.skip(1); 108 | 109 | // Can't use bit shifts here; we need 33 bits of precision, 110 | // so we're using JavaScript's double number type. Also 111 | // divide by the 90khz clock to get the pts in seconds. 112 | pts = (p32_30 * 1073741824 + p29_15 * 32768 + p14_0) / 90000; 113 | 114 | this.currentTime = pts; 115 | if (this.startTime === -1) { 116 | this.startTime = pts; 117 | } 118 | } 119 | 120 | const payloadLength = packetLength ? packetLength - headerLength - 3 : 0; 121 | this.packetStart(pi, pts, payloadLength); 122 | } 123 | 124 | // Skip the rest of the header without parsing it 125 | this.bits.index = payloadBeginIndex; 126 | } 127 | 128 | if (streamId) { 129 | // Attempt to detect if the PES packet is complete. For Audio (and 130 | // other) packets, we received a total packet length with the PES 131 | // header, so we can check the current length. 132 | 133 | // For Video packets, we have to guess the end by detecting if this 134 | // TS packet was padded - there's no good reason to pad a TS packet 135 | // in between, but it might just fit exactly. If this fails, we can 136 | // only wait for the next PES header for that stream. 137 | 138 | const pi = this.pesPacketInfo[streamId]; 139 | if (pi) { 140 | const start = this.bits.index >> 3; 141 | const complete = this.packetAddData(pi, start, end); 142 | 143 | const hasPadding = !payloadStart && adaptationField & 0x2; 144 | if (complete || (this.guessVideoFrameEnd && hasPadding)) { 145 | this.packetComplete(pi); 146 | } 147 | } 148 | } 149 | } 150 | 151 | this.bits.index = end << 3; 152 | return true; 153 | } 154 | 155 | resync() { 156 | // Check if we have enough data to attempt a resync. We need 5 full packets. 157 | if (!this.bits.has((188 * 6) << 3)) { 158 | return false; 159 | } 160 | 161 | const byteIndex = this.bits.index >> 3; 162 | 163 | // Look for the first sync token in the first 187 bytes 164 | for (let i = 0; i < 187; i++) { 165 | if (this.bits.bytes[byteIndex + i] === 0x47) { 166 | // Look for 4 more sync tokens, each 188 bytes appart 167 | let foundSync = true; 168 | for (let j = 1; j < 5; j++) { 169 | if (this.bits.bytes[byteIndex + i + 188 * j] !== 0x47) { 170 | foundSync = false; 171 | break; 172 | } 173 | } 174 | 175 | if (foundSync) { 176 | this.bits.index = (byteIndex + i + 1) << 3; 177 | return true; 178 | } 179 | } 180 | } 181 | 182 | // In theory, we shouldn't arrive here. If we do, we had enough data but 183 | // still didn't find sync - this can only happen if we were fed garbage 184 | // data. Check your source! 185 | console.warn('JSMpeg: Possible garbage data. Skipping.'); 186 | this.bits.skip(187 << 3); 187 | return false; 188 | } 189 | 190 | // eslint-disable-next-line class-methods-use-this 191 | packetStart(pi, pts, payloadLength) { 192 | pi.totalLength = payloadLength; 193 | pi.currentLength = 0; 194 | pi.pts = pts; 195 | } 196 | 197 | packetAddData(pi, start, end) { 198 | pi.buffers.push(this.bits.bytes.subarray(start, end)); 199 | pi.currentLength += end - start; 200 | 201 | return pi.totalLength !== 0 && pi.currentLength >= pi.totalLength; 202 | } 203 | 204 | // eslint-disable-next-line class-methods-use-this 205 | packetComplete(pi) { 206 | pi.destination.write(pi.pts, pi.buffers); 207 | pi.totalLength = 0; 208 | pi.currentLength = 0; 209 | pi.buffers = []; 210 | } 211 | } 212 | 213 | TS.STREAM = { 214 | PACK_HEADER: 0xba, 215 | SYSTEM_HEADER: 0xbb, 216 | PROGRAM_MAP: 0xbc, 217 | PRIVATE_1: 0xbd, 218 | PADDING: 0xbe, 219 | PRIVATE_2: 0xbf, 220 | AUDIO_1: 0xc0, 221 | VIDEO_1: 0xe0, 222 | DIRECTORY: 0xff, 223 | }; 224 | 225 | export default TS; 226 | -------------------------------------------------------------------------------- /src/lib/video-element.js: -------------------------------------------------------------------------------- 1 | // utils 2 | import isString from '@cycjimmy/awesome-js-funcs/esm/judgeBasic/isString'; 3 | import functionToPromise from '@cycjimmy/awesome-js-funcs/esm/typeConversion/functionToPromise'; 4 | 5 | // style 6 | import _style from '../theme/style.scss'; 7 | // button view 8 | import { PLAY_BUTTON, UNMUTE_BUTTON } from '../buttonView'; 9 | 10 | // service 11 | import Player from './player'; 12 | 13 | export default class VideoElement { 14 | constructor( 15 | wrapper, 16 | videoUrl, 17 | { 18 | canvas = '', 19 | poster = '', 20 | autoplay = false, 21 | autoSetWrapperSize = false, 22 | loop = false, 23 | control = true, 24 | decodeFirstFrame = true, 25 | picMode = false, 26 | progressive = true, 27 | chunkSize = 1024 * 1024, 28 | hooks = {}, 29 | } = {}, 30 | overlayOptions = {}, 31 | ) { 32 | this.options = { 33 | videoUrl, 34 | canvas, 35 | poster, 36 | picMode, 37 | autoplay, 38 | autoSetWrapperSize, 39 | loop, 40 | control, 41 | decodeFirstFrame, 42 | progressive, 43 | chunkSize, 44 | hooks: { 45 | play: () => {}, 46 | pause: () => {}, 47 | stop: () => {}, 48 | load: () => {}, 49 | ...hooks, 50 | }, 51 | ...overlayOptions, 52 | }; 53 | 54 | this.options.needPlayButton = this.options.control && !this.options.picMode; 55 | 56 | this.player = null; 57 | 58 | // Setup canvas and play button 59 | this.els = { 60 | wrapper: isString(wrapper) ? document.querySelector(wrapper) : wrapper, 61 | canvas: null, 62 | playButton: document.createElement('div'), 63 | unmuteButton: null, 64 | poster: null, 65 | }; 66 | 67 | if (window.getComputedStyle(this.els.wrapper).getPropertyValue('position') === 'static') { 68 | this.els.wrapper.style.position = 'relative'; 69 | } 70 | 71 | this.els.wrapper.clientRect = this.els.wrapper.getBoundingClientRect(); 72 | 73 | this.initCanvas(); 74 | this.initPlayButton(); 75 | this.initPlayer(); 76 | } 77 | 78 | initCanvas() { 79 | if (this.options.canvas) { 80 | this.els.canvas = isString(this.options.canvas) 81 | ? document.querySelector(this.options.canvas) 82 | : this.options.canvas; 83 | } else { 84 | this.els.canvas = document.createElement('canvas'); 85 | this.els.canvas.classList.add(_style.canvas); 86 | this.els.wrapper.appendChild(this.els.canvas); 87 | } 88 | } 89 | 90 | initPlayer() { 91 | // Parse the data-options - we try to decode the values as json. This way 92 | // we can get proper boolean and number values. If JSON.parse() fails, 93 | // treat it as a string. 94 | this.options = Object.assign(this.options, { 95 | canvas: this.els.canvas, 96 | }); 97 | 98 | // eslint-disable-next-line no-underscore-dangle 99 | const _options = { ...this.options, autoplay: false }; 100 | 101 | // Create the player instance 102 | this.player = new Player(this.options.videoUrl, _options, { 103 | play: () => { 104 | if (this.options.needPlayButton) { 105 | this.els.playButton.classList.add(_style.hidden); 106 | } 107 | 108 | if (this.els.poster) { 109 | this.els.poster.classList.add(_style.hidden); 110 | } 111 | 112 | this.options.hooks.play(); 113 | }, 114 | pause: () => { 115 | if (this.options.needPlayButton) { 116 | this.els.playButton.classList.remove(_style.hidden); 117 | } 118 | 119 | this.options.hooks.pause(); 120 | }, 121 | stop: () => { 122 | if (this.els.poster) { 123 | this.els.poster.classList.remove(_style.hidden); 124 | } 125 | 126 | this.options.hooks.stop(); 127 | }, 128 | load: () => { 129 | if (this.options.autoplay) { 130 | this.play(); 131 | } 132 | 133 | this._autoSetWrapperSize(); 134 | this.options.hooks.load(); 135 | }, 136 | }); 137 | 138 | this._copyPlayerFuncs(); 139 | this.els.wrapper.playerInstance = this.player; 140 | 141 | // Setup the poster element, if any 142 | if (this.options.poster && !this.options.autoplay && !this.player.options.streaming) { 143 | this.options.decodeFirstFrame = false; 144 | this.els.poster = new Image(); 145 | this.els.poster.src = this.options.poster; 146 | this.els.poster.classList.add(_style.poster); 147 | this.els.wrapper.appendChild(this.els.poster); 148 | } 149 | 150 | // Add the click handler if this video is pausable 151 | if (!this.player.options.streaming) { 152 | this.els.wrapper.addEventListener('click', this.onClick.bind(this)); 153 | } 154 | 155 | // Hide the play button if this video immediately begins playing 156 | if (this.options.autoplay || this.player.options.streaming) { 157 | this.els.playButton.classList.add(_style.hidden); 158 | } 159 | 160 | // Set up the unlock audio button for iOS devices. iOS only allows us to 161 | // play audio after a user action has initiated playing. For autoplay or 162 | // streaming players we set up a muted speaker icon as the button. For all 163 | // others, we can simply use the play button. 164 | if (this.player.audioOut && !this.player.audioOut.unlocked) { 165 | let unlockAudioElement = this.els.wrapper; 166 | 167 | if (this.options.autoplay || this.player.options.streaming) { 168 | this.els.unmuteButton = document.createElement('div'); 169 | this.els.unmuteButton.innerHTML = UNMUTE_BUTTON; 170 | this.els.unmuteButton.classList.add(_style.unmuteButton); 171 | this.els.wrapper.appendChild(this.els.unmuteButton); 172 | unlockAudioElement = this.els.unmuteButton; 173 | } 174 | 175 | this.unlockAudioBound = this.onUnlockAudio.bind(this, unlockAudioElement); 176 | unlockAudioElement.addEventListener('touchstart', this.unlockAudioBound, false); 177 | unlockAudioElement.addEventListener('click', this.unlockAudioBound, true); 178 | } 179 | } 180 | 181 | initPlayButton() { 182 | if (!this.options.needPlayButton) { 183 | return; 184 | } 185 | 186 | this.els.playButton.classList.add(_style.playButton); 187 | this.els.playButton.innerHTML = PLAY_BUTTON; 188 | this.els.wrapper.appendChild(this.els.playButton); 189 | } 190 | 191 | _autoSetWrapperSize() { 192 | if (!this.options.autoSetWrapperSize) { 193 | return Promise.resolve(); 194 | } 195 | 196 | const { destination } = this.player.video; 197 | 198 | if (!destination) { 199 | return Promise.resolve(); 200 | } 201 | 202 | return Promise.resolve().then(() => functionToPromise(() => { 203 | this.els.wrapper.style.width = `${destination.width}px`; 204 | this.els.wrapper.style.height = `${destination.height}px`; 205 | })); 206 | } 207 | 208 | onUnlockAudio(element, ev) { 209 | if (this.els.unmuteButton) { 210 | ev.preventDefault(); 211 | ev.stopPropagation(); 212 | } 213 | this.player.audioOut.unlock(() => { 214 | if (this.els.unmuteButton) { 215 | this.els.unmuteButton.classList.add(_style.hidden); 216 | } 217 | element.removeEventListener('touchstart', this.unlockAudioBound); 218 | element.removeEventListener('click', this.unlockAudioBound); 219 | }); 220 | } 221 | 222 | onClick() { 223 | if (!this.options.control) { 224 | return; 225 | } 226 | 227 | if (this.player.isPlaying) { 228 | this.pause(); 229 | } else { 230 | this.play(); 231 | } 232 | } 233 | 234 | /** 235 | * copy player functions 236 | * @private 237 | */ 238 | _copyPlayerFuncs() { 239 | this.play = () => this.player.play(); 240 | this.pause = () => this.player.pause(); 241 | this.stop = () => this.player.stop(); 242 | this.destroy = () => { 243 | this.player.destroy(); 244 | this.els.wrapper.innerHTML = ''; 245 | this.els.wrapper.playerInstance = null; 246 | }; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/lib/webgl.js: -------------------------------------------------------------------------------- 1 | class WebGLRenderer { 2 | constructor(options) { 3 | if (options.canvas) { 4 | this.canvas = options.canvas; 5 | this.ownsCanvasElement = false; 6 | } else { 7 | this.canvas = document.createElement('canvas'); 8 | this.ownsCanvasElement = true; 9 | } 10 | this.width = this.canvas.width; 11 | this.height = this.canvas.height; 12 | this.enabled = true; 13 | 14 | this.hasTextureData = {}; 15 | 16 | const contextCreateOptions = { 17 | preserveDrawingBuffer: !!options.preserveDrawingBuffer, 18 | alpha: false, 19 | depth: false, 20 | stencil: false, 21 | antialias: false, 22 | premultipliedAlpha: false, 23 | }; 24 | 25 | this.gl = this.canvas.getContext('webgl', contextCreateOptions) 26 | || this.canvas.getContext('experimental-webgl', contextCreateOptions); 27 | 28 | if (!this.gl) { 29 | throw new Error('Failed to get WebGL Context'); 30 | } 31 | 32 | this.handleContextLostBound = this.handleContextLost.bind(this); 33 | this.handleContextRestoredBound = this.handleContextRestored.bind(this); 34 | 35 | this.canvas.addEventListener('webglcontextlost', this.handleContextLostBound, false); 36 | this.canvas.addEventListener('webglcontextrestored', this.handleContextRestoredBound, false); 37 | 38 | this.initGL(); 39 | } 40 | 41 | initGL() { 42 | this.hasTextureData = {}; 43 | 44 | const { gl } = this; 45 | let vertexAttr = null; 46 | 47 | gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); 48 | 49 | // Init buffers 50 | this.vertexBuffer = gl.createBuffer(); 51 | const vertexCoords = new Float32Array([0, 0, 0, 1, 1, 0, 1, 1]); 52 | gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); 53 | gl.bufferData(gl.ARRAY_BUFFER, vertexCoords, gl.STATIC_DRAW); 54 | 55 | // Setup the main YCrCbToRGBA shader 56 | this.program = this.createProgram( 57 | WebGLRenderer.SHADER.VERTEX_IDENTITY, 58 | WebGLRenderer.SHADER.FRAGMENT_YCRCB_TO_RGBA, 59 | ); 60 | vertexAttr = gl.getAttribLocation(this.program, 'vertex'); 61 | gl.enableVertexAttribArray(vertexAttr); 62 | gl.vertexAttribPointer(vertexAttr, 2, gl.FLOAT, false, 0, 0); 63 | 64 | this.textureY = this.createTexture(0, 'textureY'); 65 | this.textureCb = this.createTexture(1, 'textureCb'); 66 | this.textureCr = this.createTexture(2, 'textureCr'); 67 | 68 | // Setup the loading animation shader 69 | this.loadingProgram = this.createProgram( 70 | WebGLRenderer.SHADER.VERTEX_IDENTITY, 71 | WebGLRenderer.SHADER.FRAGMENT_LOADING, 72 | ); 73 | vertexAttr = gl.getAttribLocation(this.loadingProgram, 'vertex'); 74 | gl.enableVertexAttribArray(vertexAttr); 75 | gl.vertexAttribPointer(vertexAttr, 2, gl.FLOAT, false, 0, 0); 76 | 77 | this.shouldCreateUnclampedViews = !this.allowsClampedTextureData(); 78 | } 79 | 80 | handleContextLost(ev) { 81 | ev.preventDefault(); 82 | this.contextLost = true; 83 | } 84 | 85 | handleContextRestored() { 86 | this.initGL(); 87 | } 88 | 89 | destroy() { 90 | const { gl } = this; 91 | 92 | this.deleteTexture(gl.TEXTURE0, this.textureY); 93 | this.deleteTexture(gl.TEXTURE1, this.textureCb); 94 | this.deleteTexture(gl.TEXTURE2, this.textureCr); 95 | 96 | gl.useProgram(null); 97 | gl.deleteProgram(this.program); 98 | gl.deleteProgram(this.loadingProgram); 99 | 100 | gl.bindBuffer(gl.ARRAY_BUFFER, null); 101 | gl.deleteBuffer(this.vertexBuffer); 102 | 103 | this.canvas.removeEventListener('webglcontextlost', this.handleContextLostBound, false); 104 | this.canvas.removeEventListener('webglcontextrestored', this.handleContextRestoredBound, false); 105 | 106 | if (this.ownsCanvasElement) { 107 | this.canvas.remove(); 108 | } 109 | } 110 | 111 | resize(width, height) { 112 | this.width = width | 0; 113 | this.height = height | 0; 114 | 115 | this.canvas.width = this.width; 116 | this.canvas.height = this.height; 117 | 118 | this.gl.useProgram(this.program); 119 | const codedWidth = ((this.width + 15) >> 4) << 4; 120 | this.gl.viewport(0, 0, codedWidth, this.height); 121 | } 122 | 123 | createTexture(index, name) { 124 | const { gl } = this; 125 | const texture = gl.createTexture(); 126 | 127 | gl.bindTexture(gl.TEXTURE_2D, texture); 128 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 129 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 130 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 131 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 132 | gl.uniform1i(gl.getUniformLocation(this.program, name), index); 133 | 134 | return texture; 135 | } 136 | 137 | createProgram(vsh, fsh) { 138 | const { gl } = this; 139 | const program = gl.createProgram(); 140 | 141 | gl.attachShader(program, this.compileShader(gl.VERTEX_SHADER, vsh)); 142 | gl.attachShader(program, this.compileShader(gl.FRAGMENT_SHADER, fsh)); 143 | gl.linkProgram(program); 144 | gl.useProgram(program); 145 | 146 | return program; 147 | } 148 | 149 | compileShader(type, source) { 150 | const { gl } = this; 151 | const shader = gl.createShader(type); 152 | gl.shaderSource(shader, source); 153 | gl.compileShader(shader); 154 | 155 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 156 | throw new Error(gl.getShaderInfoLog(shader)); 157 | } 158 | 159 | return shader; 160 | } 161 | 162 | allowsClampedTextureData() { 163 | const { gl } = this; 164 | const texture = gl.createTexture(); 165 | 166 | gl.bindTexture(gl.TEXTURE_2D, texture); 167 | gl.texImage2D( 168 | gl.TEXTURE_2D, 169 | 0, 170 | gl.LUMINANCE, 171 | 1, 172 | 1, 173 | 0, 174 | gl.LUMINANCE, 175 | gl.UNSIGNED_BYTE, 176 | new Uint8ClampedArray([0]), 177 | ); 178 | return gl.getError() === 0; 179 | } 180 | 181 | renderProgress(progress) { 182 | const { gl } = this; 183 | 184 | gl.useProgram(this.loadingProgram); 185 | 186 | const loc = gl.getUniformLocation(this.loadingProgram, 'progress'); 187 | gl.uniform1f(loc, progress); 188 | 189 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 190 | } 191 | 192 | render(y, cb, cr, isClampedArray) { 193 | if (!this.enabled) { 194 | return; 195 | } 196 | 197 | const { gl } = this; 198 | const w = ((this.width + 15) >> 4) << 4; 199 | const h = this.height; 200 | const w2 = w >> 1; 201 | const h2 = h >> 1; 202 | 203 | // In some browsers WebGL doesn't like Uint8ClampedArrays (this is a bug 204 | // and should be fixed soon-ish), so we have to create a Uint8Array view 205 | // for each plane. 206 | if (isClampedArray && this.shouldCreateUnclampedViews) { 207 | y = new Uint8Array(y.buffer); 208 | cb = new Uint8Array(cb.buffer); 209 | cr = new Uint8Array(cr.buffer); 210 | } 211 | 212 | gl.useProgram(this.program); 213 | 214 | this.updateTexture(gl.TEXTURE0, this.textureY, w, h, y); 215 | this.updateTexture(gl.TEXTURE1, this.textureCb, w2, h2, cb); 216 | this.updateTexture(gl.TEXTURE2, this.textureCr, w2, h2, cr); 217 | 218 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 219 | } 220 | 221 | updateTexture(unit, texture, w, h, data) { 222 | const { gl } = this; 223 | gl.activeTexture(unit); 224 | gl.bindTexture(gl.TEXTURE_2D, texture); 225 | 226 | if (this.hasTextureData[unit]) { 227 | gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, w, h, gl.LUMINANCE, gl.UNSIGNED_BYTE, data); 228 | } else { 229 | this.hasTextureData[unit] = true; 230 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, w, h, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, data); 231 | } 232 | } 233 | 234 | deleteTexture(unit, texture) { 235 | const { gl } = this; 236 | gl.activeTexture(unit); 237 | gl.bindTexture(gl.TEXTURE_2D, null); 238 | gl.deleteTexture(texture); 239 | } 240 | 241 | static IsSupported() { 242 | try { 243 | if (!window.WebGLRenderingContext) { 244 | return false; 245 | } 246 | 247 | const canvas = document.createElement('canvas'); 248 | return !!(canvas.getContext('webgl') || canvas.getContext('experimental-webgl')); 249 | } catch (err) { 250 | return false; 251 | } 252 | } 253 | } 254 | 255 | WebGLRenderer.SHADER = { 256 | FRAGMENT_YCRCB_TO_RGBA: [ 257 | 'precision mediump float;', 258 | 'uniform sampler2D textureY;', 259 | 'uniform sampler2D textureCb;', 260 | 'uniform sampler2D textureCr;', 261 | 'varying vec2 texCoord;', 262 | 263 | 'mat4 rec601 = mat4(', 264 | '1.16438, 0.00000, 1.59603, -0.87079,', 265 | '1.16438, -0.39176, -0.81297, 0.52959,', 266 | '1.16438, 2.01723, 0.00000, -1.08139,', 267 | '0, 0, 0, 1', 268 | ');', 269 | 270 | 'void main() {', 271 | 'float y = texture2D(textureY, texCoord).r;', 272 | 'float cb = texture2D(textureCb, texCoord).r;', 273 | 'float cr = texture2D(textureCr, texCoord).r;', 274 | 275 | 'gl_FragColor = vec4(y, cr, cb, 1.0) * rec601;', 276 | '}', 277 | ].join('\n'), 278 | 279 | FRAGMENT_LOADING: [ 280 | 'precision mediump float;', 281 | 'uniform float progress;', 282 | 'varying vec2 texCoord;', 283 | 284 | 'void main() {', 285 | 'float c = ceil(progress-(1.0-texCoord.y));', 286 | 'gl_FragColor = vec4(c,c,c,1);', 287 | '}', 288 | ].join('\n'), 289 | 290 | VERTEX_IDENTITY: [ 291 | 'attribute vec2 vertex;', 292 | 'varying vec2 texCoord;', 293 | 294 | 'void main() {', 295 | 'texCoord = vertex;', 296 | 'gl_Position = vec4((vertex * 2.0 - 1.0) * vec2(1, -1), 0.0, 1.0);', 297 | '}', 298 | ].join('\n'), 299 | }; 300 | 301 | export default WebGLRenderer; 302 | -------------------------------------------------------------------------------- /src/lib/player.js: -------------------------------------------------------------------------------- 1 | import { Now, Base64ToArrayBuffer } from '../utils'; 2 | 3 | import AjaxSource from './ajax'; 4 | import AjaxProgressiveSource from './ajax-progressive'; 5 | import WSSource from './websocket'; 6 | import TS from './ts'; 7 | import MPEG1 from './mpeg1'; 8 | import MPEG1WASM from './mpeg1-wasm'; 9 | import MP2 from './mp2'; 10 | import MP2WASM from './mp2-wasm'; 11 | import WebGLRenderer from './webgl'; 12 | import CanvasRenderer from './canvas2d'; 13 | import WebAudioOut from './webaudio'; 14 | import WASMModule from './wasm-module'; 15 | import WASM_BINARY from './wasm/WASM_BINARY'; 16 | 17 | export default class Player { 18 | /** 19 | * @param url 20 | * @param options 21 | * @param hooks (play: function, pause: function, stop: function) 插入UI回调 22 | * @constructor 23 | */ 24 | constructor(url, options = {}, hooks = {}) { 25 | this.options = options; 26 | 27 | this.hooks = hooks; 28 | this.options.hookOnEstablished = () => { 29 | if (this.hooks.load) { 30 | this.hooks.load(); 31 | } 32 | }; 33 | 34 | if (options.source) { 35 | // eslint-disable-next-line new-cap 36 | this.source = new options.source(url, this.options); 37 | options.streaming = !!this.source.streaming; 38 | } else if (url.match(/^wss?:\/\//)) { 39 | this.source = new WSSource(url, this.options); 40 | options.streaming = true; 41 | } else if (options.progressive) { 42 | this.source = new AjaxProgressiveSource(url, this.options); 43 | options.streaming = false; 44 | } else { 45 | this.source = new AjaxSource(url, this.options); 46 | options.streaming = false; 47 | } 48 | 49 | this.maxAudioLag = options.maxAudioLag || 0.25; 50 | this.loop = options.loop !== false; 51 | this.autoplay = !!options.autoplay || options.streaming; 52 | 53 | this.demuxer = new TS(options); 54 | this.source.connect(this.demuxer); 55 | 56 | if (!options.disableWebAssembly && WASMModule.IsSupported()) { 57 | this.wasmModule = WASMModule.GetModule(); 58 | options.wasmModule = this.wasmModule; 59 | } 60 | 61 | if (options.video !== false) { 62 | this.video = options.wasmModule ? new MPEG1WASM(options) : new MPEG1(options); 63 | 64 | this.renderer = !options.disableGl && WebGLRenderer.IsSupported() 65 | ? new WebGLRenderer(options) 66 | : new CanvasRenderer(options); 67 | 68 | this.demuxer.connect(TS.STREAM.VIDEO_1, this.video); 69 | this.video.connect(this.renderer); 70 | } 71 | 72 | if (options.audio !== false && WebAudioOut.IsSupported()) { 73 | this.audio = options.wasmModule ? new MP2WASM(options) : new MP2(options); 74 | this.audioOut = new WebAudioOut(options); 75 | this.demuxer.connect(TS.STREAM.AUDIO_1, this.audio); 76 | this.audio.connect(this.audioOut); 77 | } 78 | 79 | Object.defineProperty(this, 'currentTime', { 80 | get: this.getCurrentTime, 81 | set: this.setCurrentTime, 82 | }); 83 | Object.defineProperty(this, 'volume', { 84 | get: this.getVolume, 85 | set: this.setVolume, 86 | }); 87 | 88 | this.paused = true; 89 | this.unpauseOnShow = false; 90 | if (options.pauseWhenHidden !== false) { 91 | document.addEventListener('visibilitychange', this.showHide.bind(this)); 92 | } 93 | 94 | // If we have WebAssembly support, wait until the module is compiled before 95 | // loading the source. Otherwise the decoders won't know what to do with 96 | // the source data. 97 | if (this.wasmModule) { 98 | if (this.wasmModule.ready) { 99 | this.startLoading(); 100 | } else if (WASM_BINARY) { 101 | const wasm = Base64ToArrayBuffer(WASM_BINARY); 102 | this.wasmModule.loadFromBuffer(wasm, this.startLoading.bind(this)); 103 | } else { 104 | this.wasmModule.loadFromFile('jsmpeg.wasm', this.startLoading.bind(this)); 105 | } 106 | } else { 107 | this.startLoading(); 108 | } 109 | } 110 | 111 | startLoading() { 112 | this.source.start(); 113 | if (this.autoplay) { 114 | this.play(); 115 | } 116 | } 117 | 118 | showHide() { 119 | if (document.visibilityState === 'hidden') { 120 | this.unpauseOnShow = this.wantsToPlay; 121 | this.pause(); 122 | } else if (this.unpauseOnShow) { 123 | this.play(); 124 | } 125 | } 126 | 127 | play() { 128 | if (this.animationId) { 129 | return; 130 | } 131 | 132 | this.animationId = requestAnimationFrame(this.update.bind(this)); 133 | this.wantsToPlay = true; 134 | this.paused = false; 135 | } 136 | 137 | pause() { 138 | if (this.paused) { 139 | return; 140 | } 141 | 142 | cancelAnimationFrame(this.animationId); 143 | this.animationId = null; 144 | this.wantsToPlay = false; 145 | this.isPlaying = false; 146 | this.paused = true; 147 | 148 | if (this.audio && this.audio.canPlay) { 149 | // Seek to the currentTime again - audio may already be enqueued a bit 150 | // further, so we have to rewind it. 151 | this.audioOut.stop(); 152 | this.seek(this.currentTime); 153 | } 154 | 155 | if (this.hooks.pause) { 156 | this.hooks.pause(); 157 | } 158 | 159 | if (this.options.onPause) { 160 | this.options.onPause(this); 161 | } 162 | } 163 | 164 | getVolume() { 165 | return this.audioOut ? this.audioOut.volume : 0; 166 | } 167 | 168 | setVolume(volume) { 169 | if (this.audioOut) { 170 | this.audioOut.volume = volume; 171 | } 172 | } 173 | 174 | stop() { 175 | this.pause(); 176 | this.seek(0); 177 | if (this.video && this.options.decodeFirstFrame !== false) { 178 | this.video.decode(); 179 | } 180 | 181 | if (this.hooks.stop) { 182 | this.hooks.stop(); 183 | } 184 | } 185 | 186 | destroy() { 187 | this.pause(); 188 | this.source.destroy(); 189 | this.video && this.video.destroy(); 190 | this.renderer && this.renderer.destroy(); 191 | this.audio && this.audio.destroy(); 192 | this.audioOut && this.audioOut.destroy(); 193 | } 194 | 195 | seek(time) { 196 | const startOffset = this.audio && this.audio.canPlay 197 | ? this.audio.startTime 198 | : this.video.startTime; 199 | 200 | if (this.video) { 201 | this.video.seek(time + startOffset); 202 | } 203 | if (this.audio) { 204 | this.audio.seek(time + startOffset); 205 | } 206 | 207 | this.startTime = Now() - time; 208 | } 209 | 210 | getCurrentTime() { 211 | return this.audio && this.audio.canPlay 212 | ? this.audio.currentTime - this.audio.startTime 213 | : this.video.currentTime - this.video.startTime; 214 | } 215 | 216 | setCurrentTime(time) { 217 | this.seek(time); 218 | } 219 | 220 | update() { 221 | this.animationId = requestAnimationFrame(this.update.bind(this)); 222 | 223 | if (!this.source.established) { 224 | if (this.renderer) { 225 | this.renderer.renderProgress(this.source.progress); 226 | } 227 | return; 228 | } 229 | 230 | if (!this.isPlaying) { 231 | this.isPlaying = true; 232 | this.startTime = Now() - this.currentTime; 233 | 234 | if (this.hooks.play) { 235 | this.hooks.play(); 236 | } 237 | 238 | if (this.options.onPlay) { 239 | this.options.onPlay(this); 240 | } 241 | } 242 | 243 | if (this.options.streaming) { 244 | this.updateForStreaming(); 245 | } else { 246 | this.updateForStaticFile(); 247 | } 248 | } 249 | 250 | nextFrame() { 251 | if (this.source.established && this.video) { 252 | return this.video.decode(); 253 | } 254 | return false; 255 | } 256 | 257 | updateForStreaming() { 258 | // When streaming, immediately decode everything we have buffered up until 259 | // now to minimize playback latency. 260 | 261 | if (this.video) { 262 | this.video.decode(); 263 | } 264 | 265 | if (this.audio) { 266 | let decoded = false; 267 | do { 268 | // If there's a lot of audio enqueued already, disable output and 269 | // catch up with the encoding. 270 | if (this.audioOut.enqueuedTime > this.maxAudioLag) { 271 | this.audioOut.resetEnqueuedTime(); 272 | this.audioOut.enabled = false; 273 | } 274 | decoded = this.audio.decode(); 275 | } while (decoded); 276 | this.audioOut.enabled = true; 277 | } 278 | } 279 | 280 | updateForStaticFile() { 281 | let notEnoughData = false; 282 | let headroom = 0; 283 | 284 | // If we have an audio track, we always try to sync the video to the audio. 285 | // Gaps and discontinuities are far more percetable in audio than in video. 286 | 287 | if (this.audio && this.audio.canPlay) { 288 | // Do we have to decode and enqueue some more audio data? 289 | while (!notEnoughData && this.audio.decodedTime - this.audio.currentTime < 0.25) { 290 | notEnoughData = !this.audio.decode(); 291 | } 292 | 293 | // Sync video to audio 294 | if (this.video && this.video.currentTime < this.audio.currentTime) { 295 | notEnoughData = !this.video.decode(); 296 | } 297 | 298 | headroom = this.demuxer.currentTime - this.audio.currentTime; 299 | } else if (this.video) { 300 | // Video only - sync it to player's wallclock 301 | const targetTime = Now() - this.startTime + this.video.startTime; 302 | const lateTime = targetTime - this.video.currentTime; 303 | const frameTime = 1 / this.video.frameRate; 304 | 305 | if (this.video && lateTime > 0) { 306 | // If the video is too far behind (>2 frames), simply reset the 307 | // target time to the next frame instead of trying to catch up. 308 | if (lateTime > frameTime * 2) { 309 | this.startTime += lateTime; 310 | } 311 | 312 | notEnoughData = !this.video.decode(); 313 | } 314 | 315 | headroom = this.demuxer.currentTime - targetTime; 316 | } 317 | 318 | // Notify the source of the playhead headroom, so it can decide whether to 319 | // continue loading further data. 320 | this.source.resume(headroom); 321 | 322 | // If we failed to decode and the source is complete, it means we reached 323 | // the end of our data. We may want to loop. 324 | if (notEnoughData && this.source.completed) { 325 | if (this.loop) { 326 | this.seek(0); 327 | } else { 328 | // this.pause(); 329 | this.stop(); 330 | 331 | if (this.options.onEnded) { 332 | this.options.onEnded(this); 333 | } 334 | } 335 | } else if (notEnoughData && this.options.onStalled) { 336 | // If there's not enough data and the source is not completed, we have 337 | // just stalled. 338 | this.options.onStalled(this); 339 | } 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /src/lib/mp2.js: -------------------------------------------------------------------------------- 1 | // Based on kjmp2 by Martin J. Fiedler 2 | // http://keyj.emphy.de/kjmp2/ 3 | 4 | import { Now, Fill } from '../utils'; 5 | 6 | import BaseDecoder from './decoder'; 7 | import BitBuffer from './buffer'; 8 | 9 | class MP2 extends BaseDecoder { 10 | constructor(options) { 11 | super(options); 12 | 13 | this.onDecodeCallback = options.onAudioDecode; 14 | 15 | const bufferSize = options.audioBufferSize || 128 * 1024; 16 | const bufferMode = options.streaming ? BitBuffer.MODE.EVICT : BitBuffer.MODE.EXPAND; 17 | 18 | this.bits = new BitBuffer(bufferSize, bufferMode); 19 | 20 | this.left = new Float32Array(1152); 21 | this.right = new Float32Array(1152); 22 | this.sampleRate = 44100; 23 | 24 | this.D = new Float32Array(1024); 25 | this.D.set(MP2.SYNTHESIS_WINDOW, 0); 26 | this.D.set(MP2.SYNTHESIS_WINDOW, 512); 27 | this.V = [new Float32Array(1024), new Float32Array(1024)]; 28 | this.U = new Int32Array(32); 29 | this.VPos = 0; 30 | 31 | this.allocation = [new Array(32), new Array(32)]; 32 | this.scaleFactorInfo = [new Uint8Array(32), new Uint8Array(32)]; 33 | this.scaleFactor = [new Array(32), new Array(32)]; 34 | this.sample = [new Array(32), new Array(32)]; 35 | 36 | for (let j = 0; j < 2; j++) { 37 | for (let i = 0; i < 32; i++) { 38 | this.scaleFactor[j][i] = [0, 0, 0]; 39 | this.sample[j][i] = [0, 0, 0]; 40 | } 41 | } 42 | } 43 | 44 | decode() { 45 | const startTime = Now(); 46 | 47 | const pos = this.bits.index >> 3; 48 | if (pos >= this.bits.byteLength) { 49 | return false; 50 | } 51 | 52 | const decoded = this.decodeFrame(this.left, this.right); 53 | this.bits.index = (pos + decoded) << 3; 54 | 55 | if (!decoded) { 56 | return false; 57 | } 58 | 59 | if (this.destination) { 60 | this.destination.play(this.sampleRate, this.left, this.right); 61 | } 62 | 63 | this.advanceDecodedTime(this.left.length / this.sampleRate); 64 | 65 | const elapsedTime = Now() - startTime; 66 | if (this.onDecodeCallback) { 67 | this.onDecodeCallback(this, elapsedTime); 68 | } 69 | 70 | return true; 71 | } 72 | 73 | getCurrentTime() { 74 | const enqueuedTime = this.destination ? this.destination.enqueuedTime : 0; 75 | return this.decodedTime - enqueuedTime; 76 | } 77 | 78 | decodeFrame(left, right) { 79 | // Check for valid header: syncword OK, MPEG-Audio Layer 2 80 | const sync = this.bits.read(11); 81 | const version = this.bits.read(2); 82 | const layer = this.bits.read(2); 83 | const hasCRC = !this.bits.read(1); 84 | 85 | if (sync !== MP2.FRAME_SYNC || version !== MP2.VERSION.MPEG_1 || layer !== MP2.LAYER.II) { 86 | // Invalid header or unsupported version 87 | return 0; 88 | } 89 | 90 | let bitrateIndex = this.bits.read(4) - 1; 91 | if (bitrateIndex > 13) { 92 | // Invalid bit rate or 'free format' 93 | return 0; 94 | } 95 | 96 | let sampleRateIndex = this.bits.read(2); 97 | let sampleRate = MP2.SAMPLE_RATE[sampleRateIndex]; 98 | if (sampleRateIndex === 3) { 99 | // Invalid sample rate 100 | return 0; 101 | } 102 | if (version === MP2.VERSION.MPEG_2) { 103 | sampleRateIndex += 4; 104 | bitrateIndex += 14; 105 | } 106 | const padding = this.bits.read(1); 107 | // eslint-disable-next-line no-unused-vars 108 | const privat = this.bits.read(1); 109 | const mode = this.bits.read(2); 110 | 111 | // Parse the mode_extension, set up the stereo bound 112 | let bound = 0; 113 | if (mode === MP2.MODE.JOINT_STEREO) { 114 | bound = (this.bits.read(2) + 1) << 2; 115 | } else { 116 | this.bits.skip(2); 117 | bound = mode === MP2.MODE.MONO ? 0 : 32; 118 | } 119 | 120 | // Discard the last 4 bits of the header and the CRC value, if present 121 | this.bits.skip(4); 122 | if (hasCRC) { 123 | this.bits.skip(16); 124 | } 125 | 126 | // Compute the frame size 127 | const bitrate = MP2.BIT_RATE[bitrateIndex]; 128 | sampleRate = MP2.SAMPLE_RATE[sampleRateIndex]; 129 | const frameSize = ((144000 * bitrate) / sampleRate + padding) | 0; 130 | 131 | // Prepare the quantizer table lookups 132 | let tab3 = 0; 133 | let sblimit = 0; 134 | if (version === MP2.VERSION.MPEG_2) { 135 | // MPEG-2 (LSR) 136 | tab3 = 2; 137 | sblimit = 30; 138 | } else { 139 | // MPEG-1 140 | const tab1 = mode === MP2.MODE.MONO ? 0 : 1; 141 | const tab2 = MP2.QUANT_LUT_STEP_1[tab1][bitrateIndex]; 142 | tab3 = MP2.QUANT_LUT_STEP_2[tab2][sampleRateIndex]; 143 | sblimit = tab3 & 63; 144 | tab3 >>= 6; 145 | } 146 | 147 | if (bound > sblimit) { 148 | bound = sblimit; 149 | } 150 | 151 | // Read the allocation information 152 | for (let sb = 0; sb < bound; sb++) { 153 | this.allocation[0][sb] = this.readAllocation(sb, tab3); 154 | this.allocation[1][sb] = this.readAllocation(sb, tab3); 155 | } 156 | 157 | for (let sb = bound; sb < sblimit; sb++) { 158 | this.allocation[0][sb] = this.allocation[1][sb] = this.readAllocation(sb, tab3); 159 | } 160 | 161 | // Read scale factor selector information 162 | const channels = mode === MP2.MODE.MONO ? 1 : 2; 163 | for (let sb = 0; sb < sblimit; sb++) { 164 | for (let ch = 0; ch < channels; ch++) { 165 | if (this.allocation[ch][sb]) { 166 | this.scaleFactorInfo[ch][sb] = this.bits.read(2); 167 | } 168 | } 169 | if (mode === MP2.MODE.MONO) { 170 | this.scaleFactorInfo[1][sb] = this.scaleFactorInfo[0][sb]; 171 | } 172 | } 173 | 174 | // Read scale factors 175 | for (let sb = 0; sb < sblimit; sb++) { 176 | for (let ch = 0; ch < channels; ch++) { 177 | if (this.allocation[ch][sb]) { 178 | const sf = this.scaleFactor[ch][sb]; 179 | switch (this.scaleFactorInfo[ch][sb]) { 180 | case 0: 181 | sf[0] = this.bits.read(6); 182 | sf[1] = this.bits.read(6); 183 | sf[2] = this.bits.read(6); 184 | break; 185 | case 1: 186 | sf[0] = sf[1] = this.bits.read(6); 187 | sf[2] = this.bits.read(6); 188 | break; 189 | case 2: 190 | sf[0] = sf[1] = sf[2] = this.bits.read(6); 191 | break; 192 | case 3: 193 | sf[0] = this.bits.read(6); 194 | sf[1] = sf[2] = this.bits.read(6); 195 | break; 196 | default: 197 | } 198 | } 199 | } 200 | if (mode === MP2.MODE.MONO) { 201 | // eslint-disable-next-line prefer-destructuring 202 | this.scaleFactor[1][sb][0] = this.scaleFactor[0][sb][0]; 203 | // eslint-disable-next-line prefer-destructuring 204 | this.scaleFactor[1][sb][1] = this.scaleFactor[0][sb][1]; 205 | // eslint-disable-next-line prefer-destructuring 206 | this.scaleFactor[1][sb][2] = this.scaleFactor[0][sb][2]; 207 | } 208 | } 209 | 210 | // Coefficient input and reconstruction 211 | let outPos = 0; 212 | for (let part = 0; part < 3; part++) { 213 | for (let granule = 0; granule < 4; granule++) { 214 | // Read the samples 215 | for (let sb = 0; sb < bound; sb++) { 216 | this.readSamples(0, sb, part); 217 | this.readSamples(1, sb, part); 218 | } 219 | for (let sb = bound; sb < sblimit; sb++) { 220 | this.readSamples(0, sb, part); 221 | // eslint-disable-next-line prefer-destructuring 222 | this.sample[1][sb][0] = this.sample[0][sb][0]; 223 | // eslint-disable-next-line prefer-destructuring 224 | this.sample[1][sb][1] = this.sample[0][sb][1]; 225 | // eslint-disable-next-line prefer-destructuring 226 | this.sample[1][sb][2] = this.sample[0][sb][2]; 227 | } 228 | for (let sb = sblimit; sb < 32; sb++) { 229 | this.sample[0][sb][0] = 0; 230 | this.sample[0][sb][1] = 0; 231 | this.sample[0][sb][2] = 0; 232 | this.sample[1][sb][0] = 0; 233 | this.sample[1][sb][1] = 0; 234 | this.sample[1][sb][2] = 0; 235 | } 236 | 237 | // Synthesis loop 238 | for (let p = 0; p < 3; p++) { 239 | // Shifting step 240 | this.VPos = (this.VPos - 64) & 1023; 241 | 242 | for (let ch = 0; ch < 2; ch++) { 243 | MP2.MatrixTransform(this.sample[ch], p, this.V[ch], this.VPos); 244 | 245 | // Build U, windowing, calculate output 246 | Fill(this.U, 0); 247 | 248 | let dIndex = 512 - (this.VPos >> 1); 249 | let vIndex = this.VPos % 128 >> 1; 250 | while (vIndex < 1024) { 251 | for (let i = 0; i < 32; ++i) { 252 | this.U[i] += this.D[dIndex++] * this.V[ch][vIndex++]; 253 | } 254 | 255 | vIndex += 128 - 32; 256 | dIndex += 64 - 32; 257 | } 258 | 259 | vIndex = 128 - 32 + 1024 - vIndex; 260 | dIndex -= 512 - 32; 261 | while (vIndex < 1024) { 262 | for (let i = 0; i < 32; ++i) { 263 | this.U[i] += this.D[dIndex++] * this.V[ch][vIndex++]; 264 | } 265 | 266 | vIndex += 128 - 32; 267 | dIndex += 64 - 32; 268 | } 269 | 270 | // Output samples 271 | const outChannel = ch === 0 ? left : right; 272 | for (let j = 0; j < 32; j++) { 273 | outChannel[outPos + j] = this.U[j] / 2147418112; 274 | } 275 | } // End of synthesis channel loop 276 | outPos += 32; 277 | } // End of synthesis sub-block loop 278 | } // Decoding of the granule finished 279 | } 280 | 281 | this.sampleRate = sampleRate; 282 | return frameSize; 283 | } 284 | 285 | readAllocation(sb, tab3) { 286 | const tab4 = MP2.QUANT_LUT_STEP_3[tab3][sb]; 287 | const qtab = MP2.QUANT_LUT_STEP4[tab4 & 15][this.bits.read(tab4 >> 4)]; 288 | return qtab ? MP2.QUANT_TAB[qtab - 1] : 0; 289 | } 290 | 291 | readSamples(ch, sb, part) { 292 | const q = this.allocation[ch][sb]; 293 | let sf = this.scaleFactor[ch][sb][part]; 294 | const sample = this.sample[ch][sb]; 295 | let val = 0; 296 | 297 | if (!q) { 298 | // No bits allocated for this subband 299 | sample[0] = sample[1] = sample[2] = 0; 300 | return; 301 | } 302 | 303 | // Resolve scalefactor 304 | if (sf === 63) { 305 | sf = 0; 306 | } else { 307 | const shift = (sf / 3) | 0; 308 | sf = (MP2.SCALEFACTOR_BASE[sf % 3] + ((1 << shift) >> 1)) >> shift; 309 | } 310 | 311 | // Decode samples 312 | let adj = q.levels; 313 | if (q.group) { 314 | // Decode grouped samples 315 | val = this.bits.read(q.bits); 316 | sample[0] = val % adj; 317 | val = (val / adj) | 0; 318 | sample[1] = val % adj; 319 | sample[2] = (val / adj) | 0; 320 | } else { 321 | // Decode direct samples 322 | sample[0] = this.bits.read(q.bits); 323 | sample[1] = this.bits.read(q.bits); 324 | sample[2] = this.bits.read(q.bits); 325 | } 326 | 327 | // Postmultiply samples 328 | const scale = (65536 / (adj + 1)) | 0; 329 | adj = ((adj + 1) >> 1) - 1; 330 | 331 | val = (adj - sample[0]) * scale; 332 | sample[0] = (val * (sf >> 12) + ((val * (sf & 4095) + 2048) >> 12)) >> 12; 333 | 334 | val = (adj - sample[1]) * scale; 335 | sample[1] = (val * (sf >> 12) + ((val * (sf & 4095) + 2048) >> 12)) >> 12; 336 | 337 | val = (adj - sample[2]) * scale; 338 | sample[2] = (val * (sf >> 12) + ((val * (sf & 4095) + 2048) >> 12)) >> 12; 339 | } 340 | 341 | static MatrixTransform(s, ss, d, dp) { 342 | let t01; 343 | let t02; 344 | let t03; 345 | let t04; 346 | let t05; 347 | let t06; 348 | let t07; 349 | let t08; 350 | let t09; 351 | let t10; 352 | let t11; 353 | let t12; 354 | let t13; 355 | let t14; 356 | let t15; 357 | let t16; 358 | let t17; 359 | let t18; 360 | let t19; 361 | let t20; 362 | let t21; 363 | let t22; 364 | let t23; 365 | let t24; 366 | let t25; 367 | let t26; 368 | let t27; 369 | let t28; 370 | let t29; 371 | let t30; 372 | let t31; 373 | let t32; 374 | let t33; 375 | 376 | t01 = s[0][ss] + s[31][ss]; 377 | t02 = (s[0][ss] - s[31][ss]) * 0.500602998235; 378 | t03 = s[1][ss] + s[30][ss]; 379 | t04 = (s[1][ss] - s[30][ss]) * 0.505470959898; 380 | t05 = s[2][ss] + s[29][ss]; 381 | t06 = (s[2][ss] - s[29][ss]) * 0.515447309923; 382 | t07 = s[3][ss] + s[28][ss]; 383 | t08 = (s[3][ss] - s[28][ss]) * 0.53104259109; 384 | t09 = s[4][ss] + s[27][ss]; 385 | t10 = (s[4][ss] - s[27][ss]) * 0.553103896034; 386 | t11 = s[5][ss] + s[26][ss]; 387 | t12 = (s[5][ss] - s[26][ss]) * 0.582934968206; 388 | t13 = s[6][ss] + s[25][ss]; 389 | t14 = (s[6][ss] - s[25][ss]) * 0.622504123036; 390 | t15 = s[7][ss] + s[24][ss]; 391 | t16 = (s[7][ss] - s[24][ss]) * 0.674808341455; 392 | t17 = s[8][ss] + s[23][ss]; 393 | t18 = (s[8][ss] - s[23][ss]) * 0.744536271002; 394 | t19 = s[9][ss] + s[22][ss]; 395 | t20 = (s[9][ss] - s[22][ss]) * 0.839349645416; 396 | t21 = s[10][ss] + s[21][ss]; 397 | t22 = (s[10][ss] - s[21][ss]) * 0.972568237862; 398 | t23 = s[11][ss] + s[20][ss]; 399 | t24 = (s[11][ss] - s[20][ss]) * 1.16943993343; 400 | t25 = s[12][ss] + s[19][ss]; 401 | t26 = (s[12][ss] - s[19][ss]) * 1.48416461631; 402 | t27 = s[13][ss] + s[18][ss]; 403 | t28 = (s[13][ss] - s[18][ss]) * 2.05778100995; 404 | t29 = s[14][ss] + s[17][ss]; 405 | t30 = (s[14][ss] - s[17][ss]) * 3.40760841847; 406 | t31 = s[15][ss] + s[16][ss]; 407 | t32 = (s[15][ss] - s[16][ss]) * 10.1900081235; 408 | 409 | t33 = t01 + t31; 410 | t31 = (t01 - t31) * 0.502419286188; 411 | t01 = t03 + t29; 412 | t29 = (t03 - t29) * 0.52249861494; 413 | t03 = t05 + t27; 414 | t27 = (t05 - t27) * 0.566944034816; 415 | t05 = t07 + t25; 416 | t25 = (t07 - t25) * 0.64682178336; 417 | t07 = t09 + t23; 418 | t23 = (t09 - t23) * 0.788154623451; 419 | t09 = t11 + t21; 420 | t21 = (t11 - t21) * 1.06067768599; 421 | t11 = t13 + t19; 422 | t19 = (t13 - t19) * 1.72244709824; 423 | t13 = t15 + t17; 424 | t17 = (t15 - t17) * 5.10114861869; 425 | t15 = t33 + t13; 426 | t13 = (t33 - t13) * 0.509795579104; 427 | t33 = t01 + t11; 428 | t01 = (t01 - t11) * 0.601344886935; 429 | t11 = t03 + t09; 430 | t09 = (t03 - t09) * 0.899976223136; 431 | t03 = t05 + t07; 432 | t07 = (t05 - t07) * 2.56291544774; 433 | t05 = t15 + t03; 434 | t15 = (t15 - t03) * 0.541196100146; 435 | t03 = t33 + t11; 436 | t11 = (t33 - t11) * 1.30656296488; 437 | t33 = t05 + t03; 438 | t05 = (t05 - t03) * 0.707106781187; 439 | t03 = t15 + t11; 440 | t15 = (t15 - t11) * 0.707106781187; 441 | t03 += t15; 442 | t11 = t13 + t07; 443 | t13 = (t13 - t07) * 0.541196100146; 444 | t07 = t01 + t09; 445 | t09 = (t01 - t09) * 1.30656296488; 446 | t01 = t11 + t07; 447 | t07 = (t11 - t07) * 0.707106781187; 448 | t11 = t13 + t09; 449 | t13 = (t13 - t09) * 0.707106781187; 450 | t11 += t13; 451 | t01 += t11; 452 | t11 += t07; 453 | t07 += t13; 454 | t09 = t31 + t17; 455 | t31 = (t31 - t17) * 0.509795579104; 456 | t17 = t29 + t19; 457 | t29 = (t29 - t19) * 0.601344886935; 458 | t19 = t27 + t21; 459 | t21 = (t27 - t21) * 0.899976223136; 460 | t27 = t25 + t23; 461 | t23 = (t25 - t23) * 2.56291544774; 462 | t25 = t09 + t27; 463 | t09 = (t09 - t27) * 0.541196100146; 464 | t27 = t17 + t19; 465 | t19 = (t17 - t19) * 1.30656296488; 466 | t17 = t25 + t27; 467 | t27 = (t25 - t27) * 0.707106781187; 468 | t25 = t09 + t19; 469 | t19 = (t09 - t19) * 0.707106781187; 470 | t25 += t19; 471 | t09 = t31 + t23; 472 | t31 = (t31 - t23) * 0.541196100146; 473 | t23 = t29 + t21; 474 | t21 = (t29 - t21) * 1.30656296488; 475 | t29 = t09 + t23; 476 | t23 = (t09 - t23) * 0.707106781187; 477 | t09 = t31 + t21; 478 | t31 = (t31 - t21) * 0.707106781187; 479 | t09 += t31; 480 | t29 += t09; 481 | t09 += t23; 482 | t23 += t31; 483 | t17 += t29; 484 | t29 += t25; 485 | t25 += t09; 486 | t09 += t27; 487 | t27 += t23; 488 | t23 += t19; 489 | t19 += t31; 490 | t21 = t02 + t32; 491 | t02 = (t02 - t32) * 0.502419286188; 492 | t32 = t04 + t30; 493 | t04 = (t04 - t30) * 0.52249861494; 494 | t30 = t06 + t28; 495 | t28 = (t06 - t28) * 0.566944034816; 496 | t06 = t08 + t26; 497 | t08 = (t08 - t26) * 0.64682178336; 498 | t26 = t10 + t24; 499 | t10 = (t10 - t24) * 0.788154623451; 500 | t24 = t12 + t22; 501 | t22 = (t12 - t22) * 1.06067768599; 502 | t12 = t14 + t20; 503 | t20 = (t14 - t20) * 1.72244709824; 504 | t14 = t16 + t18; 505 | t16 = (t16 - t18) * 5.10114861869; 506 | t18 = t21 + t14; 507 | t14 = (t21 - t14) * 0.509795579104; 508 | t21 = t32 + t12; 509 | t32 = (t32 - t12) * 0.601344886935; 510 | t12 = t30 + t24; 511 | t24 = (t30 - t24) * 0.899976223136; 512 | t30 = t06 + t26; 513 | t26 = (t06 - t26) * 2.56291544774; 514 | t06 = t18 + t30; 515 | t18 = (t18 - t30) * 0.541196100146; 516 | t30 = t21 + t12; 517 | t12 = (t21 - t12) * 1.30656296488; 518 | t21 = t06 + t30; 519 | t30 = (t06 - t30) * 0.707106781187; 520 | t06 = t18 + t12; 521 | t12 = (t18 - t12) * 0.707106781187; 522 | t06 += t12; 523 | t18 = t14 + t26; 524 | t26 = (t14 - t26) * 0.541196100146; 525 | t14 = t32 + t24; 526 | t24 = (t32 - t24) * 1.30656296488; 527 | t32 = t18 + t14; 528 | t14 = (t18 - t14) * 0.707106781187; 529 | t18 = t26 + t24; 530 | t24 = (t26 - t24) * 0.707106781187; 531 | t18 += t24; 532 | t32 += t18; 533 | t18 += t14; 534 | t26 = t14 + t24; 535 | t14 = t02 + t16; 536 | t02 = (t02 - t16) * 0.509795579104; 537 | t16 = t04 + t20; 538 | t04 = (t04 - t20) * 0.601344886935; 539 | t20 = t28 + t22; 540 | t22 = (t28 - t22) * 0.899976223136; 541 | t28 = t08 + t10; 542 | t10 = (t08 - t10) * 2.56291544774; 543 | t08 = t14 + t28; 544 | t14 = (t14 - t28) * 0.541196100146; 545 | t28 = t16 + t20; 546 | t20 = (t16 - t20) * 1.30656296488; 547 | t16 = t08 + t28; 548 | t28 = (t08 - t28) * 0.707106781187; 549 | t08 = t14 + t20; 550 | t20 = (t14 - t20) * 0.707106781187; 551 | t08 += t20; 552 | t14 = t02 + t10; 553 | t02 = (t02 - t10) * 0.541196100146; 554 | t10 = t04 + t22; 555 | t22 = (t04 - t22) * 1.30656296488; 556 | t04 = t14 + t10; 557 | t10 = (t14 - t10) * 0.707106781187; 558 | t14 = t02 + t22; 559 | t02 = (t02 - t22) * 0.707106781187; 560 | t14 += t02; 561 | t04 += t14; 562 | t14 += t10; 563 | t10 += t02; 564 | t16 += t04; 565 | t04 += t08; 566 | t08 += t14; 567 | t14 += t28; 568 | t28 += t10; 569 | t10 += t20; 570 | t20 += t02; 571 | t21 += t16; 572 | t16 += t32; 573 | t32 += t04; 574 | t04 += t06; 575 | t06 += t08; 576 | t08 += t18; 577 | t18 += t14; 578 | t14 += t30; 579 | t30 += t28; 580 | t28 += t26; 581 | t26 += t10; 582 | t10 += t12; 583 | t12 += t20; 584 | t20 += t24; 585 | t24 += t02; 586 | 587 | d[dp + 48] = -t33; 588 | d[dp + 49] = d[dp + 47] = -t21; 589 | d[dp + 50] = d[dp + 46] = -t17; 590 | d[dp + 51] = d[dp + 45] = -t16; 591 | d[dp + 52] = d[dp + 44] = -t01; 592 | d[dp + 53] = d[dp + 43] = -t32; 593 | d[dp + 54] = d[dp + 42] = -t29; 594 | d[dp + 55] = d[dp + 41] = -t04; 595 | d[dp + 56] = d[dp + 40] = -t03; 596 | d[dp + 57] = d[dp + 39] = -t06; 597 | d[dp + 58] = d[dp + 38] = -t25; 598 | d[dp + 59] = d[dp + 37] = -t08; 599 | d[dp + 60] = d[dp + 36] = -t11; 600 | d[dp + 61] = d[dp + 35] = -t18; 601 | d[dp + 62] = d[dp + 34] = -t09; 602 | d[dp + 63] = d[dp + 33] = -t14; 603 | d[dp + 32] = -t05; 604 | d[dp + 0] = t05; 605 | d[dp + 31] = -t30; 606 | d[dp + 1] = t30; 607 | d[dp + 30] = -t27; 608 | d[dp + 2] = t27; 609 | d[dp + 29] = -t28; 610 | d[dp + 3] = t28; 611 | d[dp + 28] = -t07; 612 | d[dp + 4] = t07; 613 | d[dp + 27] = -t26; 614 | d[dp + 5] = t26; 615 | d[dp + 26] = -t23; 616 | d[dp + 6] = t23; 617 | d[dp + 25] = -t10; 618 | d[dp + 7] = t10; 619 | d[dp + 24] = -t15; 620 | d[dp + 8] = t15; 621 | d[dp + 23] = -t12; 622 | d[dp + 9] = t12; 623 | d[dp + 22] = -t19; 624 | d[dp + 10] = t19; 625 | d[dp + 21] = -t20; 626 | d[dp + 11] = t20; 627 | d[dp + 20] = -t13; 628 | d[dp + 12] = t13; 629 | d[dp + 19] = -t24; 630 | d[dp + 13] = t24; 631 | d[dp + 18] = -t31; 632 | d[dp + 14] = t31; 633 | d[dp + 17] = -t02; 634 | d[dp + 15] = t02; 635 | d[dp + 16] = 0.0; 636 | } 637 | } 638 | 639 | MP2.FRAME_SYNC = 0x7ff; 640 | 641 | MP2.VERSION = { 642 | MPEG_2_5: 0x0, 643 | MPEG_2: 0x2, 644 | MPEG_1: 0x3, 645 | }; 646 | 647 | MP2.LAYER = { 648 | III: 0x1, 649 | II: 0x2, 650 | I: 0x3, 651 | }; 652 | 653 | MP2.MODE = { 654 | STEREO: 0x0, 655 | JOINT_STEREO: 0x1, 656 | DUAL_CHANNEL: 0x2, 657 | MONO: 0x3, 658 | }; 659 | 660 | MP2.SAMPLE_RATE = new Uint16Array([ 661 | 44100, 662 | 48000, 663 | 32000, 664 | 0, // MPEG-1 665 | 22050, 666 | 24000, 667 | 16000, 668 | 0, // MPEG-2 669 | ]); 670 | 671 | MP2.BIT_RATE = new Uint16Array([ 672 | 32, 673 | 48, 674 | 56, 675 | 64, 676 | 80, 677 | 96, 678 | 112, 679 | 128, 680 | 160, 681 | 192, 682 | 224, 683 | 256, 684 | 320, 685 | 384, // MPEG-1 686 | 8, 687 | 16, 688 | 24, 689 | 32, 690 | 40, 691 | 48, 692 | 56, 693 | 64, 694 | 80, 695 | 96, 696 | 112, 697 | 128, 698 | 144, 699 | 160, // MPEG-2 700 | ]); 701 | 702 | MP2.SCALEFACTOR_BASE = new Uint32Array([0x02000000, 0x01965fea, 0x01428a30]); 703 | 704 | MP2.SYNTHESIS_WINDOW = new Float32Array([ 705 | 0.0, -0.5, -0.5, -0.5, -0.5, -0.5, -0.5, -1.0, -1.0, -1.0, -1.0, -1.5, -1.5, -2.0, -2.0, -2.5, 706 | -2.5, -3.0, -3.5, -3.5, -4.0, -4.5, -5.0, -5.5, -6.5, -7.0, -8.0, -8.5, -9.5, -10.5, -12.0, -13.0, 707 | -14.5, -15.5, -17.5, -19.0, -20.5, -22.5, -24.5, -26.5, -29.0, -31.5, -34.0, -36.5, -39.5, -42.5, 708 | -45.5, -48.5, -52.0, -55.5, -58.5, -62.5, -66.0, -69.5, -73.5, -77.0, -80.5, -84.5, -88.0, -91.5, 709 | -95.0, -98.0, -101.0, -104.0, 106.5, 109.0, 111.0, 112.5, 113.5, 114.0, 114.0, 113.5, 112.0, 710 | 110.5, 107.5, 104.0, 100.0, 94.5, 88.5, 81.5, 73.0, 63.5, 53.0, 41.5, 28.5, 14.5, -1.0, -18.0, 711 | -36.0, -55.5, -76.5, -98.5, -122.0, -147.0, -173.5, -200.5, -229.5, -259.5, -290.5, -322.5, 712 | -355.5, -389.5, -424.0, -459.5, -495.5, -532.0, -568.5, -605.0, -641.5, -678.0, -714.0, -749.0, 713 | -783.5, -817.0, -849.0, -879.5, -908.5, -935.0, -959.5, -981.0, -1000.5, -1016.0, -1028.5, 714 | -1037.5, -1042.5, -1043.5, -1040.0, -1031.5, 1018.5, 1000.0, 976.0, 946.5, 911.0, 869.5, 822.0, 715 | 767.5, 707.0, 640.0, 565.5, 485.0, 397.0, 302.5, 201.0, 92.5, -22.5, -144.0, -272.5, -407.0, 716 | -547.5, -694.0, -846.0, -1003.0, -1165.0, -1331.5, -1502.0, -1675.5, -1852.5, -2031.5, -2212.5, 717 | -2394.0, -2576.5, -2758.5, -2939.5, -3118.5, -3294.5, -3467.5, -3635.5, -3798.5, -3955.0, -4104.5, 718 | -4245.5, -4377.5, -4499.0, -4609.5, -4708.0, -4792.5, -4863.5, -4919.0, -4958.0, -4979.5, -4983.0, 719 | -4967.5, -4931.5, -4875.0, -4796.0, -4694.5, -4569.5, -4420.0, -4246.0, -4046.0, -3820.0, -3567.0, 720 | 3287.0, 2979.5, 2644.0, 2280.5, 1888.0, 1467.5, 1018.5, 541.0, 35.0, -499.0, -1061.0, -1650.0, 721 | -2266.5, -2909.0, -3577.0, -4270.0, -4987.5, -5727.5, -6490.0, -7274.0, -8077.5, -8899.5, -9739.0, 722 | -10594.5, -11464.5, -12347.0, -13241.0, -14144.5, -15056.0, -15973.5, -16895.5, -17820.0, 723 | -18744.5, -19668.0, -20588.0, -21503.0, -22410.5, -23308.5, -24195.0, -25068.5, -25926.5, 724 | -26767.0, -27589.0, -28389.0, -29166.5, -29919.0, -30644.5, -31342.0, -32009.5, -32645.0, 725 | -33247.0, -33814.5, -34346.0, -34839.5, -35295.0, -35710.0, -36084.5, -36417.5, -36707.5, 726 | -36954.0, -37156.5, -37315.0, -37428.0, -37496.0, 37519.0, 37496.0, 37428.0, 37315.0, 37156.5, 727 | 36954.0, 36707.5, 36417.5, 36084.5, 35710.0, 35295.0, 34839.5, 34346.0, 33814.5, 33247.0, 32645.0, 728 | 32009.5, 31342.0, 30644.5, 29919.0, 29166.5, 28389.0, 27589.0, 26767.0, 25926.5, 25068.5, 24195.0, 729 | 23308.5, 22410.5, 21503.0, 20588.0, 19668.0, 18744.5, 17820.0, 16895.5, 15973.5, 15056.0, 14144.5, 730 | 13241.0, 12347.0, 11464.5, 10594.5, 9739.0, 8899.5, 8077.5, 7274.0, 6490.0, 5727.5, 4987.5, 731 | 4270.0, 3577.0, 2909.0, 2266.5, 1650.0, 1061.0, 499.0, -35.0, -541.0, -1018.5, -1467.5, -1888.0, 732 | -2280.5, -2644.0, -2979.5, 3287.0, 3567.0, 3820.0, 4046.0, 4246.0, 4420.0, 4569.5, 4694.5, 4796.0, 733 | 4875.0, 4931.5, 4967.5, 4983.0, 4979.5, 4958.0, 4919.0, 4863.5, 4792.5, 4708.0, 4609.5, 4499.0, 734 | 4377.5, 4245.5, 4104.5, 3955.0, 3798.5, 3635.5, 3467.5, 3294.5, 3118.5, 2939.5, 2758.5, 2576.5, 735 | 2394.0, 2212.5, 2031.5, 1852.5, 1675.5, 1502.0, 1331.5, 1165.0, 1003.0, 846.0, 694.0, 547.5, 736 | 407.0, 272.5, 144.0, 22.5, -92.5, -201.0, -302.5, -397.0, -485.0, -565.5, -640.0, -707.0, -767.5, 737 | -822.0, -869.5, -911.0, -946.5, -976.0, -1000.0, 1018.5, 1031.5, 1040.0, 1043.5, 1042.5, 1037.5, 738 | 1028.5, 1016.0, 1000.5, 981.0, 959.5, 935.0, 908.5, 879.5, 849.0, 817.0, 783.5, 749.0, 714.0, 739 | 678.0, 641.5, 605.0, 568.5, 532.0, 495.5, 459.5, 424.0, 389.5, 355.5, 322.5, 290.5, 259.5, 229.5, 740 | 200.5, 173.5, 147.0, 122.0, 98.5, 76.5, 55.5, 36.0, 18.0, 1.0, -14.5, -28.5, -41.5, -53.0, -63.5, 741 | -73.0, -81.5, -88.5, -94.5, -100.0, -104.0, -107.5, -110.5, -112.0, -113.5, -114.0, -114.0, 742 | -113.5, -112.5, -111.0, -109.0, 106.5, 104.0, 101.0, 98.0, 95.0, 91.5, 88.0, 84.5, 80.5, 77.0, 743 | 73.5, 69.5, 66.0, 62.5, 58.5, 55.5, 52.0, 48.5, 45.5, 42.5, 39.5, 36.5, 34.0, 31.5, 29.0, 26.5, 744 | 24.5, 22.5, 20.5, 19.0, 17.5, 15.5, 14.5, 13.0, 12.0, 10.5, 9.5, 8.5, 8.0, 7.0, 6.5, 5.5, 5.0, 745 | 4.5, 4.0, 3.5, 3.5, 3.0, 2.5, 2.5, 2.0, 2.0, 1.5, 1.5, 1.0, 1.0, 1.0, 1.0, 0.5, 0.5, 0.5, 0.5, 746 | 0.5, 0.5, 747 | ]); 748 | 749 | // Quantizer lookup, step 1: bitrate classes 750 | MP2.QUANT_LUT_STEP_1 = [ 751 | // 32, 48, 56, 64, 80, 96,112,128,160,192,224,256,320,384 <- bitrate 752 | [0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2], // mono 753 | // 16, 24, 28, 32, 40, 48, 56, 64, 80, 96,112,128,160,192 <- bitrate / chan 754 | [0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2], // stereo 755 | ]; 756 | 757 | // Quantizer lookup, step 2: bitrate class, sample rate -> B2 table idx, sblimit 758 | MP2.QUANT_TAB = { 759 | A: 27 | 64, // Table 3-B.2a: high-rate, sblimit = 27 760 | B: 30 | 64, // Table 3-B.2b: high-rate, sblimit = 30 761 | C: 8, // Table 3-B.2c: low-rate, sblimit = 8 762 | D: 12, // Table 3-B.2d: low-rate, sblimit = 12 763 | }; 764 | 765 | MP2.QUANT_LUT_STEP_2 = [ 766 | // 44.1 kHz, 48 kHz, 32 kHz 767 | [MP2.QUANT_TAB.C, MP2.QUANT_TAB.C, MP2.QUANT_TAB.D], // 32 - 48 kbit/sec/ch 768 | [MP2.QUANT_TAB.A, MP2.QUANT_TAB.A, MP2.QUANT_TAB.A], // 56 - 80 kbit/sec/ch 769 | [MP2.QUANT_TAB.B, MP2.QUANT_TAB.A, MP2.QUANT_TAB.B], // 96+ kbit/sec/ch 770 | ]; 771 | 772 | // Quantizer lookup, step 3: B2 table, subband -> nbal, row index 773 | // (upper 4 bits: nbal, lower 4 bits: row index) 774 | MP2.QUANT_LUT_STEP_3 = [ 775 | // Low-rate table (3-B.2c and 3-B.2d) 776 | [0x44, 0x44, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34], 777 | // High-rate table (3-B.2a and 3-B.2b) 778 | [ 779 | 0x43, 0x43, 0x43, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x31, 0x31, 0x31, 0x31, 0x31, 780 | 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 781 | ], 782 | // MPEG-2 LSR table (B.2 in ISO 13818-3) 783 | [ 784 | 0x45, 0x45, 0x45, 0x45, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x24, 0x24, 0x24, 0x24, 0x24, 785 | 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 786 | ], 787 | ]; 788 | 789 | // Quantizer lookup, step 4: table row, allocation[] value -> quant table index 790 | MP2.QUANT_LUT_STEP4 = [ 791 | [0, 1, 2, 17], 792 | [0, 1, 2, 3, 4, 5, 6, 17], 793 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17], 794 | [0, 1, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], 795 | [0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17], 796 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], 797 | ]; 798 | 799 | MP2.QUANT_TAB = [ 800 | { levels: 3, group: 1, bits: 5 }, // 1 801 | { levels: 5, group: 1, bits: 7 }, // 2 802 | { levels: 7, group: 0, bits: 3 }, // 3 803 | { levels: 9, group: 1, bits: 10 }, // 4 804 | { levels: 15, group: 0, bits: 4 }, // 5 805 | { levels: 31, group: 0, bits: 5 }, // 6 806 | { levels: 63, group: 0, bits: 6 }, // 7 807 | { levels: 127, group: 0, bits: 7 }, // 8 808 | { levels: 255, group: 0, bits: 8 }, // 9 809 | { levels: 511, group: 0, bits: 9 }, // 10 810 | { levels: 1023, group: 0, bits: 10 }, // 11 811 | { levels: 2047, group: 0, bits: 11 }, // 12 812 | { levels: 4095, group: 0, bits: 12 }, // 13 813 | { levels: 8191, group: 0, bits: 13 }, // 14 814 | { levels: 16383, group: 0, bits: 14 }, // 15 815 | { levels: 32767, group: 0, bits: 15 }, // 16 816 | { levels: 65535, group: 0, bits: 16 }, // 17 817 | ]; 818 | 819 | export default MP2; 820 | -------------------------------------------------------------------------------- /src/lib/wasm/WASM_BINARY.js: -------------------------------------------------------------------------------- 1 | // get from jsmpeg 2 | export default 'AGFzbQEAAAAADwZkeWxpbmuA0MACBAAAAAE0CWAEf39/fwBgAX8Bf2ACf38Bf2ABfwBgAn9/AGABfwF9YAZ/f39/f38AYAN/f38Bf2AAAAJGBANlbnYOX19fYXNzZXJ0X2ZhaWwAAANlbnYFX3NicmsAAQNlbnYNX19tZW1vcnlfYmFzZQN/AANlbnYGbWVtb3J5AgCAAgM9PAIDAgEEBAMBBQEBAQEBAQEDBAMGAwQAAwAAAAIDAgEEBAEBAQEBAAACAwIBAgECAQEBAQMEAwMCBwcHCAYLAn8BQQALfwFBAAsHggYfEl9fcG9zdF9pbnN0YW50aWF0ZQA9BV9mcmVlADgHX21hbGxvYwAxB19tZW1jcHkAOghfbWVtbW92ZQA7B19tZW1zZXQAPBNfbXAyX2RlY29kZXJfY3JlYXRlAB0TX21wMl9kZWNvZGVyX2RlY29kZQAmFF9tcDJfZGVjb2Rlcl9kZXN0cm95AB4WX21wMl9kZWNvZGVyX2RpZF93cml0ZQAiFl9tcDJfZGVjb2Rlcl9nZXRfaW5kZXgAICFfbXAyX2RlY29kZXJfZ2V0X2xlZnRfY2hhbm5lbF9wdHIAJCJfbXAyX2RlY29kZXJfZ2V0X3JpZ2h0X2NoYW5uZWxfcHRyACUcX21wMl9kZWNvZGVyX2dldF9zYW1wbGVfcmF0ZQAjGl9tcDJfZGVjb2Rlcl9nZXRfd3JpdGVfcHRyAB8WX21wMl9kZWNvZGVyX3NldF9pbmRleAAhFV9tcGVnMV9kZWNvZGVyX2NyZWF0ZQACFV9tcGVnMV9kZWNvZGVyX2RlY29kZQARFl9tcGVnMV9kZWNvZGVyX2Rlc3Ryb3kAAxhfbXBlZzFfZGVjb2Rlcl9kaWRfd3JpdGUABxlfbXBlZzFfZGVjb2Rlcl9nZXRfY2JfcHRyABAdX21wZWcxX2RlY29kZXJfZ2V0X2NvZGVkX3NpemUACxlfbXBlZzFfZGVjb2Rlcl9nZXRfY3JfcHRyAA8dX21wZWcxX2RlY29kZXJfZ2V0X2ZyYW1lX3JhdGUAChlfbXBlZzFfZGVjb2Rlcl9nZXRfaGVpZ2h0AA0YX21wZWcxX2RlY29kZXJfZ2V0X2luZGV4AAUYX21wZWcxX2RlY29kZXJfZ2V0X3dpZHRoAAwcX21wZWcxX2RlY29kZXJfZ2V0X3dy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BQJ/IANBzQBHDQIgACgCgAFBBhAwIQMCQAJAIAAoAoABQQgQMCIEIgYEQCAGQYABRgRADAIFDAMLAAsgACgCgAFBCBAwDAILIAAoAoABQQgQMEGAfmoMAQsgBEGAfmogBCAEQYABShsLCyEEDAELIAZB/wFxIgNBACADayAAKAKAAUEBEDBFGyEEIAZBCHUhAwsgAiADaiIGIwBBQGtqLQAAIgMgBWotAAAgACgCREEAIARBAXQiAkEfdUEBciAAKAJcGyACamxsIgJBBHVBAEEBQX8gAkEPShsgAkEQcRtrIgJBgHAgAkGAcEobIQIgAEGcAWogA0ECdGogAyMAQZA1amotAAAgAkH/DyACQf8PSBtsNgIAIAZBAWohAgwBCwsgAUEESARAIABBhAFqIQUgAUEDdEEIcSAAKAJUIAAoAhgiAyAAKAJQbGpBBHRyIANBA3RBACABQQJxG2ohBAUgAEGMAWogAEGIAWogAUEERhshBSAAKAJQIAAoAhgiAUECdGwgACgCVEEDdGohBCABQQF1IQMLIANBeGohAyAFKAIAIQUgAkEBRiECIABBnAFqIQEgACgCXARAIAIEQCABKAIAQYABakEIdSAFIAQgAxAYIAFBADYCAAUgARAZIAEgBSAEIAMQGiABQQBBgAIQPBoLBSACBEAgASgCAEGAAWpBCHUgBSAEIAMQGyABQQA2AgAFIAEQGSABIAUgBCADEBwgAUEAQYACEDwaCwsL7AYAIAEgAmogAEEAIABBAEobIgBB/wEgAEH/AUgbQf8BcSIAOgAAIAEgAkEBamogADoAACABIAJBAmpqIAA6AAAgASACQQNqaiAAOgAAIAEgAkEEamogADoAACABIAJBBWpqIAA6AAAgASACQQZqaiAAOgAAIAEgAkEHamogADoAACABIAIgA0EIaiIDaiICaiAAOgAAIAEgAkEBamogADoAACABIAJBAmpqIAA6AAAgASACQQNqaiAAOgAAIAEgAkEEamogADoAACABIAJBBWpqIAA6AAAgASACQQZqaiAAOgAAIAEgAkEHamogADoAACABIAIgA2oiAmogADoAACABIAJBAWpqIAA6AAAgASACQQJqaiAAOgAAIAEgAkEDamogADoAACABIAJBBGpqIAA6AAAgASACQQVqaiAAOgAAIAEgAkEGamogADoAACABIAJBB2pqIAA6AAAgASACIANqIgJqIAA6AAAgASACQQFqaiAAOgAAIAEgAkECamogADoAACABIAJBA2pqIAA6AAAgASACQQRqaiAAOgAAIAEgAkEFamogADoAACABIAJBBmpqIAA6AAAgASACQQdqaiAAOgAAIAEgAiADaiICaiAAOgAAIAEgAkEBamogADoAACABIAJBAmpqIAA6AAAgASACQQNqaiAAOgAAIAEgAkEEamogADoAACABIAJBBWpqIAA6AAAgASACQQZqaiAAOgAAIAEgAkEHamogADoAACABIAIgA2oiAmogADoAACABIAJBAWpqIAA6AAAgASACQQJqaiAAOgAAIAEgAkEDamogADoAACABIAJBBGpqIAA6AAAgASACQQVqaiAAOgAAIAEgAkEGamogADoAACABIAJBB2pqIAA6AAAgASACIANqIgJqIAA6AAAgASACQQFqaiAAOgAAIAEgAkECamogADoAACABIAJBA2pqIAA6AAAgASACQQRqaiAAOgAAIAEgAkEFamogADoAACABIAJBBmpqIAA6AAAgASACQQdqaiAAOgAAIAEgAiADaiICaiAAOgAAIAEgAkEBamogADoAACABIAJBAmpqIAA6AAAgASACQQNqaiAAOgAAIAEgAkEEamogADoAACABIAJBBWpqIAA6AAAgASACQQZqaiAAOgAAIAEgAkEHamogADoAAAubBgEUfwNAIAFBEGpBAnQgAGoiBygCACIGIAFBMGpBAnQgAGoiDSgCACIJaiEFIAFBCGpBAnQgAGoiDigCACICIAFBOGpBAnQgAGoiDygCACIDaiEEIAIgA2siEEHZA2wgAUEoakECdCAAaiIKKAIAIgIgAUEYakECdCAAaiIRKAIAIgNrIgtBvH5sQYABampBCHUgBCACIANqIghqIgJrIgMgBCAIa0HqAmxBgAFqQQh1ayEEIAFBAnQgAGoiCCgCACIMIAFBIGpBAnQgAGoiEigCACITayIUIAYgCWtB6gJsQYABakEIdSAFayIJaiEGIAggAiAMIBNqIgggBWoiDGo2AgAgDiADIAZqNgIAIAcgFCAJayIHIARrNgIAIBEgC0HZA2xBgAFqIBBBxAFsakEIdSAEaiIJIAggBWsiBWo2AgAgEiAFIAlrNgIAIAogBCAHajYCACANIAYgA2s2AgAgDyAMIAJr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aiABQQxsaiAAKAIIQQYQMCICNgLUAiAAIANBgANsaiABQQxsaiACNgLQAiAFIAI2AgAMAgsgBSAAKAIIQQYQMDYCACAAIANBgANsaiABQQxsaiAAKAIIQQYQMCICNgLUAiAAIANBgANsaiABQQxsaiACNgLQAgsLCyADQQFqIgMgCEkNAAsgAUEBaiIBIAtHDQALCwsLaiALQSBJIRsgAEHMtgFqIQQgAEHMCGohHCAAQcz2AGohHSAAQcwLaiEeIABBzJYBaiEfA0BBACEWIBQhAwNAIBgEQEEAIQEDQCAAQQAgASAOECggAEEBIAEgDhAoIAFBAWoiASANSA0ACwsgGQRAIA0hAQNAIABBACABIA4QKCAAQcwLaiABQQxsaiAAQcwIaiABQQxsaigCADYCACAAIAFBDGxqQdALaiAAIAFBDGxqQdAIaigCADYCACAAIAFBDGxqQdQLaiAAIAFBDGxqQdQIaigCADYCACABQQFqIgEgC0gNAAsLIBsEQCALIQEDQCAAQcwIaiABQQxsakEANgIAIAAgAUEMbGpB0AhqQQA2AgAgACABQQxsakHUCGpBADYCACAAQcwLaiABQQxsakEANgIAIAAgAUEMbGpB0AtqQQA2AgAgACABQQxsakHUC2pBADYCACABQQFqIgFBIEcNAAsLIAAoAgQhASADIRFBACESA0AgACABQcAHakH/B3EiATYCBCAcIBIgHSABECkgBEIANwIAIARCADcCCCAEQgA3AhAgBEIANwIYIARCADcCICAEQgA3AiggBEIANwIwIARCADcCOCAEQUBrQgA3AgAgBEIANwJIIARCADcCUCAEQgA3AlggBEIANwJgIARCADcCaCAEQgA3AnAgBEIANwJ4IAAoAgQiD0EBdSETQf8HIA9BgAFvQQF1IgxrIgFBgH9xIRUgAUEHdkEGdEHABGohEEGABCATayEGIAwhAQNAIAYhB0EAIQkgASECA0AgB0EBaiEIIAJBAWohBSAAQcy2AWogCUECdGoiCiAAQczWAGogB0ECdGoqAgAgAEHM9gBqIAJBAnRqKgIAlCAKKAIAspKoNgIAIAlBAWoiCUEgRwRAIAghByAFIQIMAQsLIAFBgAFqIQIgBkFAayEGIAFBgAdIBEAgAiEBDAELC0HgByAMIBVqayIBQYAISARAIBAgE2tBoHxqIQYD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IkogWCBLk7tEujBFka7n9D+itiJLk7tEuEt/Zp6g5j+itiI6kiEqIANBMGpBAnQgAmogTSA8kow4AgAgA0EvakECdCACaiApICiSIEwgSZIgJJIiKJKMIik4AgAgA0ExakECdCACaiApOAIAIANBLmpBAnQgAmogRiBFkiAjkowiKTgCACADQTJqQQJ0IAJqICk4AgAgA0EtakECdCACaiAwIC2SIEAgPpIgJpIiKZIiPCAokowiKDgCACADQTNqQQJ0IAJqICg4AgAgA0EsakECdCACaiA2IE+SIFEgUJIgNZIiKJKMIjY4AgAgA0E0akECdCACaiA2OAIAIANBK2pBAnQgAmogPCA/IDiSIDOSIjwgJJIiJJKMIjY4AgAgA0E1akECdCACaiA2OAIAIANBKmpBAnQgAmogPSBSkiA7kiI9ICOSjCIjOAIAIANBNmpBAnQgAmogIzgCACADQSlqQQJ0IAJqIEsgSpIgOpIiIyAkkowiJDgCACADQTdqQQJ0IAJqICQ4AgAgA0EoakECdCACaiBIIE6SIDGSjCIkOAIAIANBOGpBAnQgAmogJDgCACADQSdqQQJ0IAJqICMgPCArkiIjkowiJDgCACADQTlqQQJ0IAJqICQ4AgAgA0EmakECdCACaiA9ICeSjCIkOAIAIANBOmpBAnQgAmogJDgCACADQSVqQQJ0IAJqICkgLSAwk7tEuEt/Zp6g5j+itiIpkiItICOSjCIjOAIAIANBO2pBAnQgAmogIzgCACADQSRqQQJ0IAJqICggR5KMIiM4AgAgA0E8akECdCACaiAjOAIAIANBI2pBAnQgAmogLSArIDKSIiOSjCIoOAIAIANBPWpBAnQgAmogKDgCACADQSJqQQJ0IAJqICcgU5KMIic4AgAgA0E+akECdCACaiAnOAIAIANBIWpBAnQgAmogIyBCkowiIzgCACADQT9qQQJ0IAJqICM4AgAgA0EgakECdCACaiA0jDgCACADQQJ0IAJqIDQ4AgAgA0EfakECdCACaiAsjDgCACADQQFqQQJ0IAJqICw4AgAgA0EeakECdCACaiA5jDgCACADQQJqQQJ0IAJqIDk4AgAgA0EdakECdCACaiApICaSIjQgQZIiI4w4AgAgA0EDakECdCACaiAjOAIAIANBHGpBAnQgAmogL4w4AgAgA0EEakECdCACaiAvOAIAIANBG2pBAnQgAmogNCA3IDOSIjSSIi+MOAIAIANBBWpBAnQgAmogLzgCACADQRpqQQJ0IAJqIEQgO5IiL4w4AgAgA0EGakECdCACaiAvOAIAIANBGWpBAnQgAmogNCA6kiI0jDgCACADQQdqQQJ0IAJqIDQ4AgAgA0EYakECdCACaiAxjDgCACADQQhqQQJ0IAJqIDE4AgAgA0EXakECdCACaiAqjDgCACADQQlqQQJ0IAJqICo4AgAgA0EWakECdCACaiA7IC6SIjGMOAIAIANBCmpBAnQgAmogMTgCACADQRVqQQJ0IAJqIEMgJpIiMYw4AgAgA0ELakECdCACaiAxOAIAIANBFGpBAnQgAmogNYw4AgAgA0EMakECdCACaiA1OAIAIANBE2pBAnQgAmogJiAlkiImjDgCACADQQ1qQQJ0IAJqICY4AgAgA0ESakECdCACaiAujDgCACADQQ5qQQJ0IAJqIC44AgAgA0ERakECdCACaiAljDgCACADQQ9qQQJ0IAJqICU4AgAgA0EQakECdCACakMAAAAAOAIACy8BAX9BFBAxIgIgATYCECACIAAQMTYCACACIAA2AgggAkEANgIMIAJBADYCBCACCw0AIAAoAgAQOCAAEDgL6QEBBH8gACgCCCIDIAAoAgwiAmsiBCABSQRAAkAgACgCEEECRgRAIAAgACgCACABIARrIANBAXQiAiACIARqIAFJGyIBEDk2AgAgACABNgIIIAAoAgQgACgCDCIBQQN0IgJNDQEgACACNgIEDAELIAIgACgCBCIFQQN2IgNGIAMgBGogAUlyBEAgAEEANgIMIABBADYCBEEAIQEMAQsgAwRAIAAoAgAiASABIANqIAIgA2sQOxogACAAKAIMIANrIgE2AgwgACAAKAIEIAVBeHFrNgIEBSACIQELCwUgAiEBCyAAKAIAIAFqC40BAQR/IAAoAgRBB2pBA3YiASAAKAIMIgRJBEACQCAAKAIAIQIDQAJAIAFBAWohAyABIAJqLAAARQRAIAIgA2osAABFBEAgAiABQQJqaiwAAEEBRg0CCwsgAyAETw0CIAMhAQwBCwsgACABQQN0QSBqNgIEIAIgAUEDamotAAAPCwsgACAEQQN0NgIEQX8LqQEBBH8gACgCBEEHakEDdiICIAAoAgwiBUkEQAJAIAAoAgAh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aiIGQRBLDQALIAMgCWohAyAIayECIQEMAgsgAyABKAIAIgU6AAAgAyABLAABOgABIAJBbGpBcHEiCEESaiIJIAFqIAJBbmogA0ECaiEEIAJBfmohBiABQQJqIQIgBSEBA0AgBCACKAICIgVBEHQgAUEQdnI2AgAgBCACKAIGIgFBEHQgBUEQdnI2AgQgBCACKAIKIgVBEHQgAUEQdnI2AgggBCACKAIOIgFBEHQgBUEQdnI2AgwgAkEQaiECIARBEGohBCAGQXBqIgZBEUsNAAsgAyAJaiEDIAhrIQIhAQwBCyADIAEoAgAiBToAACACQWxqQXBxIghBEWoiCSABaiACQW9qIANBAWohBCACQX9qIQYgAUEBaiECIAUhAQNAIAQgAigCAyIFQRh0IAFBCHZyNgIAIAQgAigCByIBQRh0IAVBCHZyNgIEIAQgAigCCyIFQRh0IAFBCHZyNgIIIAQgAigCDyIBQRh0IAVBCHZyNgIMIAJBEGohAiAEQRBqIQQgBkFwaiIGQRJLDQALIAMgCWohAyAIayECIQELCyACQRBxBEAgAyABLAAAOgAAIAMgASwAAToAASADIAEsAAI6AAIgAyABLAADOgADIAMgASwABDoABCADIAEsAAU6AAUgAyABLAAGOgAGIAMgASwABzoAByADIAEsAAg6AAggAyABLAAJOgAJIAMgASwACjoACiADIAEsAAs6AAsgAyABLAAMOgAMIAMgASwADToADSADIAEsAA46AA4gAyABLAAPOgAPIANBEGohAyABQRBqIQELIAJBCHEEQCADIAEsAAA6AAAgAyABLAABOgABIAMgASwAAjoAAiADIAEsAAM6AAMgAyABLAAEOgAEIAMgASwABToABSADIAEsAAY6AAYgAyABLAAHOgAHIANBCGohAyABQQhqIQELIAJBBHEEQCADIAEsAAA6AAAgAyABLAABOgABIAMgASwAAjoAAiADIAEsAAM6AAMgA0EEaiEDIAFBBGohAQsgAkECcQRAIAMgASwAADoAACADIAEsAAE6AAEgA0ECaiEDIAFBAmohAQsgAkEBcUUEQCAADwsgAyABLAAAOgAAIAALwAMBBn8gACABRgRAIAAPCyABIAJqIABLIAAgAmoiBSABS3FFBEAgACABIAIQOhogAA8LIAEgACIDc0EDcUUhBCADIAFJBH8g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JCQkJCQkJCQkJCQkJCQkJCQAAAABAhEAAAAAAAAAAAAAAAAAAQIDBAUGEQAAAAAAAAAAAAECAwQFBgcICQoLDA0OEQABAwUGBwgJCgsMDQ4PEBEAAQIEBQYHCAkKCwwNDg8RAAECAwQFBgcICQoLDA0ODwMAAQUFAAEHBwAAAwkAAQoPAAAEHwAABT8AAAZ/AAAH/wAACP8BAAn/AwAK/wcAC/8PAAz/HwAN/z8ADv9/AA///wAQAAAAAAAAAAAAAAAAAAAAAupflgEwikIBCAgMW1tbXltec2l6ZSA+IDAAL2hvbWUvZG9taW5pYy9wcm9qZWN0cy9lbXNkay9mYXN0Y29tcC9lbXNjcmlwdGVuL3N5c3RlbS9saWIvZW1tYWxsb2MuY3BwAGdldEZyZWVMaXN0SW5kZXgATUlOX0ZSRUVMSVNUX0lOREVYIDw9IGluZGV4ICYmIGluZGV4IDwgTUFYX0ZSRUVMSVNUX0lOREVYAGdldEFmdGVyKHJlZ2lvbikgPD0gc2JyaygwKQBlbW1hbGxvY19tYWxsb2MAcmVnaW9uLT5nZXRVc2VkKCkAZ2V0UGF5bG9hZAAoY2hhciopZXh0cmFQdHIgPT0gKGNoYXIqKXB0ciArIHNicmtTaXplAGFsbG9jYXRlUmVnaW9uACFsYXN0UmVnaW9uACFmaXJzdFJlZ2lvbgBmaXJzdFJlZ2lvbgBwdHIgPT0gZ2V0QWZ0ZXIobGFzdFJlZ2lvbikAZXh0ZW5kTGFzdFJlZ2lvbgBhZGRUb0ZyZWVMaXN0AGdldEJpZ0Vub3VnaEZyZWVMaXN0SW5kZXgAcGF5bG9hZFNpemUgPj0gc2l6ZQBwb3NzaWJseVNwbGl0UmVtYWluZGVyAGV4dHJhID49IE1JTl9SRUdJT05fU0laRQB0b3RhbFNwbGl0U2l6ZSA+PSBNSU5fUkVHSU9OX1NJWkUAbWVyZ2VJbnRvRXhpc3RpbmdGcmVlUmVnaW9uAHJlZ2lvbiA9PSBsYXN0UmVnaW9uAGVtbWFsbG9jX3JlYWxsb2M='; 3 | --------------------------------------------------------------------------------