├── .editorconfig ├── .env ├── .eslintrc.js ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── LICENSE ├── README.md ├── appveyor.yml ├── docs ├── PamDiff.html ├── fonts │ ├── OpenSans-Bold-webfont.eot │ ├── OpenSans-Bold-webfont.svg │ ├── OpenSans-Bold-webfont.woff │ ├── OpenSans-BoldItalic-webfont.eot │ ├── OpenSans-BoldItalic-webfont.svg │ ├── OpenSans-BoldItalic-webfont.woff │ ├── OpenSans-Italic-webfont.eot │ ├── OpenSans-Italic-webfont.svg │ ├── OpenSans-Italic-webfont.woff │ ├── OpenSans-Light-webfont.eot │ ├── OpenSans-Light-webfont.svg │ ├── OpenSans-Light-webfont.woff │ ├── OpenSans-LightItalic-webfont.eot │ ├── OpenSans-LightItalic-webfont.svg │ ├── OpenSans-LightItalic-webfont.woff │ ├── OpenSans-Regular-webfont.eot │ ├── OpenSans-Regular-webfont.svg │ └── OpenSans-Regular-webfont.woff ├── index.html ├── index.js.html ├── scripts │ ├── linenumber.js │ └── prettify │ │ ├── Apache-License-2.0.txt │ │ ├── lang-css.js │ │ └── prettify.js └── styles │ ├── jsdoc-default.css │ ├── prettify-jsdoc.css │ └── prettify-tomorrow.css ├── examples ├── change_when_running.js ├── createPam.js ├── createPam2.js ├── createPam3.js ├── example.js ├── example2.js ├── example3.js ├── example4.js ├── example5.js ├── example6.js ├── example7.js ├── in │ ├── circle.mp4 │ ├── circle_star.mp4 │ └── star.mp4 └── out │ ├── draw │ └── .gitignore │ └── pam │ └── .gitignore ├── index.js ├── lib └── ffmpeg.js ├── package-lock.json ├── package.json ├── prettier.config.js └── tests ├── test_gray.js ├── test_gray2.js ├── test_gray3.js ├── test_gray4.js ├── test_rgb.js ├── test_rgb2.js ├── test_rgb3.js ├── test_rgb4.js ├── test_rgba.js ├── test_rgba2.js ├── test_rgba3.js └── test_rgba4.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | 13 | # Source files with spaces 14 | [*.{js,css,html,json,toml,yml,ejs,svg}] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | TARGET=all 3 | #TARGET=regions 4 | MASK=false 5 | #MASK=true 6 | #RESPONSE=percent 7 | #RESPONSE=bounds 8 | RESPONSE=blobs 9 | #DRAW=false 10 | DRAW=true 11 | PIXFMT=gray 12 | #PIXFMT=rgb24 13 | #PIXFMT=rgba 14 | #UV_THREADPOOL_SIZE=100 15 | OUT_PATH=./ 16 | POOL=2 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es2022: true, 4 | node: true, 5 | }, 6 | extends: ['plugin:prettier/recommended', 'plugin:markdown/recommended'], 7 | plugins: ['prettier', 'markdown'], 8 | rules: { 9 | 'prettier/prettier': 'error', 10 | 'spaced-comment': ['error', 'always'], 11 | }, 12 | ignorePatterns: ['/docs/'], 13 | }; 14 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: build 5 | 6 | on: 7 | push: 8 | branches: [ master, dev ] 9 | pull_request: 10 | branches: [ master, dev ] 11 | 12 | jobs: 13 | linux: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [14.x, 16.x, 18.x, 20.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: npm ci 29 | - run: npm run build --if-present 30 | - run: npm test 31 | 32 | windows: 33 | 34 | runs-on: windows-latest 35 | 36 | strategy: 37 | matrix: 38 | node-version: [14.x, 16.x, 18.x, 20.x] 39 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 40 | 41 | steps: 42 | - uses: actions/checkout@v3 43 | - name: Use Node.js ${{ matrix.node-version }} 44 | uses: actions/setup-node@v3 45 | with: 46 | node-version: ${{ matrix.node-version }} 47 | - run: npm ci 48 | - run: npm run build --if-present 49 | - run: npm test 50 | 51 | macos: 52 | 53 | runs-on: macos-latest 54 | 55 | strategy: 56 | matrix: 57 | node-version: [14.x, 16.x, 18.x, 20.x] 58 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 59 | 60 | steps: 61 | - uses: actions/checkout@v3 62 | - name: Use Node.js ${{ matrix.node-version }} 63 | uses: actions/setup-node@v3 64 | with: 65 | node-version: ${{ matrix.node-version }} 66 | - run: npm ci 67 | - run: npm run build --if-present 68 | - run: npm test 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 - 2023 Kevin Godell 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pam-diff 2 | 3 | ###### [![Buy me a coffee](https://img.shields.io/badge/-buy%20me%20a%20coffee-red?logo=buy%20me%20a%20coffee)](https://buymeacoffee.com/kevinGodell) [![Donate via PayPal](https://img.shields.io/badge/Donate-Paypal-blue)](https://www.paypal.com/donate/?business=HTMERJAFHJJEU&no_recurring=0&item_name=support+open+source+software+development¤cy_code=USD) [![Build Status](https://github.com/kevinGodell/pam-diff/workflows/build/badge.svg)](https://github.com/kevinGodell/pam-diff/actions?query=workflow%3Abuild) [![Build Status](https://ci.appveyor.com/api/projects/status/hu6qw285sm6vfwtd/branch/master?svg=true)](https://ci.appveyor.com/project/kevinGodell/pam-diff/branch/master) [![GitHub Issues](https://img.shields.io/github/issues/kevinGodell/pam-diff.svg)](https://github.com/kevinGodell/pam-diff/issues) [![GitHub License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/kevinGodell/pam-diff/master/LICENSE) [![npm](https://img.shields.io/npm/dt/pam-diff.svg?style=flat-square)](https://www.npmjs.com/package/pam-diff) 4 | 5 | Measure differences between pixel arrays extracted from pam images. Works well with node module [pipe2pam](https://www.npmjs.com/package/pipe2pam) to extract pam images from an ffmpeg pipe. Supported **_tupltypes_** are **_rgb_**, **_rgb_alpha_**, and **_grayscale_**. It is currently being used for a video motion detection project. 6 | 7 | ## Installation: 8 | 9 | ``` 10 | npm install pam-diff --save 11 | ``` 12 | 13 | ## Changelog: 14 | 15 | ###### _version 1.2.1:_ 16 | * Set defaults if configuration object is not passed in constructor. 17 | 18 | ###### _version 1.2.0:_ 19 | * Dropping support for node.js < 14. 20 | * Data event emitted for all results returned from [pixel-change](https://www.npmjs.com/package/pixel-change). 21 | * Initialized event emitted after first chunk parsed. 22 | * Debug object attached to output if debug property is set to true. 23 | * Updated [docs](https://kevingodell.github.io/pam-diff/PamDiff.html) to show deprecations. 24 | 25 | ###### _version 1.1.0:_ 26 | * Percent is now a float to allow for more precise results. 27 | * Sync option is removed. 28 | 29 | ## Usage Options: 30 | 31 | ###### When comparing 2 equally sized buffers of grayscale, rgb, or rgba pixels, there are several options: 32 | 33 | - ### all (default) 34 | 35 | - All pixels will be targeted when checking for differences. 36 | - To use this option, set the configuration object without creating any regions. 37 | 38 | ```javascript 39 | const pamDiff = new PamDiff({ difference: 5, percent: 5 }); 40 | ``` 41 | 42 | - ### regions 43 | 44 | - Specific regions of pixels will be targeted when checking for differences and the rest will be ignored. 45 | - To use this option, create a regions array and pass it to the constructor. 46 | 47 | ```javascript 48 | const region1 = { 49 | name: 'region1', 50 | difference: 12, 51 | percent: 22, 52 | polygon: [ 53 | { x: 0, y: 0 }, 54 | { x: 0, y: 224 }, 55 | { x: 99, y: 224 }, 56 | { x: 99, y: 0 }, 57 | ], 58 | }; 59 | const region2 = { 60 | name: 'region2', 61 | difference: 14, 62 | percent: 10, 63 | polygon: [ 64 | { x: 100, y: 0 }, 65 | { x: 100, y: 224 }, 66 | { x: 199, y: 224 }, 67 | { x: 199, y: 0 }, 68 | ], 69 | }; 70 | const regions = [region1, region2]; 71 | const pamDiff = new PamDiff({ regions: regions }); 72 | ``` 73 | 74 | - ### mask 75 | 76 | - Specific regions of pixels will be ignored when checking for differences. 77 | - To use this option, create a regions array and set the mask option to true. 78 | - `difference` and `percent` of the individual region is ignored. Set global values. 79 | 80 | ```javascript 81 | const region1 = { 82 | name: 'region1', 83 | polygon: [ 84 | { x: 0, y: 0 }, 85 | { x: 0, y: 224 }, 86 | { x: 99, y: 224 }, 87 | { x: 99, y: 0 }, 88 | ], 89 | }; 90 | const region2 = { 91 | name: 'region2', 92 | polygon: [ 93 | { x: 100, y: 0 }, 94 | { x: 100, y: 224 }, 95 | { x: 199, y: 224 }, 96 | { x: 199, y: 0 }, 97 | ], 98 | }; 99 | const regions = [region1, region2]; 100 | const pamDiff = new PamDiff({ difference: 12, percent: 10, mask: true, regions: regions }); 101 | ``` 102 | 103 | ###### Getting results back from the pixel difference detection: 104 | 105 | 1. event 106 | - A _diff_ event will be emitted when there is a pixel difference detection. 107 | 108 | ```javascript 109 | pamDiff.on('diff', data => { 110 | console.log(data); 111 | }); 112 | ``` 113 | 114 | - A _data_ event will be emitted regardless of pixel difference detection. 115 | 116 | ```javascript 117 | pamDiff.on('data', data => { 118 | console.log(data); 119 | }); 120 | ``` 121 | 122 | 2. callback (deprecated) 123 | - A _callback_ function will be called with a data object passed as the only argument. 124 | - The callback can be passed as the 2nd argument to the constructor, or it can be added later. 125 | - Deprecated. Scheduled to be removed. 126 | 127 | ```javascript 128 | /* callback function */ 129 | function myCallback(data) { 130 | console.log(data); 131 | } 132 | 133 | /* via the constructor */ 134 | const pamDiff = new pamDiff({ difference: 10, percent: 20 }, myCallback); 135 | 136 | /* via the setter */ 137 | pamDiff.callback = myCallback; 138 | 139 | /* via the chain-able setter */ 140 | pamDiff.setCallback(myCallback).setDifference(10).setPercent(20); 141 | 142 | /* remove the callback */ 143 | pamDiff.callback = null; 144 | ``` 145 | 146 | ##### Expected results: 147 | 148 | 1. When targeting all pixels: 149 | 150 | ``` 151 | { 152 | trigger: [ 153 | { name: 'all', percent: 13 } 154 | ], 155 | pam: , 156 | headers: , 157 | pixels: 158 | } 159 | ``` 160 | 161 | 2. When targeting regions of pixels: 162 | 163 | ``` 164 | { 165 | trigger: [ 166 | { name: 'region1', percent: 13 }, 167 | { name: 'region2', percent: 22 } 168 | ], 169 | pam: , 170 | headers: , 171 | pixels: 172 | } 173 | ``` 174 | 175 | 3. When targeting all pixels ignored by mask: 176 | 177 | ``` 178 | { 179 | trigger: [ 180 | { name: 'mask', percent: 13 } 181 | ], 182 | pam: , 183 | headers: , 184 | pixels: 185 | } 186 | ``` 187 | 188 | 4. When targeting all pixels and setting {response: "bounds"}: 189 | 190 | ``` 191 | { 192 | trigger: [ 193 | { name: 'all', percent: 13, minX: 42, maxX: 399, minY: 113, maxY: 198 } 194 | ], 195 | pam: , 196 | headers: , 197 | pixels: 198 | } 199 | ``` 200 | 201 | 5. When targeting all pixels and setting {response: "blobs"}: 202 | 203 | ``` 204 | { 205 | trigger: [ 206 | { 207 | name: "all", 208 | percent: 9, 209 | minX: 137, 210 | maxX: 1782, 211 | minY: 392, 212 | maxY: 695, 213 | blobs: [ 214 | { 215 | label: 0, 216 | percent: 3, 217 | minX: 1192, 218 | maxX: 1486, 219 | minY: 392, 220 | maxY: 695 221 | }, 222 | { 223 | label: 1, 224 | percent: 3, 225 | minX: 1488, 226 | maxX: 1782, 227 | minY: 392, 228 | maxY: 695 229 | } 230 | ] 231 | } 232 | ], 233 | pam: , 234 | headers: , 235 | pixels: 236 | } 237 | ``` 238 | 239 | ### Other Resources: 240 | 241 | View the [docs](https://kevingodell.github.io/pam-diff/PamDiff.html), [tests](https://github.com/kevinGodell/pam-diff/tree/master/tests), or [examples](https://github.com/kevinGodell/pam-diff/tree/master/examples) for more implementations. 242 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2017 2 | environment: 3 | matrix: 4 | - nodejs_version: '14' 5 | platform: x86 6 | - nodejs_version: '14' 7 | platform: x64 8 | - nodejs_version: '16' 9 | platform: x86 10 | - nodejs_version: '16' 11 | platform: x64 12 | - nodejs_version: LTS 13 | platform: x86 14 | - nodejs_version: LTS 15 | platform: x64 16 | - nodejs_version: Current 17 | platform: x86 18 | - nodejs_version: Current 19 | platform: x64 20 | cache: 21 | - node_modules 22 | install: 23 | - ps: Install-Product node $env:nodejs_version $env:platform 24 | - npm install 25 | - ps: $env:package_version = (Get-Content -Raw -Path package.json | ConvertFrom-Json).version 26 | - ps: Update-AppveyorBuild -Version "$env:package_version-$env:APPVEYOR_REPO_BRANCH-$env:APPVEYOR_BUILD_NUMBER" 27 | test_script: 28 | - node --version 29 | - npm --version 30 | - npm test 31 | build: false 32 | -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinGodell/pam-diff/002042918ec00ffc12170f310eb505ff35490a8d/docs/fonts/OpenSans-Bold-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinGodell/pam-diff/002042918ec00ffc12170f310eb505ff35490a8d/docs/fonts/OpenSans-Bold-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-BoldItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinGodell/pam-diff/002042918ec00ffc12170f310eb505ff35490a8d/docs/fonts/OpenSans-BoldItalic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-BoldItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinGodell/pam-diff/002042918ec00ffc12170f310eb505ff35490a8d/docs/fonts/OpenSans-BoldItalic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Italic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinGodell/pam-diff/002042918ec00ffc12170f310eb505ff35490a8d/docs/fonts/OpenSans-Italic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Italic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinGodell/pam-diff/002042918ec00ffc12170f310eb505ff35490a8d/docs/fonts/OpenSans-Italic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinGodell/pam-diff/002042918ec00ffc12170f310eb505ff35490a8d/docs/fonts/OpenSans-Light-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinGodell/pam-diff/002042918ec00ffc12170f310eb505ff35490a8d/docs/fonts/OpenSans-Light-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-LightItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinGodell/pam-diff/002042918ec00ffc12170f310eb505ff35490a8d/docs/fonts/OpenSans-LightItalic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-LightItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinGodell/pam-diff/002042918ec00ffc12170f310eb505ff35490a8d/docs/fonts/OpenSans-LightItalic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinGodell/pam-diff/002042918ec00ffc12170f310eb505ff35490a8d/docs/fonts/OpenSans-Regular-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinGodell/pam-diff/002042918ec00ffc12170f310eb505ff35490a8d/docs/fonts/OpenSans-Regular-webfont.woff -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Home 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Home

21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |

30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 | 52 | 55 | 56 |
57 | 58 |
59 | Documentation generated by JSDoc 4.0.2 on Tue Aug 08 2023 21:03:37 GMT-0500 (Central Daylight Time) 60 |
61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /docs/index.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: index.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: index.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
'use strict';
 30 | 
 31 | const { Transform } = require('node:stream');
 32 | 
 33 | const { performance } = require('node:perf_hooks');
 34 | 
 35 | const PP = require('polygon-points');
 36 | 
 37 | const PC = require('pixel-change');
 38 | 
 39 | class PamDiff extends Transform {
 40 |   /**
 41 |    *
 42 |    * @param [options] {Object}
 43 |    * @param [options.difference=5] {Number} - Pixel difference value, int 1 to 255
 44 |    * @param [options.percent=5] {Number} - Percent of pixels or blobs that exceed difference value, float 0.0 to 100.0
 45 |    * @param [options.response=percent] {String} - Accepted values: percent or bounds or blobs
 46 |    * @param [options.regions] {Array} - Array of region objects
 47 |    * @param options.regions[i].name {String} - Name of region
 48 |    * @param [options.regions[i].difference=options.difference] {Number} - Difference value for region, int 1 to 255
 49 |    * @param [options.regions[i].percent=options.percent] {Number} - Percent value for region, float 0.0 to 100.0
 50 |    * @param options.regions[i].polygon {Array} - Array of x y coordinates [{x:0,y:0},{x:0,y:360},{x:160,y:360},{x:160,y:0}]
 51 |    * @param [options.mask=false] {Boolean} - Indicate if regions should be used as masks of pixels to ignore
 52 |    * @param [options.draw=false] {Boolean} - If true and response is 'bounds' or 'blobs', return a pixel buffer with drawn bounding box
 53 |    * @param [options.debug=false] {Boolean} - If true, debug object will be attached to output
 54 |    * @param [callback] {Function} - Function to be called when diff event occurs. Deprecated
 55 |    */
 56 |   constructor(options, callback) {
 57 |     super({ objectMode: true });
 58 |     this.config = options; // configuration for pixel change detection
 59 |     this.callback = callback; // callback function to be called when pixel change is detected
 60 |     this._parseChunk = this._parseFirstChunk; // first parsing will be used to configure pixel diff engine
 61 |   }
 62 | 
 63 |   /**
 64 |    *
 65 |    * @param obj {Object}
 66 |    */
 67 |   set config(obj) {
 68 |     obj = PamDiff._validateObject(obj);
 69 |     this._difference = PamDiff._validateInt(obj.difference, 5, 1, 255);
 70 |     this._percent = PamDiff._validateFloat(obj.percent, 5, 0, 100);
 71 |     this._response = PamDiff._validateString(obj.response, ['percent', 'bounds', 'blobs']);
 72 |     this._regions = PamDiff._validateArray(obj.regions);
 73 |     this._mask = PamDiff._validateBoolean(obj.mask);
 74 |     this._draw = PamDiff._validateBoolean(obj.draw);
 75 |     this._debug = PamDiff._validateBoolean(obj.debug);
 76 |     this._configurePixelDiffEngine();
 77 |   }
 78 | 
 79 |   /**
 80 |    *
 81 |    * @returns {Object}
 82 |    */
 83 |   get config() {
 84 |     return {
 85 |       difference: this._difference,
 86 |       percent: this._percent,
 87 |       response: this._response,
 88 |       regions: this._regions,
 89 |       mask: this._mask,
 90 |       draw: this._draw,
 91 |       debug: this._debug,
 92 |     };
 93 |   }
 94 | 
 95 |   /**
 96 |    *
 97 |    * @param num {Number}
 98 |    */
 99 |   set difference(num) {
100 |     this._difference = PamDiff._validateInt(num, 5, 1, 255);
101 |     this._configurePixelDiffEngine();
102 |   }
103 | 
104 |   /**
105 |    *
106 |    * @return {Number}
107 |    */
108 |   get difference() {
109 |     return this._difference;
110 |   }
111 | 
112 |   /**
113 |    *
114 |    * @param num {Number}
115 |    * @return {PamDiff}
116 |    * @deprecated
117 |    */
118 |   setDifference(num) {
119 |     this.difference = num;
120 |     return this;
121 |   }
122 | 
123 |   /**
124 |    *
125 |    * @param num {Number|String}
126 |    */
127 |   set percent(num) {
128 |     this._percent = PamDiff._validateFloat(num, 5, 0, 100);
129 |     this._configurePixelDiffEngine();
130 |   }
131 | 
132 |   /**
133 |    *
134 |    * @return {Number}
135 |    */
136 |   get percent() {
137 |     return this._percent;
138 |   }
139 | 
140 |   /**
141 |    *
142 |    * @param num {Number}
143 |    * @return {PamDiff}
144 |    * @deprecated
145 |    */
146 |   setPercent(num) {
147 |     this.percent = num;
148 |     return this;
149 |   }
150 | 
151 |   /**
152 |    *
153 |    * @param str {String}
154 |    */
155 |   set response(str) {
156 |     this._response = PamDiff._validateString(str, ['percent', 'bounds', 'blobs']);
157 |     this._configurePixelDiffEngine();
158 |   }
159 | 
160 |   /**
161 |    *
162 |    * @return {String}
163 |    */
164 |   get response() {
165 |     return this._response;
166 |   }
167 | 
168 |   /**
169 |    *
170 |    * @param str {String}
171 |    * @return {PamDiff}
172 |    * @deprecated
173 |    */
174 |   setResponse(str) {
175 |     this.response = str;
176 |     return this;
177 |   }
178 | 
179 |   /**
180 |    *
181 |    * @param arr {Array}
182 |    */
183 |   set regions(arr) {
184 |     this._regions = PamDiff._validateArray(arr);
185 |     this._configurePixelDiffEngine();
186 |   }
187 | 
188 |   /**
189 |    *
190 |    * @return {Array}
191 |    */
192 |   get regions() {
193 |     return this._regions;
194 |   }
195 | 
196 |   /**
197 |    *
198 |    * @param arr {Object[]}
199 |    * @return {PamDiff}
200 |    * @deprecated
201 |    */
202 |   setRegions(arr) {
203 |     this.regions = arr;
204 |     return this;
205 |   }
206 | 
207 |   /**
208 |    *
209 |    * @param bool {Boolean|String|Number}
210 |    */
211 |   set mask(bool) {
212 |     this._mask = PamDiff._validateBoolean(bool);
213 |     this._configurePixelDiffEngine();
214 |   }
215 | 
216 |   /**
217 |    *
218 |    * @returns {Boolean}
219 |    */
220 |   get mask() {
221 |     return this._mask;
222 |   }
223 | 
224 |   /**
225 |    *
226 |    * @param bool {Boolean}
227 |    * @returns {PamDiff}
228 |    * @deprecated
229 |    */
230 |   setMask(bool) {
231 |     this.mask = bool;
232 |     return this;
233 |   }
234 | 
235 |   /**
236 |    *
237 |    * @param bool {Boolean}
238 |    */
239 |   set draw(bool) {
240 |     this._draw = PamDiff._validateBoolean(bool);
241 |     this._configurePixelDiffEngine();
242 |   }
243 | 
244 |   /**
245 |    *
246 |    * @return {Boolean}
247 |    */
248 |   get draw() {
249 |     return this._draw;
250 |   }
251 | 
252 |   /**
253 |    *
254 |    * @param bool {Boolean}
255 |    * @return {PamDiff}
256 |    * @deprecated
257 |    */
258 |   setDraw(bool) {
259 |     this.draw = bool;
260 |     return this;
261 |   }
262 | 
263 |   /**
264 |    *
265 |    * @param bool {Boolean|String|Number}
266 |    */
267 |   set debug(bool) {
268 |     this._debug = PamDiff._validateBoolean(bool);
269 |     this._configurePixelDiffEngine();
270 |   }
271 | 
272 |   /**
273 |    *
274 |    * @return {Boolean}
275 |    */
276 |   get debug() {
277 |     return this._debug;
278 |   }
279 | 
280 |   /**
281 |    *
282 |    * @param bool {Boolean}
283 |    * @return {PamDiff}
284 |    * @deprecated
285 |    */
286 |   setDebug(bool) {
287 |     this.debug = bool;
288 |     return this;
289 |   }
290 | 
291 |   /**
292 |    *
293 |    * @param func {Function}
294 |    * @deprecated
295 |    */
296 |   set callback(func) {
297 |     if (!func) {
298 |       this._callback = undefined;
299 |     } else if (typeof func === 'function' && func.length === 1) {
300 |       this._callback = func;
301 |     } else {
302 |       throw new Error('Callback must be a function that accepts 1 argument.');
303 |     }
304 |   }
305 | 
306 |   /**
307 |    *
308 |    * @return {Function}
309 |    * @deprecated
310 |    */
311 |   get callback() {
312 |     return this._callback;
313 |   }
314 | 
315 |   /**
316 |    *
317 |    * @param func {Function}
318 |    * @return {PamDiff}
319 |    * @deprecated
320 |    */
321 |   setCallback(func) {
322 |     this.callback = func;
323 |     return this;
324 |   }
325 | 
326 |   /**
327 |    *
328 |    * @return {PamDiff}
329 |    * @deprecated
330 |    */
331 |   resetCache() {
332 |     return this.reset();
333 |   }
334 | 
335 |   /**
336 |    *
337 |    * @return {PamDiff}
338 |    */
339 |   reset() {
340 |     this.emit('reset');
341 |     this._debugInfo = undefined;
342 |     this._engine = undefined;
343 |     this._oldPix = undefined;
344 |     this._width = undefined;
345 |     this._height = undefined;
346 |     this._depth = undefined;
347 |     this._tupltype = undefined;
348 |     this._parseChunk = this._parseFirstChunk;
349 |     return this;
350 |   }
351 | 
352 |   /**
353 |    *
354 |    * @returns {Array|null}
355 |    * @private
356 |    */
357 |   _processRegions() {
358 |     if (this._regions) {
359 |       const regions = [];
360 |       if (this._mask === true) {
361 |         // combine all regions to form a single region of flipped 0's and 1's
362 |         let minX = this._width;
363 |         let maxX = 0;
364 |         let minY = this._height;
365 |         let maxY = 0;
366 |         const wxh = this._width * this._height;
367 |         const maskBitset = Buffer.alloc(wxh, 1);
368 |         for (const region of this._regions) {
369 |           if (!region.hasOwnProperty('polygon')) {
370 |             throw new Error('Region must include a polygon property');
371 |           }
372 |           const pp = new PP(region.polygon);
373 |           const bitset = pp.getBitset(this._width, this._height);
374 |           if (bitset.count === 0) {
375 |             throw new Error('Bitset count must be greater than 0.');
376 |           }
377 |           const bitsetBuffer = bitset.buffer;
378 |           for (let i = 0; i < wxh; ++i) {
379 |             if (bitsetBuffer[i] === 1) {
380 |               maskBitset[i] = 0;
381 |             }
382 |           }
383 |         }
384 |         let maskBitsetCount = 0;
385 |         for (let i = 0; i < wxh; ++i) {
386 |           if (maskBitset[i] === 1) {
387 |             const y = Math.floor(i / this._width);
388 |             const x = i % this._width;
389 |             minX = Math.min(minX, x);
390 |             maxX = Math.max(maxX, x);
391 |             minY = Math.min(minY, y);
392 |             maxY = Math.max(maxY, y);
393 |             maskBitsetCount++;
394 |           }
395 |         }
396 |         if (maskBitsetCount === 0) {
397 |           throw new Error('Bitset count must be greater than 0');
398 |         }
399 |         regions.push({
400 |           name: 'mask',
401 |           bitset: maskBitset,
402 |           bitsetCount: maskBitsetCount,
403 |           difference: this._difference,
404 |           percent: this._percent,
405 |           minX: minX,
406 |           maxX: maxX,
407 |           minY: minY,
408 |           maxY: maxY,
409 |         });
410 |       } else {
411 |         for (const region of this._regions) {
412 |           if (!region.hasOwnProperty('name') || !region.hasOwnProperty('polygon')) {
413 |             throw new Error('Region must include a name and a polygon property');
414 |           }
415 |           const pp = new PP(region.polygon);
416 |           const bitset = pp.getBitset(this._width, this._height);
417 |           if (bitset.count === 0) {
418 |             throw new Error('Bitset count must be greater than 0');
419 |           }
420 |           const difference = PamDiff._validateInt(region.difference, this._difference, 1, 255);
421 |           const percent = PamDiff._validateFloat(region.percent, this._percent, 0, 100);
422 |           regions.push({
423 |             name: region.name,
424 |             bitset: bitset.buffer,
425 |             bitsetCount: bitset.count,
426 |             difference: difference,
427 |             percent: percent,
428 |             minX: bitset.minX,
429 |             maxX: bitset.maxX,
430 |             minY: bitset.minY,
431 |             maxY: bitset.maxY,
432 |           });
433 |         }
434 |       }
435 |       return regions;
436 |     }
437 |     return null;
438 |   }
439 | 
440 |   /**
441 |    *
442 |    * @private
443 |    */
444 |   _configurePixelDiffEngine() {
445 |     if (!this._tupltype || !this._width || !this._height) {
446 |       return;
447 |     }
448 |     const regions = this._processRegions();
449 |     let name = `${this._tupltype}_${this._width}w_${this._height}h_${this._depth}d`;
450 |     const config = { width: this._width, height: this._height, depth: this._depth, response: this._response };
451 |     if (regions) {
452 |       if (regions.length === 1) {
453 |         if (this._mask === true) {
454 |           name += '_mask';
455 |         } else {
456 |           name += '_region';
457 |         }
458 |       } else {
459 |         name += `_regions`;
460 |       }
461 |       config.regions = regions;
462 |     } else {
463 |       name += '_all';
464 |       config.difference = this._difference;
465 |       config.percent = this._percent;
466 |     }
467 |     name += `_${this._response}`;
468 |     if ((this._response === 'bounds' || this._response === 'blobs') && this._draw) {
469 |       config.draw = this._draw;
470 |       name += '_draw';
471 |     }
472 |     name += '_async';
473 |     const pixelChange = PC(config);
474 |     this._engine = pixelChange.compare.bind(pixelChange);
475 |     if (this._debug) {
476 |       this._parseChunk = this._parsePixelsDebug;
477 |       this._debugInfo = { name, count: 0 };
478 |     } else {
479 |       this._parseChunk = this._parsePixels;
480 |     }
481 |   }
482 | 
483 |   /**
484 |    *
485 |    * @param chunk {Object}
486 |    * @private
487 |    */
488 |   _parsePixels(chunk) {
489 |     const oldPix = this._oldPix;
490 |     const newPix = (this._oldPix = chunk.pixels);
491 |     this._engine(oldPix, newPix, (err, data) => {
492 |       if (data) {
493 |         const { results, pixels } = data;
494 |         const diff = { trigger: results, pam: chunk.pam, headers: chunk.headers, pixels: pixels || newPix };
495 |         this.emit('data', diff);
496 |         if (results.length) {
497 |           this.emit('diff', diff);
498 |           if (this._callback) {
499 |             this._callback(diff);
500 |           }
501 |         }
502 |       } else {
503 |         throw new Error(err);
504 |       }
505 |     });
506 |   }
507 | 
508 |   /**
509 |    *
510 |    * @param chunk {Object}
511 |    * @private
512 |    */
513 |   _parsePixelsDebug(chunk) {
514 |     const oldPix = this._oldPix;
515 |     const newPix = (this._oldPix = chunk.pixels);
516 |     const count = ++this._debugInfo.count;
517 |     const name = this._debugInfo.name;
518 |     const start = performance.now();
519 |     this._engine(oldPix, newPix, (err, data) => {
520 |       const duration = Math.round((performance.now() - start) * 1000) / 1000;
521 |       if (data) {
522 |         const { results, pixels } = data;
523 |         const diff = { trigger: results, pam: chunk.pam, headers: chunk.headers, pixels: pixels || newPix, debug: { name, count, duration } };
524 |         this.emit('data', diff);
525 |         if (results.length) {
526 |           this.emit('diff', diff);
527 |           if (this._callback) {
528 |             this._callback(diff);
529 |           }
530 |         }
531 |       } else {
532 |         throw new Error(err);
533 |       }
534 |     });
535 |   }
536 | 
537 |   /**
538 |    *
539 |    * @param chunk {Object}
540 |    * @private
541 |    */
542 |   _parseFirstChunk(chunk) {
543 |     this._width = Number.parseInt(chunk.width);
544 |     this._height = Number.parseInt(chunk.height);
545 |     this._depth = Number.parseInt(chunk.depth);
546 |     this._oldPix = chunk.pixels;
547 |     this._tupltype = chunk.tupltype;
548 |     this._configurePixelDiffEngine();
549 |     this.emit('initialized', { width: this._width, height: this._height, depth: this._depth, tupltype: this._tupltype });
550 |   }
551 | 
552 |   /**
553 |    *
554 |    * @param chunk {Object}
555 |    * @param encoding
556 |    * @param callback
557 |    * @private
558 |    */
559 |   _transform(chunk, encoding, callback) {
560 |     this._parseChunk(chunk);
561 |     callback();
562 |   }
563 | 
564 |   /**
565 |    *
566 |    * @param callback
567 |    * @private
568 |    */
569 |   _flush(callback) {
570 |     this.reset();
571 |     callback();
572 |   }
573 | 
574 |   /**
575 |    *
576 |    * @param num {Number|String}
577 |    * @param def {Number}
578 |    * @param min {Number}
579 |    * @param max {Number}
580 |    * @returns {Number}
581 |    * @private
582 |    */
583 |   static _validateInt(num, def, min, max) {
584 |     num = Number.parseInt(num);
585 |     return Number.isNaN(num) ? def : num < min ? min : num > max ? max : num;
586 |   }
587 | 
588 |   /**
589 |    *
590 |    * @param num {Number|String}
591 |    * @param def {Number}
592 |    * @param min {Number}
593 |    * @param max {Number}
594 |    * @returns {Number}
595 |    * @private
596 |    */
597 |   static _validateFloat(num, def, min, max) {
598 |     num = Number.parseFloat(num);
599 |     return Number.isNaN(num) ? def : num < min ? min : num > max ? max : num;
600 |   }
601 | 
602 |   /**
603 |    *
604 |    * @param bool {Boolean|String|Number}
605 |    * @return {Boolean}
606 |    * @private
607 |    */
608 |   static _validateBoolean(bool) {
609 |     return bool === true || bool === 'true' || bool === 1 || bool === '1';
610 |   }
611 | 
612 |   /**
613 |    *
614 |    * @param str {String}
615 |    * @param arr {String[]}
616 |    * @returns {String}
617 |    * @private
618 |    */
619 |   static _validateString(str, arr) {
620 |     return arr.includes(str) ? str : arr[0];
621 |   }
622 | 
623 |   /**
624 |    *
625 |    * @param arr (Array}
626 |    * @returns {Array|null}
627 |    * @private
628 |    */
629 |   static _validateArray(arr) {
630 |     return Array.isArray(arr) && arr.length ? arr : null;
631 |   }
632 | 
633 |   /**
634 |    *
635 |    * @param obj (Object}
636 |    * @returns {Object}
637 |    * @private
638 |    */
639 |   static _validateObject(obj) {
640 |     return obj && typeof obj === 'object' ? obj : {};
641 |   }
642 | }
643 | 
644 | /**
645 |  *
646 |  * @type {PamDiff}
647 |  */
648 | module.exports = PamDiff;
649 | 
650 |
651 |
652 | 653 | 654 | 655 | 656 |
657 | 658 | 661 | 662 |
663 | 664 |
665 | Documentation generated by JSDoc 4.0.2 on Tue Aug 08 2023 21:03:37 GMT-0500 (Central Daylight Time) 666 |
667 | 668 | 669 | 670 | 671 | 672 | -------------------------------------------------------------------------------- /docs/scripts/linenumber.js: -------------------------------------------------------------------------------- 1 | /*global document */ 2 | (() => { 3 | const source = document.getElementsByClassName('prettyprint source linenums'); 4 | let i = 0; 5 | let lineNumber = 0; 6 | let lineId; 7 | let lines; 8 | let totalLines; 9 | let anchorHash; 10 | 11 | if (source && source[0]) { 12 | anchorHash = document.location.hash.substring(1); 13 | lines = source[0].getElementsByTagName('li'); 14 | totalLines = lines.length; 15 | 16 | for (; i < totalLines; i++) { 17 | lineNumber++; 18 | lineId = `line${lineNumber}`; 19 | lines[i].id = lineId; 20 | if (lineId === anchorHash) { 21 | lines[i].className += ' selected'; 22 | } 23 | } 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /docs/scripts/prettify/Apache-License-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /docs/scripts/prettify/lang-css.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", 2 | /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /docs/scripts/prettify/prettify.js: -------------------------------------------------------------------------------- 1 | var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; 2 | (function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= 3 | [],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), 9 | l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, 10 | q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, 11 | q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, 12 | "");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), 13 | a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} 14 | for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], 18 | "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], 19 | H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], 20 | J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ 21 | I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), 22 | ["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", 23 | /^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), 24 | ["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", 25 | hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= 26 | !k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p th:last-child { border-right: 1px solid #ddd; } 224 | 225 | .ancestors, .attribs { color: #999; } 226 | .ancestors a, .attribs a 227 | { 228 | color: #999 !important; 229 | text-decoration: none; 230 | } 231 | 232 | .clear 233 | { 234 | clear: both; 235 | } 236 | 237 | .important 238 | { 239 | font-weight: bold; 240 | color: #950B02; 241 | } 242 | 243 | .yes-def { 244 | text-indent: -1000px; 245 | } 246 | 247 | .type-signature { 248 | color: #aaa; 249 | } 250 | 251 | .name, .signature { 252 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 253 | } 254 | 255 | .details { margin-top: 14px; border-left: 2px solid #DDD; } 256 | .details dt { width: 120px; float: left; padding-left: 10px; padding-top: 6px; } 257 | .details dd { margin-left: 70px; } 258 | .details ul { margin: 0; } 259 | .details ul { list-style-type: none; } 260 | .details li { margin-left: 30px; padding-top: 6px; } 261 | .details pre.prettyprint { margin: 0 } 262 | .details .object-value { padding-top: 0; } 263 | 264 | .description { 265 | margin-bottom: 1em; 266 | margin-top: 1em; 267 | } 268 | 269 | .code-caption 270 | { 271 | font-style: italic; 272 | font-size: 107%; 273 | margin: 0; 274 | } 275 | 276 | .source 277 | { 278 | border: 1px solid #ddd; 279 | width: 80%; 280 | overflow: auto; 281 | } 282 | 283 | .prettyprint.source { 284 | width: inherit; 285 | } 286 | 287 | .source code 288 | { 289 | font-size: 100%; 290 | line-height: 18px; 291 | display: block; 292 | padding: 4px 12px; 293 | margin: 0; 294 | background-color: #fff; 295 | color: #4D4E53; 296 | } 297 | 298 | .prettyprint code span.line 299 | { 300 | display: inline-block; 301 | } 302 | 303 | .prettyprint.linenums 304 | { 305 | padding-left: 70px; 306 | -webkit-user-select: none; 307 | -moz-user-select: none; 308 | -ms-user-select: none; 309 | user-select: none; 310 | } 311 | 312 | .prettyprint.linenums ol 313 | { 314 | padding-left: 0; 315 | } 316 | 317 | .prettyprint.linenums li 318 | { 319 | border-left: 3px #ddd solid; 320 | } 321 | 322 | .prettyprint.linenums li.selected, 323 | .prettyprint.linenums li.selected * 324 | { 325 | background-color: lightyellow; 326 | } 327 | 328 | .prettyprint.linenums li * 329 | { 330 | -webkit-user-select: text; 331 | -moz-user-select: text; 332 | -ms-user-select: text; 333 | user-select: text; 334 | } 335 | 336 | .params .name, .props .name, .name code { 337 | color: #4D4E53; 338 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 339 | font-size: 100%; 340 | } 341 | 342 | .params td.description > p:first-child, 343 | .props td.description > p:first-child 344 | { 345 | margin-top: 0; 346 | padding-top: 0; 347 | } 348 | 349 | .params td.description > p:last-child, 350 | .props td.description > p:last-child 351 | { 352 | margin-bottom: 0; 353 | padding-bottom: 0; 354 | } 355 | 356 | .disabled { 357 | color: #454545; 358 | } 359 | -------------------------------------------------------------------------------- /docs/styles/prettify-jsdoc.css: -------------------------------------------------------------------------------- 1 | /* JSDoc prettify.js theme */ 2 | 3 | /* plain text */ 4 | .pln { 5 | color: #000000; 6 | font-weight: normal; 7 | font-style: normal; 8 | } 9 | 10 | /* string content */ 11 | .str { 12 | color: #006400; 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | 17 | /* a keyword */ 18 | .kwd { 19 | color: #000000; 20 | font-weight: bold; 21 | font-style: normal; 22 | } 23 | 24 | /* a comment */ 25 | .com { 26 | font-weight: normal; 27 | font-style: italic; 28 | } 29 | 30 | /* a type name */ 31 | .typ { 32 | color: #000000; 33 | font-weight: normal; 34 | font-style: normal; 35 | } 36 | 37 | /* a literal value */ 38 | .lit { 39 | color: #006400; 40 | font-weight: normal; 41 | font-style: normal; 42 | } 43 | 44 | /* punctuation */ 45 | .pun { 46 | color: #000000; 47 | font-weight: bold; 48 | font-style: normal; 49 | } 50 | 51 | /* lisp open bracket */ 52 | .opn { 53 | color: #000000; 54 | font-weight: bold; 55 | font-style: normal; 56 | } 57 | 58 | /* lisp close bracket */ 59 | .clo { 60 | color: #000000; 61 | font-weight: bold; 62 | font-style: normal; 63 | } 64 | 65 | /* a markup tag name */ 66 | .tag { 67 | color: #006400; 68 | font-weight: normal; 69 | font-style: normal; 70 | } 71 | 72 | /* a markup attribute name */ 73 | .atn { 74 | color: #006400; 75 | font-weight: normal; 76 | font-style: normal; 77 | } 78 | 79 | /* a markup attribute value */ 80 | .atv { 81 | color: #006400; 82 | font-weight: normal; 83 | font-style: normal; 84 | } 85 | 86 | /* a declaration */ 87 | .dec { 88 | color: #000000; 89 | font-weight: bold; 90 | font-style: normal; 91 | } 92 | 93 | /* a variable name */ 94 | .var { 95 | color: #000000; 96 | font-weight: normal; 97 | font-style: normal; 98 | } 99 | 100 | /* a function name */ 101 | .fun { 102 | color: #000000; 103 | font-weight: bold; 104 | font-style: normal; 105 | } 106 | 107 | /* Specify class=linenums on a pre to get line numbering */ 108 | ol.linenums { 109 | margin-top: 0; 110 | margin-bottom: 0; 111 | } 112 | -------------------------------------------------------------------------------- /docs/styles/prettify-tomorrow.css: -------------------------------------------------------------------------------- 1 | /* Tomorrow Theme */ 2 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 3 | /* Pretty printing styles. Used with prettify.js. */ 4 | /* SPAN elements with the classes below are added by prettyprint. */ 5 | /* plain text */ 6 | .pln { 7 | color: #4d4d4c; } 8 | 9 | @media screen { 10 | /* string content */ 11 | .str { 12 | color: #718c00; } 13 | 14 | /* a keyword */ 15 | .kwd { 16 | color: #8959a8; } 17 | 18 | /* a comment */ 19 | .com { 20 | color: #8e908c; } 21 | 22 | /* a type name */ 23 | .typ { 24 | color: #4271ae; } 25 | 26 | /* a literal value */ 27 | .lit { 28 | color: #f5871f; } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #4d4d4c; } 33 | 34 | /* lisp open bracket */ 35 | .opn { 36 | color: #4d4d4c; } 37 | 38 | /* lisp close bracket */ 39 | .clo { 40 | color: #4d4d4c; } 41 | 42 | /* a markup tag name */ 43 | .tag { 44 | color: #c82829; } 45 | 46 | /* a markup attribute name */ 47 | .atn { 48 | color: #f5871f; } 49 | 50 | /* a markup attribute value */ 51 | .atv { 52 | color: #3e999f; } 53 | 54 | /* a declaration */ 55 | .dec { 56 | color: #f5871f; } 57 | 58 | /* a variable name */ 59 | .var { 60 | color: #c82829; } 61 | 62 | /* a function name */ 63 | .fun { 64 | color: #4271ae; } } 65 | /* Use higher contrast and text-weight for printable form. */ 66 | @media print, projection { 67 | .str { 68 | color: #060; } 69 | 70 | .kwd { 71 | color: #006; 72 | font-weight: bold; } 73 | 74 | .com { 75 | color: #600; 76 | font-style: italic; } 77 | 78 | .typ { 79 | color: #404; 80 | font-weight: bold; } 81 | 82 | .lit { 83 | color: #044; } 84 | 85 | .pun, .opn, .clo { 86 | color: #440; } 87 | 88 | .tag { 89 | color: #006; 90 | font-weight: bold; } 91 | 92 | .atn { 93 | color: #404; } 94 | 95 | .atv { 96 | color: #060; } } 97 | /* Style */ 98 | /* 99 | pre.prettyprint { 100 | background: white; 101 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 102 | font-size: 12px; 103 | line-height: 1.5; 104 | border: 1px solid #ccc; 105 | padding: 10px; } 106 | */ 107 | 108 | /* Specify class=linenums on a pre to get line numbering */ 109 | ol.linenums { 110 | margin-top: 0; 111 | margin-bottom: 0; } 112 | 113 | /* IE indents via margin-left */ 114 | li.L0, 115 | li.L1, 116 | li.L2, 117 | li.L3, 118 | li.L4, 119 | li.L5, 120 | li.L6, 121 | li.L7, 122 | li.L8, 123 | li.L9 { 124 | /* */ } 125 | 126 | /* Alternate shading for lines */ 127 | li.L1, 128 | li.L3, 129 | li.L5, 130 | li.L7, 131 | li.L9 { 132 | /* */ } 133 | -------------------------------------------------------------------------------- /examples/change_when_running.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.NODE_ENV = 'development'; 4 | 5 | console.time('=====> testing rgb pam diffs with 2 region masks set'); 6 | 7 | const assert = require('assert'); 8 | 9 | const P2P = require('pipe2pam'); 10 | 11 | const PamDiff = require('../index'); 12 | 13 | const ffmpegPath = require('../lib/ffmpeg'); 14 | 15 | const spawn = require('child_process').spawn; 16 | 17 | const pamCount = 100; 18 | 19 | let pamCounter = 0; 20 | 21 | let pamDiffCounter = 0; 22 | 23 | const pamDiffResults = [13, 14, 13, 13, 13, 12, 14, 13, 12]; 24 | 25 | const params = [ 26 | /* log info to console */ 27 | '-loglevel', 28 | 'quiet', 29 | // '-stats', 30 | 31 | /* use an artificial video input */ 32 | '-re', 33 | '-f', 34 | 'lavfi', 35 | '-i', 36 | 'testsrc=size=1920x1080:rate=15', 37 | 38 | /* set output flags */ 39 | '-an', 40 | '-c:v', 41 | 'pam', 42 | '-pix_fmt', 43 | 'rgb24', 44 | '-f', 45 | 'image2pipe', 46 | '-vf', 47 | 'fps=1,scale=400:225', 48 | '-frames', 49 | pamCount, 50 | 'pipe:1', 51 | ]; 52 | 53 | const p2p = new P2P(); 54 | 55 | p2p.on('pam', data => { 56 | pamCounter++; 57 | }); 58 | 59 | const region1 = { 60 | name: 'region1', 61 | difference: 1, 62 | percent: 1, 63 | polygon: [ 64 | { x: 0, y: 0 }, 65 | { x: 0, y: 225 }, 66 | { x: 100, y: 225 }, 67 | { x: 100, y: 0 }, 68 | ], 69 | }; 70 | 71 | const region2 = { 72 | name: 'region2', 73 | difference: 1, 74 | percent: 1, 75 | polygon: [ 76 | { x: 100, y: 0 }, 77 | { x: 100, y: 225 }, 78 | { x: 200, y: 225 }, 79 | { x: 200, y: 0 }, 80 | ], 81 | }; 82 | 83 | const regions = [region1, region2]; 84 | 85 | const pamDiff = new PamDiff({ mask: true, regions: regions }); 86 | 87 | pamDiff.on('diff', data => { 88 | console.log(data); 89 | // assert(data.trigger[0].name === 'mask', 'trigger name is not correct'); 90 | // assert(data.trigger[0].percent === pamDiffResults[pamDiffCounter++], 'trigger percent is not correct'); 91 | }); 92 | 93 | const ffmpeg = spawn(ffmpegPath, params, { stdio: ['ignore', 'pipe', 'inherit'] }); 94 | 95 | ffmpeg.on('error', error => { 96 | console.log(error); 97 | }); 98 | 99 | ffmpeg.on('exit', (code, signal) => { 100 | assert(code === 0, `FFMPEG exited with code ${code} and signal ${signal}`); 101 | assert(pamDiffCounter === pamCount - 1, `did not get ${pamCount - 1} pam diffs`); 102 | console.timeEnd('=====> testing rgb pam diffs with 2 region masks set'); 103 | }); 104 | 105 | ffmpeg.stdout.pipe(p2p).pipe(pamDiff); 106 | 107 | setTimeout(() => { 108 | console.log(1); 109 | pamDiff.resetCache(); 110 | }, 10000); 111 | 112 | setTimeout(() => { 113 | console.log(2); 114 | pamDiff.mask = false; 115 | }, 20000); 116 | 117 | setTimeout(() => { 118 | console.log(3); 119 | pamDiff.regions = null; 120 | }, 30000); 121 | -------------------------------------------------------------------------------- /examples/createPam.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { config: dotenvConfig } = require('dotenv'); 4 | 5 | dotenvConfig(); 6 | 7 | const argv = require('minimist')(process.argv.slice(2)); 8 | 9 | function getVal(...vals) { 10 | // returns first defined val or undefined 11 | for (let val of vals) { 12 | if (val === undefined) { 13 | continue; 14 | } 15 | return val; 16 | } 17 | return undefined; 18 | } 19 | 20 | // get vals in order of priority, args || env || default 21 | 22 | const target = getVal(argv.target, process.env.TARGET, 'all'); // all || region(s) 23 | 24 | const mask = getVal(argv.mask, process.env.MASK, false); // true || false 25 | 26 | const sync = getVal(argv.sync, process.env.SYNC, false); // true || false 27 | 28 | const response = getVal(argv.response, process.env.RESPONSE, 'percent'); // percent || bounds || blobs 29 | 30 | const draw = getVal(argv.draw, process.env.DRAW, false); // true || false 31 | 32 | const pixFmt = getVal(argv.pixfmt, process.env.PIXFMT, 'gray'); // gray || rgb24 || rgba 33 | 34 | const { cpus } = require('os'); 35 | 36 | const basePathToJpeg = `${__dirname}/out/${(response === 'bounds' || response === 'blobs') && toBool(draw) ? 'draw' : 'pam'}/`; 37 | 38 | function toBool(val) { 39 | return val === true || val === 'true' || val === 1 || val === '1'; 40 | } 41 | 42 | console.log(`cpu cores available: ${cpus().length}`); 43 | 44 | const P2P = require('pipe2pam'); 45 | const PamDiff = require('../index'); 46 | const ffmpegPath = require('../lib/ffmpeg'); 47 | const { spawn, execFile } = require('child_process'); 48 | const { createWriteStream } = require('fs'); 49 | 50 | const params = [ 51 | '-loglevel', 52 | 'quiet', 53 | 54 | /* use hardware acceleration */ 55 | '-hwaccel', 56 | 'auto', // vda, videotoolbox, none, auto 57 | 58 | '-stream_loop', 59 | '1', // -1 for infinite 60 | 61 | /* use a pre-recorded mp4 video as input */ 62 | // '-re',//comment out to have ffmpeg read video as fast as possible 63 | '-i', 64 | `${__dirname}/in/circle_star.mp4`, 65 | 66 | /* '-re', 67 | '-f', 68 | 'lavfi', 69 | '-i', 70 | 'testsrc=size=1920x1080:rate=2',*/ 71 | 72 | /* set output flags */ 73 | '-an', 74 | '-c:v', 75 | 'pam', 76 | '-pix_fmt', 77 | pixFmt, 78 | '-f', 79 | 'image2pipe', 80 | '-vf', 81 | 'fps=4,scale=640:360', 82 | 'pipe:1', 83 | ]; 84 | 85 | const ffmpeg = spawn(ffmpegPath, params, { 86 | stdio: ['ignore', 'pipe', 'ignore'], 87 | }); 88 | 89 | console.log(ffmpeg.spawnargs.join(' ')); 90 | 91 | ffmpeg.on('error', error => { 92 | console.log(error); 93 | }); 94 | 95 | ffmpeg.on('exit', (code, signal) => { 96 | console.log(`exit ${code} ${signal}`); 97 | // may be a race condition with these values 98 | console.log(`pam count: ${pamCounter}`); 99 | console.log(`diff count: ${diffCounter}`); 100 | console.log(`jpeg count: ${jpegCounter}`); 101 | }); 102 | 103 | const p2p = new P2P(); 104 | 105 | let pamCounter = 0; 106 | let diffCounter = 0; 107 | let jpegCounter = 0; 108 | 109 | p2p.on('pam', data => { 110 | ++pamCounter; 111 | }); 112 | 113 | let regions; 114 | 115 | if (target === 'all') { 116 | regions = null; 117 | } else { 118 | // 640 360 119 | const region1 = { 120 | name: 'TOP_LEFT', 121 | difference: 10, 122 | percent: 7, 123 | polygon: [ 124 | { x: 0, y: 0 }, 125 | { x: 319, y: 0 }, 126 | { x: 319, y: 179 }, 127 | { x: 0, y: 179 }, 128 | ], 129 | }; 130 | const region2 = { 131 | name: 'BOTTOM_LEFT', 132 | difference: 10, 133 | percent: 7, 134 | polygon: [ 135 | { x: 0, y: 180 }, 136 | { x: 319, y: 180 }, 137 | { x: 319, y: 359 }, 138 | { x: 0, y: 359 }, 139 | ], 140 | }; 141 | const region3 = { 142 | name: 'TOP_RIGHT', 143 | difference: 10, 144 | percent: 7, 145 | polygon: [ 146 | { x: 320, y: 0 }, 147 | { x: 639, y: 0 }, 148 | { x: 639, y: 179 }, 149 | { x: 320, y: 179 }, 150 | ], 151 | }; 152 | regions = [region1, region2, region3]; 153 | } 154 | 155 | const pamDiff = new PamDiff({ regions: regions, mask: mask, response: response, sync: sync, draw: draw }); 156 | 157 | console.log({ mask, response, sync, draw, regions }); 158 | 159 | pamDiff.on('diff', data => { 160 | // console.log(data); 161 | 162 | ++diffCounter; 163 | // comment out the following line if you want to use ffmpeg to create a jpeg from the pam image that triggered an image difference event 164 | // if(true){return;} 165 | 166 | const date = new Date(); 167 | let name = `${pixFmt}-${toBool(sync) ? 'sync' : 'async'}-${diffCounter}`; 168 | for (const region of data.trigger) { 169 | if (response === 'bounds') { 170 | name += `--${region.name}-percent${region.percent}-minX${region.minX}-maxX${region.maxX}-minY${region.minY}-maxY${region.maxY}`; 171 | } else { 172 | name += `--${region.name}-percent${region.percent}`; 173 | } 174 | } 175 | const jpeg = `${name}.jpeg`; 176 | const pathToJpeg = `${basePathToJpeg}${jpeg}`; 177 | 178 | // const ff = execFile(ffmpegPath, ['-y', '-f', 'rawvideo', '-pix_fmt', 'gray', '-s', '640x360', '-i', 'pipe:0', '-frames', 1, '-c:v', 'mjpeg', '-pix_fmt', 'yuvj422p', '-q:v', '1', '-huffman', 'optimal', pathToJpeg]); 179 | 180 | const ff = execFile(ffmpegPath, ['-y', '-f', 'pam_pipe', '-c:v', 'pam', '-i', 'pipe:0', '-c:v', 'mjpeg', '-pix_fmt', 'yuvj420p', '-q:v', 1, '-huffman', 1, pathToJpeg]); 181 | 182 | ff.on('exit', (data, other) => { 183 | if (data === 0) { 184 | // console.log(`FFMPEG clean exit after creating ${jpeg}`); 185 | ++jpegCounter; 186 | } else { 187 | throw new Error('FFMPEG is not working with current parameters'); 188 | } 189 | }); 190 | 191 | ff.stderr.on('data', data => { 192 | // console.log('ffmpeg stderr data', data); 193 | }); 194 | 195 | ff.stdin.write(data.headers); 196 | ff.stdin.end(data.pixels); 197 | 198 | const writeStream = createWriteStream(`${pathToJpeg}.pam`); 199 | writeStream.write(data.headers); 200 | writeStream.end(data.pixels); 201 | 202 | if (global.gc) { 203 | global.gc(); 204 | } 205 | }); 206 | 207 | ffmpeg.stdout.pipe(p2p).pipe(pamDiff); 208 | -------------------------------------------------------------------------------- /examples/createPam2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { config: dotenvConfig } = require('dotenv'); 4 | 5 | dotenvConfig(); 6 | 7 | const argv = require('minimist')(process.argv.slice(2)); 8 | 9 | function getVal(...vals) { 10 | // returns first defined val or undefined 11 | for (let val of vals) { 12 | if (val === undefined) { 13 | continue; 14 | } 15 | return val; 16 | } 17 | return undefined; 18 | } 19 | 20 | // get vals in order of priority, args || env || default 21 | 22 | const target = getVal(argv.target, process.env.TARGET, 'all'); // all || region(s) 23 | 24 | const mask = getVal(argv.mask, process.env.MASK, false); // true || false 25 | 26 | const sync = getVal(argv.sync, process.env.SYNC, false); // true || false 27 | 28 | const response = getVal(argv.response, process.env.RESPONSE, 'percent'); // percent || bounds || blobs 29 | 30 | const draw = getVal(argv.draw, process.env.DRAW, false); // true || false 31 | 32 | const pixFmt = getVal(argv.pixfmt, process.env.PIXFMT, 'gray'); // gray || rgb24 || rgba 33 | 34 | const { cpus } = require('os'); 35 | 36 | const basePathToJpeg = `${__dirname}/out/${(response === 'bounds' || response === 'blobs') && toBool(draw) ? 'draw' : 'pam'}/`; 37 | 38 | function toBool(val) { 39 | return val === true || val === 'true' || val === 1 || val === '1'; 40 | } 41 | 42 | console.log(`cpu cores available: ${cpus().length}`); 43 | 44 | const P2P = require('pipe2pam'); 45 | const PamDiff = require('../index'); 46 | const ffmpegPath = require('../lib/ffmpeg'); 47 | const { spawn, execFile } = require('child_process'); 48 | const { createWriteStream } = require('fs'); 49 | 50 | const params = [ 51 | '-loglevel', 52 | 'quiet', 53 | 54 | /* use hardware acceleration */ 55 | '-hwaccel', 56 | 'auto', // vda, videotoolbox, none, auto 57 | 58 | '-stream_loop', 59 | '-1', // -1 for infinite 60 | 61 | /* use a pre-recorded mp4 video as input */ 62 | '-re', // comment out to have ffmpeg read video as fast as possible 63 | '-i', 64 | `${__dirname}/in/circle_star.mp4`, 65 | 66 | /* '-re', 67 | '-f', 68 | 'lavfi', 69 | '-i', 70 | 'testsrc=size=1920x1080:rate=2',*/ 71 | 72 | /* set output flags */ 73 | '-an', 74 | '-c:v', 75 | 'pam', 76 | '-pix_fmt', 77 | pixFmt, 78 | '-f', 79 | 'image2pipe', 80 | '-vf', 81 | 'fps=4,scale=1920:1080', 82 | 'pipe:1', 83 | ]; 84 | 85 | const ffmpeg = spawn(ffmpegPath, params, { 86 | stdio: ['ignore', 'pipe', 'ignore'], 87 | }); 88 | 89 | console.log(ffmpeg.spawnargs.join(' ')); 90 | 91 | ffmpeg.on('error', error => { 92 | console.log(error); 93 | }); 94 | 95 | ffmpeg.on('exit', (code, signal) => { 96 | console.log(`exit ${code} ${signal}`); 97 | // may be a race condition with these values 98 | console.log(`pam count: ${pamCounter}`); 99 | console.log(`diff count: ${diffCounter}`); 100 | console.log(`jpeg count: ${jpegCounter}`); 101 | }); 102 | 103 | const p2p = new P2P(); 104 | 105 | let pamCounter = 0; 106 | let diffCounter = 0; 107 | let jpegCounter = 0; 108 | 109 | p2p.on('pam', data => { 110 | ++pamCounter; 111 | }); 112 | 113 | let regions; 114 | 115 | if (target === 'all') { 116 | regions = null; 117 | } else { 118 | const region1 = { 119 | name: 'TOP_LEFT', 120 | difference: 10, 121 | percent: 7, 122 | polygon: [ 123 | { x: 0, y: 0 }, 124 | { x: 959, y: 0 }, 125 | { x: 959, y: 539 }, 126 | { x: 0, y: 539 }, 127 | ], 128 | }; 129 | const region2 = { 130 | name: 'BOTTOM_LEFT', 131 | difference: 10, 132 | percent: 7, 133 | polygon: [ 134 | { x: 0, y: 540 }, 135 | { x: 959, y: 540 }, 136 | { x: 959, y: 1079 }, 137 | { x: 0, y: 1079 }, 138 | ], 139 | }; 140 | const region3 = { 141 | name: 'TOP_RIGHT', 142 | difference: 10, 143 | percent: 7, 144 | polygon: [ 145 | { x: 960, y: 0 }, 146 | { x: 1919, y: 0 }, 147 | { x: 1919, y: 539 }, 148 | { x: 960, y: 539 }, 149 | ], 150 | }; 151 | regions = [region1, region2, region3]; 152 | } 153 | 154 | const pamDiff = new PamDiff({ percent: 2, regions: regions, mask: mask, response: response, sync: sync, draw: draw }); 155 | 156 | pamDiff.on('diff', data => { 157 | // console.log(data); 158 | 159 | ++diffCounter; 160 | 161 | if (global.gc) { 162 | global.gc(); 163 | } 164 | 165 | // comment out the following line if you want to use ffmpeg to create a jpeg from the pam image that triggered an image difference event 166 | // if(true){return;} 167 | 168 | const date = new Date(); 169 | let name = `${pixFmt}-${toBool(sync) ? 'sync' : 'async'}-${diffCounter}`; 170 | for (const region of data.trigger) { 171 | if (response === 'bounds') { 172 | name += `--${region.name}-percent${region.percent}-minX${region.minX}-maxX${region.maxX}-minY${region.minY}-maxY${region.maxY}`; 173 | } else { 174 | name += `--${region.name}-percent${region.percent}`; 175 | } 176 | } 177 | const jpeg = `${name}.jpeg`; 178 | const pathToJpeg = `${basePathToJpeg}${jpeg}`; 179 | 180 | // const ff = execFile(ffmpegPath, ['-y', '-f', 'rawvideo', '-pix_fmt', 'gray', '-s', '640x360', '-i', 'pipe:0', '-frames', 1, '-c:v', 'mjpeg', '-pix_fmt', 'yuvj422p', '-q:v', '1', '-huffman', 'optimal', pathToJpeg]); 181 | 182 | // ff.stdin.end(data.bc); 183 | 184 | /* const ff = execFile(ffmpegPath, ['-y', '-f', 'pam_pipe', '-c:v', 'pam', '-i', 'pipe:0', '-c:v', 'mjpeg', '-pix_fmt', 'yuvj420p', '-q:v', 1, '-huffman', 1, pathToJpeg]); 185 | 186 | ff.on('exit', (data, other) => { 187 | if (data === 0) { 188 | //console.log(`FFMPEG clean exit after creating ${jpeg}`); 189 | ++jpegCounter; 190 | } else { 191 | throw new Error('FFMPEG is not working with current parameters'); 192 | } 193 | }); 194 | 195 | ff.stderr.on('data', data => { 196 | //console.log('ffmpeg stderr data', data); 197 | }); 198 | 199 | ff.stdin.write(data.headers); 200 | ff.stdin.end(data.pixels);*/ 201 | 202 | const writeStream = createWriteStream(`${pathToJpeg}.pam`); 203 | writeStream.write(data.headers); 204 | writeStream.end(data.pixels); 205 | 206 | if (global.gc) { 207 | global.gc(); 208 | } 209 | }); 210 | 211 | ffmpeg.stdout.pipe(p2p).pipe(pamDiff); 212 | -------------------------------------------------------------------------------- /examples/createPam3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { config: dotenvConfig } = require('dotenv'); 4 | 5 | dotenvConfig(); 6 | 7 | const argv = require('minimist')(process.argv.slice(2)); 8 | 9 | function getVal(...vals) { 10 | // returns first defined val or undefined 11 | for (let val of vals) { 12 | if (val === undefined) { 13 | continue; 14 | } 15 | return val; 16 | } 17 | return undefined; 18 | } 19 | 20 | // get vals in order of priority, args || env || default 21 | 22 | const target = getVal(argv.target, process.env.TARGET, 'all'); // all || region(s) 23 | 24 | const mask = getVal(argv.mask, process.env.MASK, false); // true || false 25 | 26 | const sync = getVal(argv.sync, process.env.SYNC, false); // true || false 27 | 28 | const response = getVal(argv.response, process.env.RESPONSE, 'percent'); // percent || bounds || blobs 29 | 30 | const draw = getVal(argv.draw, process.env.DRAW, false); // true || false 31 | 32 | const pixFmt = getVal(argv.pixfmt, process.env.PIXFMT, 'gray'); // gray || rgb24 || rgba 33 | 34 | const outPath = getVal(argv.outPath, process.env.OUT_PATH, './'); // ./ 35 | 36 | const { cpus } = require('os'); 37 | 38 | const basePathToJpeg = `${__dirname}/out/${(response === 'bounds' || response === 'blobs') && toBool(draw) ? 'draw' : 'pam'}/`; 39 | 40 | function toBool(val) { 41 | return val === true || val === 'true' || val === 1 || val === '1'; 42 | } 43 | 44 | console.log(`cpu cores available: ${cpus().length}`); 45 | 46 | const P2P = require('pipe2pam'); 47 | const PamDiff = require('../index'); 48 | const ffmpegPath = require('../lib/ffmpeg'); 49 | const { spawn, execFile } = require('child_process'); 50 | const { createWriteStream } = require('fs'); 51 | 52 | const params = [ 53 | '-loglevel', 54 | 'quiet', 55 | 56 | /* use hardware acceleration */ 57 | '-hwaccel', 58 | 'auto', // vda, videotoolbox, none, auto 59 | 60 | // '-stream_loop', '-1',//-1 for infinite 61 | 62 | /* use a pre-recorded mp4 video as input */ 63 | '-re', // comment out to have ffmpeg read video as fast as possible 64 | 65 | '-rtsp_transport', 66 | 'tcp', 67 | '-i', 68 | // `${__dirname}/in/circle_star.mp4`, 69 | // 'https://cdn.shinobi.video/videos/bears.mp4', 70 | // 'https://cdn.shinobi.video/videos/PlateDemo_Front.mp4', 71 | 'rtsp://192.168.1.23:554/user=admin_password=pass_channel=1_stream=1.sdp', 72 | 73 | /* '-re', 74 | '-f', 75 | 'lavfi', 76 | '-i', 77 | 'testsrc=size=1920x1080:rate=2',*/ 78 | 79 | /* set output flags */ 80 | '-an', 81 | '-c:v', 82 | 'pam', 83 | '-pix_fmt', 84 | pixFmt, 85 | '-f', 86 | 'image2pipe', 87 | '-vf', 88 | // 'fps=4,scale=1920:1080', 89 | 'fps=2,scale=640:360', 90 | 'pipe:1', 91 | ]; 92 | 93 | const ffmpeg = spawn(`${outPath}ffmpeg`, params, { 94 | stdio: ['ignore', 'pipe', 'ignore'], 95 | }); 96 | 97 | console.log(ffmpeg.spawnargs.join(' ')); 98 | 99 | ffmpeg.on('error', error => { 100 | console.log(error); 101 | }); 102 | 103 | ffmpeg.on('exit', (code, signal) => { 104 | console.log(`exit ${code} ${signal}`); 105 | // may be a race condition with these values 106 | console.log(`pam count: ${pamCounter}`); 107 | console.log(`diff count: ${diffCounter}`); 108 | console.log(`jpeg count: ${jpegCounter}`); 109 | }); 110 | 111 | const p2p = new P2P(); 112 | 113 | let pamCounter = 0; 114 | let diffCounter = 0; 115 | let jpegCounter = 0; 116 | 117 | p2p.on('pam', data => { 118 | ++pamCounter; 119 | }); 120 | 121 | let regions; 122 | 123 | if (target === 'all') { 124 | regions = null; 125 | } else { 126 | const region1 = { 127 | name: 'TOP_LEFT', 128 | difference: 10, 129 | percent: 7, 130 | polygon: [ 131 | { x: 0, y: 0 }, 132 | { x: 959, y: 0 }, 133 | { x: 959, y: 539 }, 134 | { x: 0, y: 539 }, 135 | ], 136 | }; 137 | const region2 = { 138 | name: 'BOTTOM_LEFT', 139 | difference: 10, 140 | percent: 7, 141 | polygon: [ 142 | { x: 0, y: 540 }, 143 | { x: 959, y: 540 }, 144 | { x: 959, y: 1079 }, 145 | { x: 0, y: 1079 }, 146 | ], 147 | }; 148 | const region3 = { 149 | name: 'TOP_RIGHT', 150 | difference: 10, 151 | percent: 7, 152 | polygon: [ 153 | { x: 960, y: 0 }, 154 | { x: 1919, y: 0 }, 155 | { x: 1919, y: 539 }, 156 | { x: 960, y: 539 }, 157 | ], 158 | }; 159 | regions = [region1, region2, region3]; 160 | } 161 | 162 | const pamDiff = new PamDiff({ percent: 2, regions: regions, mask: mask, response: response, sync: sync, draw: draw }); 163 | 164 | pamDiff.on('diff', data => { 165 | // console.log(data); 166 | 167 | ++diffCounter; 168 | // comment out the following line if you want to use ffmpeg to create a jpeg from the pam image that triggered an image difference event 169 | // if(true){return;} 170 | 171 | const date = new Date(); 172 | let name = `${pixFmt}-${toBool(sync) ? 'sync' : 'async'}-${diffCounter}`; 173 | for (const region of data.trigger) { 174 | if (response === 'bounds') { 175 | name += `--${region.name}-percent${region.percent}-minX${region.minX}-maxX${region.maxX}-minY${region.minY}-maxY${region.maxY}`; 176 | } else { 177 | name += `--${region.name}-percent${region.percent}`; 178 | } 179 | } 180 | const jpeg = `${name}.jpeg`; 181 | const pathToJpeg = `${outPath}${jpeg}`; 182 | 183 | // const ff = execFile(ffmpegPath, ['-y', '-f', 'rawvideo', '-pix_fmt', 'gray', '-s', '640x360', '-i', 'pipe:0', '-frames', 1, '-c:v', 'mjpeg', '-pix_fmt', 'yuvj422p', '-q:v', '1', '-huffman', 'optimal', pathToJpeg]); 184 | 185 | // ff.stdin.end(data.bc); 186 | 187 | /* const ff = execFile(ffmpegPath, ['-y', '-f', 'pam_pipe', '-c:v', 'pam', '-i', 'pipe:0', '-c:v', 'mjpeg', '-pix_fmt', 'yuvj420p', '-q:v', 1, '-huffman', 1, pathToJpeg]); 188 | 189 | ff.on('exit', (data, other) => { 190 | if (data === 0) { 191 | //console.log(`FFMPEG clean exit after creating ${jpeg}`); 192 | ++jpegCounter; 193 | } else { 194 | throw new Error('FFMPEG is not working with current parameters'); 195 | } 196 | }); 197 | 198 | ff.stderr.on('data', data => { 199 | //console.log('ffmpeg stderr data', data); 200 | }); 201 | 202 | ff.stdin.write(data.headers); 203 | ff.stdin.end(data.pixels);*/ 204 | 205 | const writeStream = createWriteStream(`${pathToJpeg}.pam`); 206 | writeStream.write(data.headers); 207 | writeStream.end(data.pixels); 208 | 209 | console.log(`${pathToJpeg}.pam`); 210 | 211 | if (global.gc) { 212 | global.gc(); 213 | } 214 | }); 215 | 216 | ffmpeg.stdout.pipe(p2p).pipe(pamDiff); 217 | -------------------------------------------------------------------------------- /examples/example.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.NODE_ENV = 'development'; 4 | 5 | const P2P = require('pipe2pam'); 6 | const PamDiff = require('../index'); 7 | const ffmpegPath = require('../lib/ffmpeg'); 8 | const ChildProcess = require('child_process'); 9 | const spawn = ChildProcess.spawn; 10 | const execFile = ChildProcess.execFile; 11 | 12 | const params = [ 13 | '-loglevel', 14 | 'quiet', 15 | 16 | /* use hardware acceleration */ 17 | '-hwaccel', 18 | 'auto', // vda, videotoolbox, none, auto 19 | 20 | /* use an artificial video input */ 21 | // '-re', 22 | 23 | // '-f', 24 | // 'lavfi', 25 | // '-i', 26 | // 'testsrc=size=1920x1080:rate=15', 27 | 28 | /* use an rtsp ip cam video input */ 29 | '-rtsp_transport', 30 | 'tcp', 31 | '-i', 32 | 'rtsp://192.168.1.22:554/user=admin_password=pass_channel=1_stream=0.sdp', 33 | 34 | /* set output flags */ 35 | '-an', 36 | '-c:v', 37 | 'pam', 38 | '-pix_fmt', 39 | 'gray', 40 | // 'rgba', 41 | // 'rgb24', 42 | '-f', 43 | 'image2pipe', 44 | '-vf', 45 | 'fps=2,scale=400:225', // 1920:1080 scaled down = 640:360, 400:225, 384:216, 368:207, 352:198, 336:189, 320:180 46 | // 'fps=1,scale=iw*1/6:ih*1/6', 47 | '-frames', 48 | '100', 49 | 'pipe:1', 50 | ]; 51 | 52 | const ffmpeg = spawn(ffmpegPath, params, { 53 | stdio: ['ignore', 'pipe', 'ignore'], 54 | }); 55 | 56 | console.log(ffmpeg.spawnargs.join(' ')); 57 | 58 | ffmpeg.on('error', error => { 59 | console.log(error); 60 | }); 61 | 62 | ffmpeg.on('exit', (code, signal) => { 63 | console.log('exit', code, signal); 64 | }); 65 | 66 | const p2p = new P2P(); 67 | 68 | let counter = 0; 69 | 70 | p2p.on('pam', data => { 71 | // you do not have to listen to this event if you are just piping this data to pam-diff 72 | console.log(`received pam ${++counter}`); 73 | }); 74 | 75 | const pamDiff = new PamDiff({ difference: 10, percent: 10, response: 'bounds' }); 76 | 77 | pamDiff.on('diff', data => { 78 | console.log(data); 79 | 80 | // comment out the following line if you want to use ffmpeg to create a jpeg from the pam image that triggered an image difference event 81 | if (true) { 82 | return; 83 | } 84 | 85 | const date = new Date(); 86 | let name = `${date.getUTCFullYear()}-${date.getUTCMonth() + 1}-${date.getUTCDate()}_${date.getHours()}-${date.getUTCMinutes()}-${date.getUTCSeconds()}-${date.getUTCMilliseconds()}`; 87 | for (const region of data.trigger) { 88 | name += `(${region.name}=${region.percent})`; 89 | } 90 | const jpeg = `${name}.jpeg`; 91 | const ff = execFile(ffmpegPath, ['-f', 'pam_pipe', '-c:v', 'pam', '-i', 'pipe:0', '-c:v', 'mjpeg', '-pix_fmt', 'yuvj422p', '-q:v', '1', '-huffman', 'optimal', jpeg]); 92 | ff.stdin.end(data.pam); 93 | ff.on('exit', data => { 94 | if (data === 0) { 95 | console.log(`FFMPEG clean exit after creating ${jpeg}`); 96 | } else { 97 | throw new Error('FFMPEG is not working with current parameters'); 98 | } 99 | }); 100 | }); 101 | 102 | ffmpeg.stdout.pipe(p2p).pipe(pamDiff); 103 | -------------------------------------------------------------------------------- /examples/example2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.NODE_ENV = 'development'; 4 | 5 | const P2P = require('pipe2pam'); 6 | const PamDiff = require('../index'); 7 | const ffmpegPath = require('../lib/ffmpeg'); 8 | const ChildProcess = require('child_process'); 9 | const spawn = ChildProcess.spawn; 10 | const execFile = ChildProcess.execFile; 11 | 12 | const params = [ 13 | '-loglevel', 14 | 'quiet', 15 | 16 | /* use hardware acceleration */ 17 | '-hwaccel', 18 | 'auto', // vda, videotoolbox, none, auto 19 | 20 | /* use an artificial video input */ 21 | // '-re', 22 | '-f', 23 | 'lavfi', 24 | '-i', 25 | 'testsrc=size=1920x1080:rate=15', 26 | 27 | /* use an rtsp ip cam video input */ 28 | /* '-rtsp_transport', 29 | 'tcp', 30 | '-i', 31 | 'rtsp://192.168.1.22:554/user=admin_password=pass_channel=1_stream=0.sdp',*/ 32 | 33 | /* set output flags */ 34 | '-an', 35 | '-c:v', 36 | 'pam', 37 | '-pix_fmt', 38 | // 'gray', 39 | // 'rgba', 40 | 'rgb24', 41 | '-f', 42 | 'image2pipe', 43 | '-vf', 44 | 'fps=2,scale=400:225', // 1920:1080 scaled down = 640:360, 400:225, 384:216, 368:207, 352:198, 336:189, 320:180 45 | // 'fps=1,scale=iw*1/6:ih*1/6', 46 | '-frames', 47 | '100', 48 | 'pipe:1', 49 | ]; 50 | 51 | const ffmpeg = spawn(ffmpegPath, params, { 52 | stdio: ['ignore', 'pipe', 'ignore'], 53 | }); 54 | 55 | console.log(ffmpeg.spawnargs.join(' ')); 56 | 57 | ffmpeg.on('error', error => { 58 | console.log(error); 59 | }); 60 | 61 | ffmpeg.on('exit', (code, signal) => { 62 | console.log('exit', code, signal); 63 | }); 64 | 65 | const p2p = new P2P(); 66 | 67 | let counter = 0; 68 | 69 | p2p.on('pam', data => { 70 | // you do not have to listen to this event if you are just piping this data to pam-diff 71 | console.log(`received pam ${++counter}`); 72 | }); 73 | 74 | const region1 = { 75 | name: 'region1', 76 | difference: 1, 77 | percent: 1, 78 | polygon: [ 79 | { x: 0, y: 0 }, 80 | { x: 0, y: 225 }, 81 | { x: 100, y: 225 }, 82 | { x: 100, y: 0 }, 83 | ], 84 | }; 85 | 86 | const regions = [region1]; 87 | 88 | const pamDiff = new PamDiff({ regions: regions, response: 'bounds' }); 89 | 90 | pamDiff.on('diff', data => { 91 | console.log(data); 92 | 93 | // comment out the following line if you want to use ffmpeg to create a jpeg from the pam image that triggered an image difference event 94 | if (true) { 95 | return; 96 | } 97 | 98 | const date = new Date(); 99 | let name = `${date.getUTCFullYear()}-${date.getUTCMonth() + 1}-${date.getUTCDate()}_${date.getHours()}-${date.getUTCMinutes()}-${date.getUTCSeconds()}-${date.getUTCMilliseconds()}`; 100 | for (const region of data.trigger) { 101 | name += `(${region.name}=${region.percent})`; 102 | } 103 | const jpeg = `${name}.jpeg`; 104 | const ff = execFile(ffmpegPath, ['-f', 'pam_pipe', '-c:v', 'pam', '-i', 'pipe:0', '-c:v', 'mjpeg', '-pix_fmt', 'yuvj422p', '-q:v', '1', '-huffman', 'optimal', jpeg]); 105 | ff.stdin.end(data.pam); 106 | ff.on('exit', data => { 107 | if (data === 0) { 108 | console.log(`FFMPEG clean exit after creating ${jpeg}`); 109 | } else { 110 | throw new Error('FFMPEG is not working with current parameters'); 111 | } 112 | }); 113 | }); 114 | 115 | ffmpeg.stdout.pipe(p2p).pipe(pamDiff); 116 | -------------------------------------------------------------------------------- /examples/example3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.NODE_ENV = 'development'; 4 | 5 | const P2P = require('pipe2pam'); 6 | const PamDiff = require('../index'); 7 | const ffmpegPath = require('../lib/ffmpeg'); 8 | const ChildProcess = require('child_process'); 9 | const spawn = ChildProcess.spawn; 10 | const execFile = ChildProcess.execFile; 11 | 12 | const params = [ 13 | '-loglevel', 14 | 'quiet', 15 | 16 | /* use hardware acceleration */ 17 | '-hwaccel', 18 | 'auto', // vda, videotoolbox, none, auto 19 | 20 | /* use an artificial video input */ 21 | // '-re', 22 | '-f', 23 | 'lavfi', 24 | '-i', 25 | 'testsrc=size=1920x1080:rate=15', 26 | 27 | /* use an rtsp ip cam video input */ 28 | /* '-rtsp_transport', 29 | 'tcp', 30 | '-i', 31 | 'rtsp://192.168.1.22:554/user=admin_password=pass_channel=1_stream=0.sdp',*/ 32 | 33 | /* set output flags */ 34 | '-an', 35 | '-c:v', 36 | 'pam', 37 | '-pix_fmt', 38 | // 'gray', 39 | // 'rgba', 40 | 'rgb24', 41 | '-f', 42 | 'image2pipe', 43 | '-vf', 44 | 'fps=2,scale=400:225', // 1920:1080 scaled down = 640:360, 400:225, 384:216, 368:207, 352:198, 336:189, 320:180 45 | // 'fps=1,scale=iw*1/6:ih*1/6', 46 | '-frames', 47 | '100', 48 | 'pipe:1', 49 | ]; 50 | 51 | const ffmpeg = spawn(ffmpegPath, params, { 52 | stdio: ['ignore', 'pipe', 'ignore'], 53 | }); 54 | 55 | console.log(ffmpeg.spawnargs.join(' ')); 56 | 57 | ffmpeg.on('error', error => { 58 | console.log(error); 59 | }); 60 | 61 | ffmpeg.on('exit', (code, signal) => { 62 | console.log('exit', code, signal); 63 | }); 64 | 65 | const p2p = new P2P(); 66 | 67 | let counter = 0; 68 | 69 | p2p.on('pam', data => { 70 | // you do not have to listen to this event if you are just piping this data to pam-diff 71 | console.log(`received pam ${++counter}`); 72 | }); 73 | 74 | const region1 = { 75 | name: 'region1', 76 | difference: 1, 77 | percent: 1, 78 | polygon: [ 79 | { x: 0, y: 0 }, 80 | { x: 0, y: 225 }, 81 | { x: 100, y: 225 }, 82 | { x: 100, y: 0 }, 83 | ], 84 | }; 85 | 86 | const region2 = { 87 | name: 'region2', 88 | difference: 1, 89 | percent: 1, 90 | polygon: [ 91 | { x: 100, y: 0 }, 92 | { x: 100, y: 225 }, 93 | { x: 200, y: 225 }, 94 | { x: 200, y: 0 }, 95 | ], 96 | }; 97 | 98 | const region3 = { 99 | name: 'region3', 100 | difference: 1, 101 | percent: 1, 102 | polygon: [ 103 | { x: 200, y: 0 }, 104 | { x: 200, y: 225 }, 105 | { x: 300, y: 225 }, 106 | { x: 300, y: 0 }, 107 | ], 108 | }; 109 | 110 | const region4 = { 111 | name: 'region4', 112 | difference: 1, 113 | percent: 1, 114 | polygon: [ 115 | { x: 300, y: 0 }, 116 | { x: 300, y: 225 }, 117 | { x: 400, y: 225 }, 118 | { x: 400, y: 0 }, 119 | ], 120 | }; 121 | 122 | const regions = [region1, region2, region3, region4]; 123 | 124 | const pamDiff = new PamDiff({ regions: regions, response: 'bounds' }); 125 | 126 | pamDiff.on('diff', data => { 127 | console.log(data); 128 | 129 | // comment out the following line if you want to use ffmpeg to create a jpeg from the pam image that triggered an image difference event 130 | if (true) { 131 | return; 132 | } 133 | 134 | const date = new Date(); 135 | let name = `${date.getUTCFullYear()}-${date.getUTCMonth() + 1}-${date.getUTCDate()}_${date.getHours()}-${date.getUTCMinutes()}-${date.getUTCSeconds()}-${date.getUTCMilliseconds()}`; 136 | for (const region of data.trigger) { 137 | name += `(${region.name}=${region.percent})`; 138 | } 139 | const jpeg = `${name}.jpeg`; 140 | const ff = execFile(ffmpegPath, ['-f', 'pam_pipe', '-c:v', 'pam', '-i', 'pipe:0', '-c:v', 'mjpeg', '-pix_fmt', 'yuvj422p', '-q:v', '1', '-huffman', 'optimal', jpeg]); 141 | ff.stdin.end(data.pam); 142 | ff.on('exit', data => { 143 | if (data === 0) { 144 | console.log(`FFMPEG clean exit after creating ${jpeg}`); 145 | } else { 146 | throw new Error('FFMPEG is not working with current parameters'); 147 | } 148 | }); 149 | }); 150 | 151 | ffmpeg.stdout.pipe(p2p).pipe(pamDiff); 152 | -------------------------------------------------------------------------------- /examples/example4.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.NODE_ENV = 'development'; 4 | 5 | const P2P = require('pipe2pam'); 6 | const PamDiff = require('../index'); 7 | const ffmpegPath = require('../lib/ffmpeg'); 8 | const ChildProcess = require('child_process'); 9 | const spawn = ChildProcess.spawn; 10 | const execFile = ChildProcess.execFile; 11 | 12 | const params = [ 13 | '-loglevel', 14 | 'quiet', 15 | 16 | /* use hardware acceleration */ 17 | '-hwaccel', 18 | 'auto', // vda, videotoolbox, none, auto 19 | 20 | /* use an artificial video input */ 21 | // '-re', 22 | '-f', 23 | 'lavfi', 24 | '-i', 25 | 'testsrc=size=1920x1080:rate=15', 26 | 27 | /* use an rtsp ip cam video input */ 28 | /* '-rtsp_transport', 29 | 'tcp', 30 | '-i', 31 | 'rtsp://192.168.1.22:554/user=admin_password=pass_channel=1_stream=0.sdp',*/ 32 | 33 | /* set output flags */ 34 | '-an', 35 | '-c:v', 36 | 'pam', 37 | '-pix_fmt', 38 | // 'gray', 39 | // 'rgba', 40 | 'rgb24', 41 | // 'monob', 42 | '-f', 43 | 'image2pipe', 44 | '-vf', 45 | 'fps=2,scale=640:360', // 1920:1080 scaled down = 640:360, 400:225, 384:216, 368:207, 352:198, 336:189, 320:180 46 | // 'fps=1,scale=iw*1/6:ih*1/6', 47 | '-frames', 48 | '100', 49 | 'pipe:1', 50 | ]; 51 | 52 | const ffmpeg = spawn(ffmpegPath, params, { 53 | stdio: ['ignore', 'pipe', 'ignore'], 54 | }); 55 | 56 | console.log(ffmpeg.spawnargs.join(' ')); 57 | 58 | ffmpeg.on('error', error => { 59 | console.log(error); 60 | }); 61 | 62 | ffmpeg.on('exit', (code, signal) => { 63 | console.log('exit', code, signal); 64 | }); 65 | 66 | const p2p = new P2P(); 67 | 68 | let counter = 0; 69 | 70 | p2p.on('pam', data => { 71 | // you do not have to listen to this event if you are just piping this data to pam-diff 72 | console.log(`received pam ${++counter}`); 73 | }); 74 | 75 | const region1 = { 76 | name: 'region1', 77 | difference: 1, 78 | percent: 1, 79 | polygon: [ 80 | { x: 0, y: 0 }, 81 | { x: 0, y: 360 }, 82 | { x: 160, y: 360 }, 83 | { x: 160, y: 0 }, 84 | ], 85 | }; 86 | 87 | const region2 = { 88 | name: 'region2', 89 | difference: 1, 90 | percent: 1, 91 | polygon: [ 92 | { x: 160, y: 0 }, 93 | { x: 160, y: 360 }, 94 | { x: 320, y: 360 }, 95 | { x: 320, y: 0 }, 96 | ], 97 | }; 98 | 99 | const region3 = { 100 | name: 'region3', 101 | difference: 1, 102 | percent: 1, 103 | polygon: [ 104 | { x: 320, y: 0 }, 105 | { x: 320, y: 360 }, 106 | { x: 480, y: 360 }, 107 | { x: 480, y: 0 }, 108 | ], 109 | }; 110 | 111 | const region4 = { 112 | name: 'region4', 113 | difference: 1, 114 | percent: 1, 115 | polygon: [ 116 | { x: 480, y: 0 }, 117 | { x: 480, y: 360 }, 118 | { x: 640, y: 360 }, 119 | { x: 640, y: 0 }, 120 | ], 121 | }; 122 | 123 | const regions = [region1, region2, region3, region4]; 124 | 125 | const pamDiff = new PamDiff({ regions: regions, response: 'bounds' }); 126 | 127 | pamDiff.on('diff', data => { 128 | console.log(data); 129 | 130 | // comment out the following line if you want to use ffmpeg to create a jpeg from the pam image that triggered an image difference event 131 | if (true) { 132 | return; 133 | } 134 | 135 | const date = new Date(); 136 | let name = `${date.getUTCFullYear()}-${date.getUTCMonth() + 1}-${date.getUTCDate()}_${date.getHours()}-${date.getUTCMinutes()}-${date.getUTCSeconds()}-${date.getUTCMilliseconds()}`; 137 | for (const region of data.trigger) { 138 | name += `(${region.name}=${region.percent})`; 139 | } 140 | const jpeg = `${name}.jpeg`; 141 | const ff = execFile(ffmpegPath, ['-f', 'pam_pipe', '-c:v', 'pam', '-i', 'pipe:0', '-c:v', 'mjpeg', '-pix_fmt', 'yuvj422p', '-q:v', '1', '-huffman', 'optimal', jpeg]); 142 | ff.stdin.end(data.pam); 143 | ff.on('exit', data => { 144 | if (data === 0) { 145 | console.log(`FFMPEG clean exit after creating ${jpeg}`); 146 | } else { 147 | throw new Error('FFMPEG is not working with current parameters'); 148 | } 149 | }); 150 | }); 151 | 152 | ffmpeg.stdout.pipe(p2p).pipe(pamDiff); 153 | -------------------------------------------------------------------------------- /examples/example5.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const dotenv = require('dotenv'); 4 | 5 | dotenv.config(); 6 | 7 | const argv = require('minimist')(process.argv.slice(2)); 8 | 9 | const async = argv.async || process.env.ASYNC || false; 10 | 11 | const response = argv.response || process.env.RESPONSE || 'percent'; 12 | 13 | const { cpus } = require('os'); 14 | 15 | console.log(`cpu cores available: ${cpus().length}`); 16 | 17 | const P2P = require('pipe2pam'); 18 | const PamDiff = require('../index'); 19 | const ffmpegPath = require('../lib/ffmpeg'); 20 | const ChildProcess = require('child_process'); 21 | const spawn = ChildProcess.spawn; 22 | const execFile = ChildProcess.execFile; 23 | 24 | const params = [ 25 | '-loglevel', 26 | 'quiet', 27 | 28 | /* use hardware acceleration */ 29 | '-hwaccel', 30 | 'auto', // vda, videotoolbox, none, auto 31 | 32 | /* use an artificial video input */ 33 | '-re', 34 | '-f', 35 | 'lavfi', 36 | '-i', 37 | 'testsrc=size=1920x1080:rate=15', 38 | 39 | /* use an rtsp ip cam video input */ 40 | /* '-rtsp_transport', 41 | 'tcp', 42 | '-i', 43 | //'rtsp://192.168.1.22:554/user=admin_password=pass_channel=1_stream=0.sdp', 44 | 'rtsp://131.95.3.162/axis-media/media.3gp',*/ 45 | 46 | /* set output flags */ 47 | '-an', 48 | '-c:v', 49 | 'pam', 50 | '-pix_fmt', 51 | 'gray', 52 | // 'rgba', 53 | // 'rgb24', 54 | // 'monob', 55 | '-f', 56 | 'image2pipe', 57 | '-vf', 58 | // 'fps=2,scale=1280:720', 59 | // 'fps=2,scale=1500:800', 60 | 'fps=2,scale=1920:1080', // 1920:1080 scaled down = 640:360, 400:225, 384:216, 368:207, 352:198, 336:189, 320:180 61 | // 'fps=1,scale=iw*1/6:ih*1/6', 62 | '-frames', 63 | '10000', 64 | 'pipe:1', 65 | ]; 66 | 67 | const ffmpeg = spawn(ffmpegPath, params, { 68 | stdio: ['ignore', 'pipe', 'ignore'], 69 | }); 70 | 71 | console.log(ffmpeg.spawnargs.join(' ')); 72 | 73 | ffmpeg.on('error', error => { 74 | console.log(error); 75 | }); 76 | 77 | ffmpeg.on('exit', (code, signal) => { 78 | console.log('exit', code, signal); 79 | }); 80 | 81 | const p2p = new P2P(); 82 | 83 | let counter = 0; 84 | 85 | p2p.on('pam', data => { 86 | // you do not have to listen to this event if you are just piping this data to pam-diff 87 | console.log(`received pam ${++counter}`); 88 | }); 89 | 90 | const region1 = { 91 | name: 'region1', 92 | difference: 1, 93 | percent: 1, 94 | polygon: [ 95 | { x: 0, y: 0 }, 96 | { x: 0, y: 1080 }, 97 | { x: 480, y: 1080 }, 98 | { x: 480, y: 0 }, 99 | ], 100 | }; 101 | 102 | const region2 = { 103 | name: 'region2', 104 | difference: 1, 105 | percent: 1, 106 | polygon: [ 107 | { x: 480, y: 0 }, 108 | { x: 480, y: 1080 }, 109 | { x: 960, y: 1080 }, 110 | { x: 960, y: 0 }, 111 | ], 112 | }; 113 | 114 | const region3 = { 115 | name: 'region3', 116 | difference: 1, 117 | percent: 1, 118 | polygon: [ 119 | { x: 960, y: 0 }, 120 | { x: 960, y: 1080 }, 121 | { x: 1440, y: 1080 }, 122 | { x: 1440, y: 0 }, 123 | ], 124 | }; 125 | 126 | const region4 = { 127 | name: 'region4', 128 | difference: 1, 129 | percent: 1, 130 | polygon: [ 131 | { x: 1440, y: 0 }, 132 | { x: 1440, y: 1080 }, 133 | { x: 1920, y: 1080 }, 134 | { x: 1920, y: 0 }, 135 | ], 136 | }; 137 | 138 | const regions = [region1, region2, region3, region4]; 139 | 140 | const pamDiff = new PamDiff({ regions: regions, response: response, async: async }); 141 | 142 | pamDiff.on('diff', data => { 143 | console.log(data); 144 | 145 | // comment out the following line if you want to use ffmpeg to create a jpeg from the pam image that triggered an image difference event 146 | if (true) { 147 | return; 148 | } 149 | 150 | const date = new Date(); 151 | let name = `${date.getUTCFullYear()}-${date.getUTCMonth() + 1}-${date.getUTCDate()}_${date.getHours()}-${date.getUTCMinutes()}-${date.getUTCSeconds()}-${date.getUTCMilliseconds()}`; 152 | for (const region of data.trigger) { 153 | name += `(${region.name}=${region.percent})`; 154 | } 155 | const jpeg = `${name}.jpeg`; 156 | const ff = execFile(ffmpegPath, ['-f', 'pam_pipe', '-c:v', 'pam', '-i', 'pipe:0', '-c:v', 'mjpeg', '-pix_fmt', 'yuvj422p', '-q:v', '1', '-huffman', 'optimal', jpeg]); 157 | ff.stdin.end(data.pam); 158 | ff.on('exit', data => { 159 | if (data === 0) { 160 | console.log(`FFMPEG clean exit after creating ${jpeg}`); 161 | } else { 162 | throw new Error('FFMPEG is not working with current parameters'); 163 | } 164 | }); 165 | }); 166 | 167 | ffmpeg.stdout.pipe(p2p).pipe(pamDiff); 168 | -------------------------------------------------------------------------------- /examples/example6.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const P2P = require('pipe2pam'); 4 | const PamDiff = require('../index'); 5 | const ffmpegPath = require('../lib/ffmpeg'); 6 | const ChildProcess = require('child_process'); 7 | const spawn = ChildProcess.spawn; 8 | const execFile = ChildProcess.execFile; 9 | 10 | const params = [ 11 | '-loglevel', 12 | 'quiet', 13 | 14 | /* use hardware acceleration */ 15 | '-hwaccel', 16 | 'auto', // vda, videotoolbox, none, auto 17 | 18 | /* use an artificial video input */ 19 | '-re', 20 | '-f', 21 | 'lavfi', 22 | '-i', 23 | 'testsrc=size=1920x1080:rate=15', 24 | 25 | /* use an rtsp ip cam video input */ 26 | /* '-rtsp_transport', 27 | 'tcp', 28 | '-i', 29 | 'rtsp://192.168.1.22:554/user=admin_password=pass_channel=1_stream=0.sdp',*/ 30 | 31 | /* set output flags */ 32 | '-an', 33 | '-c:v', 34 | 'pam', 35 | '-pix_fmt', 36 | // 'gray', 37 | // 'rgba', 38 | 'rgb24', 39 | '-f', 40 | 'image2pipe', 41 | '-vf', 42 | 'fps=2,scale=400:225', // 1920:1080 scaled down = 640:360, 400:225, 384:216, 368:207, 352:198, 336:189, 320:180 43 | // 'fps=1,scale=iw*1/6:ih*1/6', 44 | '-frames', 45 | '100', 46 | 'pipe:1', 47 | ]; 48 | 49 | const ffmpeg = spawn(ffmpegPath, params, { 50 | stdio: ['ignore', 'pipe', 'ignore'], 51 | }); 52 | 53 | console.log(ffmpeg.spawnargs.join(' ')); 54 | 55 | ffmpeg.on('error', error => { 56 | console.log(error); 57 | }); 58 | 59 | ffmpeg.on('exit', (code, signal) => { 60 | console.log('exit', code, signal); 61 | console.log(diffCount); 62 | }); 63 | 64 | const p2p = new P2P(); 65 | 66 | let counter = 0; 67 | 68 | p2p.on('pam', data => { 69 | // you do not have to listen to this event if you are just piping this data to pam-diff 70 | console.log(`received pam ${++counter}`); 71 | }); 72 | 73 | const region1 = { 74 | name: 'region1', 75 | difference: 1, 76 | percent: 1, 77 | polygon: [ 78 | { x: 0, y: 0 }, 79 | { x: 0, y: 225 }, 80 | { x: 100, y: 225 }, 81 | { x: 100, y: 0 }, 82 | ], 83 | }; 84 | 85 | const region2 = { 86 | name: 'region2', 87 | difference: 1, 88 | percent: 1, 89 | polygon: [ 90 | { x: 100, y: 0 }, 91 | { x: 100, y: 225 }, 92 | { x: 200, y: 225 }, 93 | { x: 200, y: 0 }, 94 | ], 95 | }; 96 | 97 | const region3 = { 98 | name: 'region3', 99 | difference: 1, 100 | percent: 1, 101 | polygon: [ 102 | { x: 200, y: 0 }, 103 | { x: 200, y: 225 }, 104 | { x: 300, y: 225 }, 105 | { x: 300, y: 0 }, 106 | ], 107 | }; 108 | 109 | const region4 = { 110 | name: 'region4', 111 | difference: 1, 112 | percent: 1, 113 | polygon: [ 114 | { x: 300, y: 0 }, 115 | { x: 300, y: 225 }, 116 | { x: 400, y: 225 }, 117 | { x: 400, y: 0 }, 118 | ], 119 | }; 120 | 121 | const regions = [region1, region2, region3, region4]; 122 | 123 | const pamDiff = new PamDiff({ regions: regions }); 124 | 125 | let diffCount = 0; 126 | 127 | pamDiff.on('diff', data => { 128 | // console.log(data); 129 | 130 | diffCount++; 131 | 132 | // comment out the following line if you want to use ffmpeg to create a jpeg from the pam image that triggered an image difference event 133 | if (true) { 134 | return; 135 | } 136 | 137 | const date = new Date(); 138 | let name = `${date.getUTCFullYear()}-${date.getUTCMonth() + 1}-${date.getUTCDate()}_${date.getHours()}-${date.getUTCMinutes()}-${date.getUTCSeconds()}-${date.getUTCMilliseconds()}`; 139 | for (const region of data.trigger) { 140 | name += `(${region.name}=${region.percent})`; 141 | } 142 | const jpeg = `${name}.jpeg`; 143 | const ff = execFile('ffmpeg', ['-f', 'pam_pipe', '-c:v', 'pam', '-i', 'pipe:0', '-c:v', 'mjpeg', '-pix_fmt', 'yuvj422p', '-q:v', '1', '-huffman', 'optimal', jpeg]); 144 | ff.stdin.end(data.pam); 145 | ff.on('exit', data => { 146 | if (data === 0) { 147 | console.log(`FFMPEG clean exit after creating ${jpeg}`); 148 | } else { 149 | throw new Error('FFMPEG is not working with current parameters'); 150 | } 151 | }); 152 | }); 153 | 154 | ffmpeg.stdout.pipe(p2p).pipe(pamDiff); 155 | 156 | let tempRegions; 157 | 158 | setTimeout(() => { 159 | console.log('reset cache while running -----------------------------------------------------------\n'); 160 | pamDiff.resetCache(); 161 | }, 10000); 162 | 163 | setTimeout(() => { 164 | console.log('delete regions while running -----------------------------------------------------------\n'); 165 | tempRegions = pamDiff.regions; 166 | pamDiff.regions = null; 167 | }, 20000); 168 | 169 | setTimeout(() => { 170 | console.log('add regions while running -----------------------------------------------------------\n'); 171 | pamDiff.regions = tempRegions; 172 | }, 30000); 173 | -------------------------------------------------------------------------------- /examples/example7.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const dotenv = require('dotenv'); 4 | 5 | dotenv.config(); 6 | 7 | const argv = require('minimist')(process.argv.slice(2)); 8 | 9 | const async = argv.async || process.env.ASYNC || false; 10 | 11 | const response = argv.response || process.env.RESPONSE || 'percent'; 12 | 13 | const { cpus } = require('os'); 14 | 15 | console.log(`cpu cores available: ${cpus().length}`); 16 | 17 | const P2P = require('pipe2pam'); 18 | const PamDiff = require('../index'); 19 | const ffmpegPath = require('../lib/ffmpeg'); 20 | const ChildProcess = require('child_process'); 21 | const spawn = ChildProcess.spawn; 22 | const execFile = ChildProcess.execFile; 23 | 24 | const params = [ 25 | '-loglevel', 26 | 'quiet', 27 | 28 | /* use hardware acceleration */ 29 | '-hwaccel', 30 | 'auto', // vda, videotoolbox, none, auto 31 | 32 | /* use a pre-recorded mp4 video as input */ 33 | // '-re',//comment out to have ffmpeg read video as fast as possible 34 | '-i', 35 | `${__dirname}/in/circle_star.mp4`, 36 | 37 | /* set output flags */ 38 | '-an', 39 | '-c:v', 40 | 'pam', 41 | '-pix_fmt', 42 | 'gray', 43 | // 'rgba', 44 | // 'rgb24', 45 | // 'monob', 46 | '-f', 47 | 'image2pipe', 48 | '-vf', 49 | // 'fps=2,scale=1280:720', 50 | // 'fps=2,scale=1500:800', 51 | 'fps=2,scale=1920:1080', // 1920:1080 scaled down = 640:360, 400:225, 384:216, 368:207, 352:198, 336:189, 320:180 52 | // 'fps=1,scale=iw*1/6:ih*1/6', 53 | // '-frames', 54 | // '10000', 55 | 'pipe:1', 56 | ]; 57 | 58 | const ffmpeg = spawn(ffmpegPath, params, { 59 | stdio: ['ignore', 'pipe', 'ignore'], 60 | }); 61 | 62 | console.log(ffmpeg.spawnargs.join(' ')); 63 | 64 | ffmpeg.on('error', error => { 65 | console.log(error); 66 | }); 67 | 68 | ffmpeg.on('exit', (code, signal) => { 69 | console.log('exit', code, signal); 70 | }); 71 | 72 | const p2p = new P2P(); 73 | 74 | let counter = 0; 75 | 76 | p2p.on('pam', data => { 77 | // you do not have to listen to this event if you are just piping this data to pam-diff 78 | console.log(`received pam ${++counter}`); 79 | }); 80 | 81 | const region1 = { 82 | name: 'region1', 83 | difference: 1, 84 | percent: 1, 85 | polygon: [ 86 | { x: 0, y: 0 }, 87 | { x: 0, y: 1080 }, 88 | { x: 480, y: 1080 }, 89 | { x: 480, y: 0 }, 90 | ], 91 | }; 92 | 93 | const region2 = { 94 | name: 'region2', 95 | difference: 1, 96 | percent: 1, 97 | polygon: [ 98 | { x: 480, y: 0 }, 99 | { x: 480, y: 1080 }, 100 | { x: 960, y: 1080 }, 101 | { x: 960, y: 0 }, 102 | ], 103 | }; 104 | 105 | const region3 = { 106 | name: 'region3', 107 | difference: 1, 108 | percent: 1, 109 | polygon: [ 110 | { x: 960, y: 0 }, 111 | { x: 960, y: 1080 }, 112 | { x: 1440, y: 1080 }, 113 | { x: 1440, y: 0 }, 114 | ], 115 | }; 116 | 117 | const region4 = { 118 | name: 'region4', 119 | difference: 1, 120 | percent: 1, 121 | polygon: [ 122 | { x: 1440, y: 0 }, 123 | { x: 1440, y: 1080 }, 124 | { x: 1920, y: 1080 }, 125 | { x: 1920, y: 0 }, 126 | ], 127 | }; 128 | 129 | const regions = [region1, region2, region3, region4]; 130 | 131 | const pamDiff = new PamDiff({ regions: regions, response: response, async: async }); 132 | 133 | pamDiff.on('diff', data => { 134 | console.log(data); 135 | 136 | // comment out the following line if you want to use ffmpeg to create a jpeg from the pam image that triggered an image difference event 137 | // if(true){return;} 138 | 139 | const date = new Date(); 140 | // let name = `${date.getUTCFullYear()}-${date.getUTCMonth() + 1}-${date.getUTCDate()}_${date.getHours()}-${date.getUTCMinutes()}-${date.getUTCSeconds()}-${date.getUTCMilliseconds()}`; 141 | let name = counter; 142 | for (const region of data.trigger) { 143 | if (response === 'bounds') { 144 | name += `--${region.name}-percent${region.percent}-minX${region.minX}-maxX${region.maxX}-minY${region.minY}-maxY${region.maxY}`; 145 | } else { 146 | name += `--${region.name}-percent${region.percent}`; 147 | } 148 | } 149 | const jpeg = `${__dirname}/out/${name}.jpeg`; 150 | const ff = execFile(ffmpegPath, ['-f', 'pam_pipe', '-c:v', 'pam', '-i', 'pipe:0', '-c:v', 'mjpeg', '-pix_fmt', 'yuvj422p', '-q:v', '1', '-huffman', 'optimal', jpeg]); 151 | ff.stdin.end(data.pam); 152 | ff.on('exit', data => { 153 | if (data === 0) { 154 | console.log(`FFMPEG clean exit after creating ${jpeg}`); 155 | } else { 156 | throw new Error('FFMPEG is not working with current parameters'); 157 | } 158 | }); 159 | }); 160 | 161 | ffmpeg.stdout.pipe(p2p).pipe(pamDiff); 162 | -------------------------------------------------------------------------------- /examples/in/circle.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinGodell/pam-diff/002042918ec00ffc12170f310eb505ff35490a8d/examples/in/circle.mp4 -------------------------------------------------------------------------------- /examples/in/circle_star.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinGodell/pam-diff/002042918ec00ffc12170f310eb505ff35490a8d/examples/in/circle_star.mp4 -------------------------------------------------------------------------------- /examples/in/star.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinGodell/pam-diff/002042918ec00ffc12170f310eb505ff35490a8d/examples/in/star.mp4 -------------------------------------------------------------------------------- /examples/out/draw/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /examples/out/pam/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Transform } = require('node:stream'); 4 | 5 | const { performance } = require('node:perf_hooks'); 6 | 7 | const PP = require('polygon-points'); 8 | 9 | const PC = require('pixel-change'); 10 | 11 | class PamDiff extends Transform { 12 | /** 13 | * 14 | * @param [options] {Object} 15 | * @param [options.difference=5] {Number} - Pixel difference value, int 1 to 255 16 | * @param [options.percent=5] {Number} - Percent of pixels or blobs that exceed difference value, float 0.0 to 100.0 17 | * @param [options.response=percent] {String} - Accepted values: percent or bounds or blobs 18 | * @param [options.regions] {Array} - Array of region objects 19 | * @param options.regions[i].name {String} - Name of region 20 | * @param [options.regions[i].difference=options.difference] {Number} - Difference value for region, int 1 to 255 21 | * @param [options.regions[i].percent=options.percent] {Number} - Percent value for region, float 0.0 to 100.0 22 | * @param options.regions[i].polygon {Array} - Array of x y coordinates [{x:0,y:0},{x:0,y:360},{x:160,y:360},{x:160,y:0}] 23 | * @param [options.mask=false] {Boolean} - Indicate if regions should be used as masks of pixels to ignore 24 | * @param [options.draw=false] {Boolean} - If true and response is 'bounds' or 'blobs', return a pixel buffer with drawn bounding box 25 | * @param [options.debug=false] {Boolean} - If true, debug object will be attached to output 26 | * @param [callback] {Function} - Function to be called when diff event occurs. Deprecated 27 | */ 28 | constructor(options, callback) { 29 | super({ objectMode: true }); 30 | this.config = options; // configuration for pixel change detection 31 | this.callback = callback; // callback function to be called when pixel change is detected 32 | this._parseChunk = this._parseFirstChunk; // first parsing will be used to configure pixel diff engine 33 | } 34 | 35 | /** 36 | * 37 | * @param obj {Object} 38 | */ 39 | set config(obj) { 40 | obj = PamDiff._validateObject(obj); 41 | this._difference = PamDiff._validateInt(obj.difference, 5, 1, 255); 42 | this._percent = PamDiff._validateFloat(obj.percent, 5, 0, 100); 43 | this._response = PamDiff._validateString(obj.response, ['percent', 'bounds', 'blobs']); 44 | this._regions = PamDiff._validateArray(obj.regions); 45 | this._mask = PamDiff._validateBoolean(obj.mask); 46 | this._draw = PamDiff._validateBoolean(obj.draw); 47 | this._debug = PamDiff._validateBoolean(obj.debug); 48 | this._configurePixelDiffEngine(); 49 | } 50 | 51 | /** 52 | * 53 | * @returns {Object} 54 | */ 55 | get config() { 56 | return { 57 | difference: this._difference, 58 | percent: this._percent, 59 | response: this._response, 60 | regions: this._regions, 61 | mask: this._mask, 62 | draw: this._draw, 63 | debug: this._debug, 64 | }; 65 | } 66 | 67 | /** 68 | * 69 | * @param num {Number} 70 | */ 71 | set difference(num) { 72 | this._difference = PamDiff._validateInt(num, 5, 1, 255); 73 | this._configurePixelDiffEngine(); 74 | } 75 | 76 | /** 77 | * 78 | * @return {Number} 79 | */ 80 | get difference() { 81 | return this._difference; 82 | } 83 | 84 | /** 85 | * 86 | * @param num {Number} 87 | * @return {PamDiff} 88 | * @deprecated 89 | */ 90 | setDifference(num) { 91 | this.difference = num; 92 | return this; 93 | } 94 | 95 | /** 96 | * 97 | * @param num {Number|String} 98 | */ 99 | set percent(num) { 100 | this._percent = PamDiff._validateFloat(num, 5, 0, 100); 101 | this._configurePixelDiffEngine(); 102 | } 103 | 104 | /** 105 | * 106 | * @return {Number} 107 | */ 108 | get percent() { 109 | return this._percent; 110 | } 111 | 112 | /** 113 | * 114 | * @param num {Number} 115 | * @return {PamDiff} 116 | * @deprecated 117 | */ 118 | setPercent(num) { 119 | this.percent = num; 120 | return this; 121 | } 122 | 123 | /** 124 | * 125 | * @param str {String} 126 | */ 127 | set response(str) { 128 | this._response = PamDiff._validateString(str, ['percent', 'bounds', 'blobs']); 129 | this._configurePixelDiffEngine(); 130 | } 131 | 132 | /** 133 | * 134 | * @return {String} 135 | */ 136 | get response() { 137 | return this._response; 138 | } 139 | 140 | /** 141 | * 142 | * @param str {String} 143 | * @return {PamDiff} 144 | * @deprecated 145 | */ 146 | setResponse(str) { 147 | this.response = str; 148 | return this; 149 | } 150 | 151 | /** 152 | * 153 | * @param arr {Array} 154 | */ 155 | set regions(arr) { 156 | this._regions = PamDiff._validateArray(arr); 157 | this._configurePixelDiffEngine(); 158 | } 159 | 160 | /** 161 | * 162 | * @return {Array} 163 | */ 164 | get regions() { 165 | return this._regions; 166 | } 167 | 168 | /** 169 | * 170 | * @param arr {Object[]} 171 | * @return {PamDiff} 172 | * @deprecated 173 | */ 174 | setRegions(arr) { 175 | this.regions = arr; 176 | return this; 177 | } 178 | 179 | /** 180 | * 181 | * @param bool {Boolean|String|Number} 182 | */ 183 | set mask(bool) { 184 | this._mask = PamDiff._validateBoolean(bool); 185 | this._configurePixelDiffEngine(); 186 | } 187 | 188 | /** 189 | * 190 | * @returns {Boolean} 191 | */ 192 | get mask() { 193 | return this._mask; 194 | } 195 | 196 | /** 197 | * 198 | * @param bool {Boolean} 199 | * @returns {PamDiff} 200 | * @deprecated 201 | */ 202 | setMask(bool) { 203 | this.mask = bool; 204 | return this; 205 | } 206 | 207 | /** 208 | * 209 | * @param bool {Boolean} 210 | */ 211 | set draw(bool) { 212 | this._draw = PamDiff._validateBoolean(bool); 213 | this._configurePixelDiffEngine(); 214 | } 215 | 216 | /** 217 | * 218 | * @return {Boolean} 219 | */ 220 | get draw() { 221 | return this._draw; 222 | } 223 | 224 | /** 225 | * 226 | * @param bool {Boolean} 227 | * @return {PamDiff} 228 | * @deprecated 229 | */ 230 | setDraw(bool) { 231 | this.draw = bool; 232 | return this; 233 | } 234 | 235 | /** 236 | * 237 | * @param bool {Boolean|String|Number} 238 | */ 239 | set debug(bool) { 240 | this._debug = PamDiff._validateBoolean(bool); 241 | this._configurePixelDiffEngine(); 242 | } 243 | 244 | /** 245 | * 246 | * @return {Boolean} 247 | */ 248 | get debug() { 249 | return this._debug; 250 | } 251 | 252 | /** 253 | * 254 | * @param bool {Boolean} 255 | * @return {PamDiff} 256 | * @deprecated 257 | */ 258 | setDebug(bool) { 259 | this.debug = bool; 260 | return this; 261 | } 262 | 263 | /** 264 | * 265 | * @param func {Function} 266 | * @deprecated 267 | */ 268 | set callback(func) { 269 | if (!func) { 270 | this._callback = undefined; 271 | } else if (typeof func === 'function' && func.length === 1) { 272 | this._callback = func; 273 | } else { 274 | throw new Error('Callback must be a function that accepts 1 argument.'); 275 | } 276 | } 277 | 278 | /** 279 | * 280 | * @return {Function} 281 | * @deprecated 282 | */ 283 | get callback() { 284 | return this._callback; 285 | } 286 | 287 | /** 288 | * 289 | * @param func {Function} 290 | * @return {PamDiff} 291 | * @deprecated 292 | */ 293 | setCallback(func) { 294 | this.callback = func; 295 | return this; 296 | } 297 | 298 | /** 299 | * 300 | * @return {PamDiff} 301 | * @deprecated 302 | */ 303 | resetCache() { 304 | return this.reset(); 305 | } 306 | 307 | /** 308 | * 309 | * @return {PamDiff} 310 | */ 311 | reset() { 312 | this.emit('reset'); 313 | this._debugInfo = undefined; 314 | this._engine = undefined; 315 | this._oldPix = undefined; 316 | this._width = undefined; 317 | this._height = undefined; 318 | this._depth = undefined; 319 | this._tupltype = undefined; 320 | this._parseChunk = this._parseFirstChunk; 321 | return this; 322 | } 323 | 324 | /** 325 | * 326 | * @returns {Array|null} 327 | * @private 328 | */ 329 | _processRegions() { 330 | if (this._regions) { 331 | const regions = []; 332 | if (this._mask === true) { 333 | // combine all regions to form a single region of flipped 0's and 1's 334 | let minX = this._width; 335 | let maxX = 0; 336 | let minY = this._height; 337 | let maxY = 0; 338 | const wxh = this._width * this._height; 339 | const maskBitset = Buffer.alloc(wxh, 1); 340 | for (const region of this._regions) { 341 | if (!region.hasOwnProperty('polygon')) { 342 | throw new Error('Region must include a polygon property'); 343 | } 344 | const pp = new PP(region.polygon); 345 | const bitset = pp.getBitset(this._width, this._height); 346 | if (bitset.count === 0) { 347 | throw new Error('Bitset count must be greater than 0.'); 348 | } 349 | const bitsetBuffer = bitset.buffer; 350 | for (let i = 0; i < wxh; ++i) { 351 | if (bitsetBuffer[i] === 1) { 352 | maskBitset[i] = 0; 353 | } 354 | } 355 | } 356 | let maskBitsetCount = 0; 357 | for (let i = 0; i < wxh; ++i) { 358 | if (maskBitset[i] === 1) { 359 | const y = Math.floor(i / this._width); 360 | const x = i % this._width; 361 | minX = Math.min(minX, x); 362 | maxX = Math.max(maxX, x); 363 | minY = Math.min(minY, y); 364 | maxY = Math.max(maxY, y); 365 | maskBitsetCount++; 366 | } 367 | } 368 | if (maskBitsetCount === 0) { 369 | throw new Error('Bitset count must be greater than 0'); 370 | } 371 | regions.push({ 372 | name: 'mask', 373 | bitset: maskBitset, 374 | bitsetCount: maskBitsetCount, 375 | difference: this._difference, 376 | percent: this._percent, 377 | minX: minX, 378 | maxX: maxX, 379 | minY: minY, 380 | maxY: maxY, 381 | }); 382 | } else { 383 | for (const region of this._regions) { 384 | if (!region.hasOwnProperty('name') || !region.hasOwnProperty('polygon')) { 385 | throw new Error('Region must include a name and a polygon property'); 386 | } 387 | const pp = new PP(region.polygon); 388 | const bitset = pp.getBitset(this._width, this._height); 389 | if (bitset.count === 0) { 390 | throw new Error('Bitset count must be greater than 0'); 391 | } 392 | const difference = PamDiff._validateInt(region.difference, this._difference, 1, 255); 393 | const percent = PamDiff._validateFloat(region.percent, this._percent, 0, 100); 394 | regions.push({ 395 | name: region.name, 396 | bitset: bitset.buffer, 397 | bitsetCount: bitset.count, 398 | difference: difference, 399 | percent: percent, 400 | minX: bitset.minX, 401 | maxX: bitset.maxX, 402 | minY: bitset.minY, 403 | maxY: bitset.maxY, 404 | }); 405 | } 406 | } 407 | return regions; 408 | } 409 | return null; 410 | } 411 | 412 | /** 413 | * 414 | * @private 415 | */ 416 | _configurePixelDiffEngine() { 417 | if (!this._tupltype || !this._width || !this._height) { 418 | return; 419 | } 420 | const regions = this._processRegions(); 421 | let name = `${this._tupltype}_${this._width}w_${this._height}h_${this._depth}d`; 422 | const config = { width: this._width, height: this._height, depth: this._depth, response: this._response }; 423 | if (regions) { 424 | if (regions.length === 1) { 425 | if (this._mask === true) { 426 | name += '_mask'; 427 | } else { 428 | name += '_region'; 429 | } 430 | } else { 431 | name += `_regions`; 432 | } 433 | config.regions = regions; 434 | } else { 435 | name += '_all'; 436 | config.difference = this._difference; 437 | config.percent = this._percent; 438 | } 439 | name += `_${this._response}`; 440 | if ((this._response === 'bounds' || this._response === 'blobs') && this._draw) { 441 | config.draw = this._draw; 442 | name += '_draw'; 443 | } 444 | name += '_async'; 445 | const pixelChange = PC(config); 446 | this._engine = pixelChange.compare.bind(pixelChange); 447 | if (this._debug) { 448 | this._parseChunk = this._parsePixelsDebug; 449 | this._debugInfo = { name, count: 0 }; 450 | } else { 451 | this._parseChunk = this._parsePixels; 452 | } 453 | } 454 | 455 | /** 456 | * 457 | * @param chunk {Object} 458 | * @private 459 | */ 460 | _parsePixels(chunk) { 461 | const oldPix = this._oldPix; 462 | const newPix = (this._oldPix = chunk.pixels); 463 | this._engine(oldPix, newPix, (err, data) => { 464 | if (data) { 465 | const { results, pixels } = data; 466 | const diff = { trigger: results, pam: chunk.pam, headers: chunk.headers, pixels: pixels || newPix }; 467 | this.emit('data', diff); 468 | if (results.length) { 469 | this.emit('diff', diff); 470 | if (this._callback) { 471 | this._callback(diff); 472 | } 473 | } 474 | } else { 475 | throw new Error(err); 476 | } 477 | }); 478 | } 479 | 480 | /** 481 | * 482 | * @param chunk {Object} 483 | * @private 484 | */ 485 | _parsePixelsDebug(chunk) { 486 | const oldPix = this._oldPix; 487 | const newPix = (this._oldPix = chunk.pixels); 488 | const count = ++this._debugInfo.count; 489 | const name = this._debugInfo.name; 490 | const start = performance.now(); 491 | this._engine(oldPix, newPix, (err, data) => { 492 | const duration = Math.round((performance.now() - start) * 1000) / 1000; 493 | if (data) { 494 | const { results, pixels } = data; 495 | const diff = { trigger: results, pam: chunk.pam, headers: chunk.headers, pixels: pixels || newPix, debug: { name, count, duration } }; 496 | this.emit('data', diff); 497 | if (results.length) { 498 | this.emit('diff', diff); 499 | if (this._callback) { 500 | this._callback(diff); 501 | } 502 | } 503 | } else { 504 | throw new Error(err); 505 | } 506 | }); 507 | } 508 | 509 | /** 510 | * 511 | * @param chunk {Object} 512 | * @private 513 | */ 514 | _parseFirstChunk(chunk) { 515 | this._width = Number.parseInt(chunk.width); 516 | this._height = Number.parseInt(chunk.height); 517 | this._depth = Number.parseInt(chunk.depth); 518 | this._oldPix = chunk.pixels; 519 | this._tupltype = chunk.tupltype; 520 | this._configurePixelDiffEngine(); 521 | this.emit('initialized', { width: this._width, height: this._height, depth: this._depth, tupltype: this._tupltype }); 522 | } 523 | 524 | /** 525 | * 526 | * @param chunk {Object} 527 | * @param encoding 528 | * @param callback 529 | * @private 530 | */ 531 | _transform(chunk, encoding, callback) { 532 | this._parseChunk(chunk); 533 | callback(); 534 | } 535 | 536 | /** 537 | * 538 | * @param callback 539 | * @private 540 | */ 541 | _flush(callback) { 542 | this.reset(); 543 | callback(); 544 | } 545 | 546 | /** 547 | * 548 | * @param num {Number|String} 549 | * @param def {Number} 550 | * @param min {Number} 551 | * @param max {Number} 552 | * @returns {Number} 553 | * @private 554 | */ 555 | static _validateInt(num, def, min, max) { 556 | num = Number.parseInt(num); 557 | return Number.isNaN(num) ? def : num < min ? min : num > max ? max : num; 558 | } 559 | 560 | /** 561 | * 562 | * @param num {Number|String} 563 | * @param def {Number} 564 | * @param min {Number} 565 | * @param max {Number} 566 | * @returns {Number} 567 | * @private 568 | */ 569 | static _validateFloat(num, def, min, max) { 570 | num = Number.parseFloat(num); 571 | return Number.isNaN(num) ? def : num < min ? min : num > max ? max : num; 572 | } 573 | 574 | /** 575 | * 576 | * @param bool {Boolean|String|Number} 577 | * @return {Boolean} 578 | * @private 579 | */ 580 | static _validateBoolean(bool) { 581 | return bool === true || bool === 'true' || bool === 1 || bool === '1'; 582 | } 583 | 584 | /** 585 | * 586 | * @param str {String} 587 | * @param arr {String[]} 588 | * @returns {String} 589 | * @private 590 | */ 591 | static _validateString(str, arr) { 592 | return arr.includes(str) ? str : arr[0]; 593 | } 594 | 595 | /** 596 | * 597 | * @param arr (Array} 598 | * @returns {Array|null} 599 | * @private 600 | */ 601 | static _validateArray(arr) { 602 | return Array.isArray(arr) && arr.length ? arr : null; 603 | } 604 | 605 | /** 606 | * 607 | * @param obj (Object} 608 | * @returns {Object} 609 | * @private 610 | */ 611 | static _validateObject(obj) { 612 | return obj && typeof obj === 'object' ? obj : {}; 613 | } 614 | } 615 | 616 | /** 617 | * 618 | * @type {PamDiff} 619 | */ 620 | module.exports = PamDiff; 621 | -------------------------------------------------------------------------------- /lib/ffmpeg.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path; 4 | 5 | module.exports = ffmpegPath; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pam-diff", 3 | "version": "1.2.1", 4 | "description": "Measure differences between pixel arrays extracted from pam images", 5 | "main": "index.js", 6 | "scripts": { 7 | "pack": "npm --verbose pack", 8 | "preversion": "npm install && npm test", 9 | "postversion": "npm run doc", 10 | "doc": "./node_modules/.bin/jsdoc index.js -d docs && git commit -m \"update docs\" -- docs", 11 | "lint": "./node_modules/.bin/eslint --fix --ext js,md .", 12 | "examples": "node examples/example && node examples/example2 && node examples/example3 && node examples/example4 && node examples/example5", 13 | "test": "npm run gray && npm run rgb && npm run rgba", 14 | "gray": "npm run gray:all && npm run gray:mask && npm run gray:region && npm run gray:regions", 15 | "gray:all": "npm run gray:all:percent && npm run gray:all:bounds && npm run gray:all:blobs", 16 | "gray:all:percent": "node tests/test_gray --response percent --pool 2", 17 | "gray:all:bounds": "node tests/test_gray --response bounds --pool 2", 18 | "gray:all:blobs": "node tests/test_gray --response blobs --pool 2", 19 | "gray:mask": "npm run gray:mask:percent && npm run gray:mask:bounds && npm run gray:mask:blobs", 20 | "gray:mask:percent": "node tests/test_gray2 --response percent --pool 2", 21 | "gray:mask:bounds": "node tests/test_gray2 --response bounds --pool 2", 22 | "gray:mask:blobs": "node tests/test_gray2 --response blobs --pool 2", 23 | "gray:region": "npm run gray:region:percent && npm run gray:region:bounds && npm run gray:region:blobs", 24 | "gray:region:percent": "node tests/test_gray3 --response percent --pool 2", 25 | "gray:region:bounds": "node tests/test_gray3 --response bounds --pool 2", 26 | "gray:region:blobs": "node tests/test_gray3 --response blobs --pool 2", 27 | "gray:regions": "npm run gray:regions:percent && npm run gray:regions:bounds && npm run gray:regions:blobs", 28 | "gray:regions:percent": "node tests/test_gray4 --response percent --pool 2", 29 | "gray:regions:bounds": "node tests/test_gray4 --response bounds --pool 2", 30 | "gray:regions:blobs": "node tests/test_gray4 --response blobs --pool 2", 31 | "rgb": "npm run rgb:all && npm run rgb:mask && npm run rgb:region && npm run rgb:regions", 32 | "rgb:all": "npm run rgb:all:percent && npm run rgb:all:bounds && npm run rgb:all:blobs", 33 | "rgb:all:percent": "node tests/test_rgb --response percent --pool 2", 34 | "rgb:all:bounds": "node tests/test_rgb --response bounds --pool 2", 35 | "rgb:all:blobs": "node tests/test_rgb --response blobs --pool 2", 36 | "rgb:mask": "npm run rgb:mask:percent && npm run rgb:mask:bounds && npm run rgb:mask:blobs", 37 | "rgb:mask:percent": "node tests/test_rgb2 --response percent --pool 2", 38 | "rgb:mask:bounds": "node tests/test_rgb2 --response bounds --pool 2", 39 | "rgb:mask:blobs": "node tests/test_rgb2 --response blobs --pool 2", 40 | "rgb:region": "npm run rgb:region:percent && npm run rgb:region:bounds && npm run rgb:region:blobs", 41 | "rgb:region:percent": "node tests/test_rgb3 --response percent --pool 2", 42 | "rgb:region:bounds": "node tests/test_rgb3 --response bounds --pool 2", 43 | "rgb:region:blobs": "node tests/test_rgb3 --response blobs --pool 2", 44 | "rgb:regions": "npm run rgb:regions:percent && npm run rgb:regions:bounds && npm run rgb:regions:blobs", 45 | "rgb:regions:percent": "node tests/test_rgb4 --response percent --pool 2", 46 | "rgb:regions:bounds": "node tests/test_rgb4 --response bounds --pool 2", 47 | "rgb:regions:blobs": "node tests/test_rgb4 --response blobs --pool 2", 48 | "rgba": "npm run rgba:all && npm run rgba:mask && npm run rgba:region && npm run rgba:regions", 49 | "rgba:all": "npm run rgba:all:percent && npm run rgba:all:bounds && npm run rgba:all:blobs", 50 | "rgba:all:percent": "node tests/test_rgba --response percent --pool 2", 51 | "rgba:all:bounds": "node tests/test_rgba --response bounds --pool 2", 52 | "rgba:all:blobs": "node tests/test_rgba -response blobs --pool 2", 53 | "rgba:mask": "npm run rgba:mask:percent && npm run rgba:mask:bounds && npm run rgba:mask:blobs", 54 | "rgba:mask:percent": "node tests/test_rgba2 --response percent --pool 2", 55 | "rgba:mask:bounds": "node tests/test_rgba2 --response bounds --pool 2", 56 | "rgba:mask:blobs": "node tests/test_rgba2 --response blobs --pool 2", 57 | "rgba:region": "npm run rgba:region:percent && npm run rgba:region:bounds && npm run rgba:region:blobs", 58 | "rgba:region:percent": "node tests/test_rgba3 --response percent --pool 2", 59 | "rgba:region:bounds": "node tests/test_rgba3 --response bounds --pool 2", 60 | "rgba:region:blobs": "node tests/test_rgba3 --response blobs --pool 2", 61 | "rgba:regions": "npm run rgba:regions:percent && npm run rgba:regions:bounds && npm run rgba:regions:blobs", 62 | "rgba:regions:percent": "node tests/test_rgba4 --response percent --pool 2", 63 | "rgba:regions:bounds": "node tests/test_rgba4 --response bounds --pool 2", 64 | "rgba:regions:blobs": "node tests/test_rgba4 --response blobs --pool 2", 65 | "pam": "npm run pam:gray && npm run pam:rgb && npm run pam:rgba", 66 | "pam:gray": "node --expose-gc examples/createPam --pixfmt gray --response bounds --draw false --target regions", 67 | "pam:rgb": "node --expose-gc examples/createPam --pixfmt rgb24 --response bounds --draw false --target regions", 68 | "pam:rgba": "node --expose-gc examples/createPam --pixfmt rgba --response bounds --draw false --target regions", 69 | "draw": "npm run draw:gray&& npm run draw:rgb && npm run draw:rgba", 70 | "draw:gray": "node --expose-gc examples/createPam --pixfmt gray --response blobs --draw true --target regions", 71 | "draw:rgb": "node --expose-gc examples/createPam --pixfmt rgb24 --response blobs --draw true --target regions", 72 | "draw:rgba": "node --expose-gc examples/createPam --pixfmt rgba --response blobs --draw true --target regions", 73 | "draw:big": "node examples/createPam2", 74 | "draw:big:gc": "node --expose-gc examples/createPam2" 75 | }, 76 | "repository": { 77 | "type": "git", 78 | "url": "git+https://github.com/kevinGodell/pam-diff.git" 79 | }, 80 | "keywords": [ 81 | "ffmpeg", 82 | "pam", 83 | "pixels", 84 | "pixel", 85 | "difference", 86 | "motion", 87 | "detection", 88 | "region" 89 | ], 90 | "author": "Kevin Godell ", 91 | "license": "MIT", 92 | "bugs": { 93 | "url": "https://github.com/kevinGodell/pam-diff/issues" 94 | }, 95 | "homepage": "https://github.com/kevinGodell/pam-diff#readme", 96 | "dependencies": { 97 | "pixel-change": "^1.1.0", 98 | "polygon-points": "^0.6.0" 99 | }, 100 | "devDependencies": { 101 | "@ffmpeg-installer/ffmpeg": "^1.1.0", 102 | "dotenv": "^16.3.1", 103 | "eslint": "^8.46.0", 104 | "eslint-config-prettier": "^9.0.0", 105 | "eslint-plugin-markdown": "^3.0.1", 106 | "eslint-plugin-prettier": "^5.0.0", 107 | "jsdoc": "^4.0.2", 108 | "minimist": "^1.2.8", 109 | "pipe2pam": "^0.7.1", 110 | "prettier": "^3.0.1" 111 | }, 112 | "private": false, 113 | "files": [ 114 | "package.json", 115 | "index.js", 116 | "LICENSE", 117 | "README.md" 118 | ], 119 | "engines": { 120 | "node": ">=14" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 200, 3 | tabWidth: 2, 4 | semi: true, 5 | singleQuote: true, 6 | arrowParens: 'avoid', 7 | }; 8 | -------------------------------------------------------------------------------- /tests/test_gray.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const dotenv = require('dotenv'); 4 | 5 | dotenv.config(); 6 | 7 | const argv = require('minimist')(process.argv.slice(2)); 8 | 9 | function getVal(...vals) { 10 | // returns first defined val or undefined 11 | for (let val of vals) { 12 | if (val === undefined) { 13 | continue; 14 | } 15 | return val; 16 | } 17 | return undefined; 18 | } 19 | 20 | // get vals in order of priority, args || env || default 21 | 22 | const nodeEnv = getVal(argv.nodeEnv, process.env.NODE_ENV, 'production'); // production || development 23 | 24 | const sync = getVal(argv.sync, process.env.SYNC, false); // true || false 25 | 26 | const response = getVal(argv.response, process.env.RESPONSE, 'percent'); // percent || bounds || blobs 27 | 28 | const draw = getVal(argv.draw, process.env.DRAW, false); // true || false 29 | 30 | const pool = getVal(argv.pool, process.env.POOL, 0); // 0 || 2 31 | 32 | const percent = 1; 33 | 34 | const { cpus } = require('node:os'); 35 | 36 | const assert = require('node:assert'); 37 | 38 | const { spawn } = require('node:child_process'); 39 | 40 | const P2P = require('pipe2pam'); 41 | 42 | const PamDiff = require('../index'); 43 | 44 | const ffmpegPath = require('../lib/ffmpeg'); 45 | 46 | console.log(`cpu cores available: ${cpus().length}`); 47 | 48 | console.time('=====> testing gray pam diffs with no region set'); 49 | 50 | const pamCount = 10; 51 | 52 | let pamCounter = 0; 53 | 54 | let pamDiffCounter = 0; 55 | 56 | const pamDiffResults = [14.74, 15.03, 14.27, 14.49, 14.5, 14, 14.99, 14.77, 14]; 57 | 58 | const params = [ 59 | /* log info to console */ 60 | '-loglevel', 61 | 'quiet', 62 | // '-stats', 63 | 64 | /* use an artificial video input */ 65 | // '-re', 66 | '-f', 67 | 'lavfi', 68 | '-i', 69 | 'testsrc=size=1920x1080:rate=15', 70 | 71 | /* set output flags */ 72 | '-an', 73 | '-c:v', 74 | 'pam', 75 | '-pix_fmt', 76 | 'gray', 77 | '-f', 78 | 'image2pipe', 79 | '-vf', 80 | 'fps=1,scale=400:225', 81 | '-frames', 82 | pamCount, 83 | 'pipe:1', 84 | ]; 85 | 86 | const p2p = new P2P({ pool }); 87 | 88 | p2p.on('data', data => { 89 | pamCounter++; 90 | }); 91 | 92 | const pamDiff = new PamDiff({ difference: 1, percent: percent, sync: sync, response: response, draw: draw, debug: nodeEnv === 'development' }); 93 | 94 | p2p.on('initialized', data => { 95 | console.log(data); 96 | }); 97 | 98 | pamDiff.on('diff', data => { 99 | if (data.debug) { 100 | const { name, count, duration } = data.debug; 101 | console.log(`${name}-${count}: ${duration}ms`); 102 | } 103 | // console.log(data); 104 | // console.log(~~(data.trigger[0].percent * 100) / 100, pamDiffResults[pamDiffCounter]); 105 | assert(data.trigger[0].name === 'all', 'trigger name is not correct'); 106 | assert(~~(data.trigger[0].percent * 100) / 100 === pamDiffResults[pamDiffCounter++], 'trigger percent is not correct'); 107 | }); 108 | 109 | const ffmpeg = spawn(ffmpegPath, params, { stdio: ['ignore', 'pipe', 'inherit'] }); 110 | 111 | ffmpeg.on('error', error => { 112 | console.log(error); 113 | }); 114 | 115 | ffmpeg.on('exit', (code, signal) => { 116 | assert(code === 0, `FFMPEG exited with code ${code} and signal ${signal}`); 117 | setTimeout(() => { 118 | assert(pamDiffCounter === pamCount - 1, `did not get ${pamCount - 1} pam diffs`); 119 | console.timeEnd('=====> testing gray pam diffs with no region set'); 120 | }, 1000); 121 | }); 122 | 123 | ffmpeg.stdout.pipe(p2p).pipe(pamDiff); 124 | -------------------------------------------------------------------------------- /tests/test_gray2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const dotenv = require('dotenv'); 4 | 5 | dotenv.config(); 6 | 7 | const argv = require('minimist')(process.argv.slice(2)); 8 | 9 | function getVal(...vals) { 10 | // returns first defined val or undefined 11 | for (let val of vals) { 12 | if (val === undefined) { 13 | continue; 14 | } 15 | return val; 16 | } 17 | return undefined; 18 | } 19 | 20 | // get vals in order of priority, args || env || default 21 | 22 | const nodeEnv = getVal(argv.nodeEnv, process.env.NODE_ENV, 'production'); // production || development 23 | 24 | const sync = getVal(argv.sync, process.env.SYNC, false); // true || false 25 | 26 | const response = getVal(argv.response, process.env.RESPONSE, 'percent'); // percent || bounds || blobs 27 | 28 | const draw = getVal(argv.draw, process.env.DRAW, false); // true || false 29 | 30 | const pool = getVal(argv.pool, process.env.POOL, 0); // 0 || 2 31 | 32 | const { cpus } = require('node:os'); 33 | 34 | const assert = require('node:assert'); 35 | 36 | const { spawn } = require('node:child_process'); 37 | 38 | const P2P = require('pipe2pam'); 39 | 40 | const PamDiff = require('../index'); 41 | 42 | const ffmpegPath = require('../lib/ffmpeg'); 43 | 44 | console.log(`cpu cores available: ${cpus().length}`); 45 | 46 | console.time('=====> testing gray pam diffs with 2 region masks set'); 47 | 48 | const pamCount = 10; 49 | 50 | let pamCounter = 0; 51 | 52 | let pamDiffCounter = 0; 53 | 54 | const pamDiffResults = [15, 16, 14, 15, 15, 14, 16, 15, 14]; 55 | 56 | const params = [ 57 | /* log info to console */ 58 | '-loglevel', 59 | 'quiet', 60 | // '-stats', 61 | 62 | /* use an artificial video input */ 63 | // '-re', 64 | '-f', 65 | 'lavfi', 66 | '-i', 67 | 'testsrc=size=1920x1080:rate=15', 68 | 69 | /* set output flags */ 70 | '-an', 71 | '-c:v', 72 | 'pam', 73 | '-pix_fmt', 74 | 'gray', 75 | '-f', 76 | 'image2pipe', 77 | '-vf', 78 | 'fps=1,scale=400:225', 79 | '-frames', 80 | pamCount, 81 | 'pipe:1', 82 | ]; 83 | 84 | const p2p = new P2P({ pool }); 85 | 86 | p2p.on('data', data => { 87 | pamCounter++; 88 | }); 89 | 90 | const region1 = { 91 | polygon: [ 92 | { x: 0, y: 0 }, 93 | { x: 0, y: 224 }, 94 | { x: 99, y: 224 }, 95 | { x: 99, y: 0 }, 96 | ], 97 | }; 98 | 99 | const region2 = { 100 | polygon: [ 101 | { x: 100, y: 0 }, 102 | { x: 100, y: 224 }, 103 | { x: 199, y: 224 }, 104 | { x: 199, y: 0 }, 105 | ], 106 | }; 107 | 108 | const regions = [region1, region2]; 109 | 110 | const pamDiff = new PamDiff({ 111 | difference: 1, 112 | percent: 1, 113 | mask: true, 114 | regions: regions, 115 | sync: sync, 116 | response: response, 117 | draw: draw, 118 | debug: nodeEnv === 'development', 119 | }); 120 | 121 | pamDiff.on('diff', data => { 122 | if (data.debug) { 123 | const { name, count, duration } = data.debug; 124 | console.log(`${name}-${count}: ${duration}ms`); 125 | } 126 | // console.log(~~(data.trigger[0].percent)); 127 | assert(data.trigger[0].name === 'mask', 'trigger name is not correct'); 128 | assert(~~data.trigger[0].percent === pamDiffResults[pamDiffCounter++], 'trigger percent is not correct'); 129 | }); 130 | 131 | const ffmpeg = spawn(ffmpegPath, params, { stdio: ['ignore', 'pipe', 'inherit'] }); 132 | 133 | ffmpeg.on('error', error => { 134 | console.log(error); 135 | }); 136 | 137 | ffmpeg.on('exit', (code, signal) => { 138 | assert(code === 0, `FFMPEG exited with code ${code} and signal ${signal}`); 139 | setTimeout(() => { 140 | assert(pamDiffCounter === pamCount - 1, `did not get ${pamCount - 1} pam diffs`); 141 | console.timeEnd('=====> testing gray pam diffs with 2 region masks set'); 142 | }, 1000); 143 | }); 144 | 145 | ffmpeg.stdout.pipe(p2p).pipe(pamDiff); 146 | -------------------------------------------------------------------------------- /tests/test_gray3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const dotenv = require('dotenv'); 4 | 5 | dotenv.config(); 6 | 7 | const argv = require('minimist')(process.argv.slice(2)); 8 | 9 | function getVal(...vals) { 10 | // returns first defined val or undefined 11 | for (let val of vals) { 12 | if (val === undefined) { 13 | continue; 14 | } 15 | return val; 16 | } 17 | return undefined; 18 | } 19 | 20 | // get vals in order of priority, args || env || default 21 | 22 | const nodeEnv = getVal(argv.nodeEnv, process.env.NODE_ENV, 'production'); // production || development 23 | 24 | const sync = getVal(argv.sync, process.env.SYNC, false); // true || false 25 | 26 | const response = getVal(argv.response, process.env.RESPONSE, 'percent'); // percent || bounds || blobs 27 | 28 | const draw = getVal(argv.draw, process.env.DRAW, false); // true || false 29 | 30 | const pool = getVal(argv.pool, process.env.POOL, 0); // 0 || 2 31 | 32 | const { cpus } = require('node:os'); 33 | 34 | const assert = require('node:assert'); 35 | 36 | const { spawn } = require('node:child_process'); 37 | 38 | const P2P = require('pipe2pam'); 39 | 40 | const PamDiff = require('../index'); 41 | 42 | const ffmpegPath = require('../lib/ffmpeg'); 43 | 44 | console.log(`cpu cores available: ${cpus().length}`); 45 | 46 | console.time('=====> testing gray pam diffs with a single region set'); 47 | 48 | const pamCount = 10; 49 | 50 | let pamCounter = 0; 51 | 52 | let pamDiffCounter = 0; 53 | 54 | const pamDiffResults = [13, 13, 13, 13, 13, 13, 13, 13, 13]; 55 | 56 | const params = [ 57 | /* log info to console */ 58 | '-loglevel', 59 | 'quiet', 60 | // '-stats', 61 | 62 | /* use an artificial video input */ 63 | // '-re', 64 | '-f', 65 | 'lavfi', 66 | '-i', 67 | 'testsrc=size=1920x1080:rate=15', 68 | 69 | /* set output flags */ 70 | '-an', 71 | '-c:v', 72 | 'pam', 73 | '-pix_fmt', 74 | 'gray', 75 | '-f', 76 | 'image2pipe', 77 | '-vf', 78 | 'fps=1,scale=400:225', 79 | '-frames', 80 | pamCount, 81 | 'pipe:1', 82 | ]; 83 | 84 | const p2p = new P2P({ pool }); 85 | 86 | p2p.on('data', data => { 87 | pamCounter++; 88 | }); 89 | 90 | const region1 = { 91 | name: 'region1', 92 | difference: 1, 93 | percent: 1, 94 | polygon: [ 95 | { x: 0, y: 0 }, 96 | { x: 0, y: 224 }, 97 | { x: 99, y: 224 }, 98 | { x: 99, y: 0 }, 99 | ], 100 | }; 101 | 102 | const regions = [region1]; 103 | 104 | const pamDiff = new PamDiff({ regions: regions, sync: sync, response: response, draw: draw, debug: nodeEnv === 'development' }); 105 | 106 | pamDiff.on('diff', data => { 107 | if (data.debug) { 108 | const { name, count, duration } = data.debug; 109 | console.log(`${name}-${count}: ${duration}ms`); 110 | } 111 | // console.log(~~(data.trigger[0].percent)); 112 | assert(data.trigger[0].name === 'region1', 'trigger name is not correct'); 113 | assert(~~data.trigger[0].percent === pamDiffResults[pamDiffCounter++], 'trigger percent is not correct'); 114 | }); 115 | 116 | const ffmpeg = spawn(ffmpegPath, params, { stdio: ['ignore', 'pipe', 'inherit'] }); 117 | 118 | ffmpeg.on('error', error => { 119 | console.log(error); 120 | }); 121 | 122 | ffmpeg.on('exit', (code, signal) => { 123 | assert(code === 0, `FFMPEG exited with code ${code} and signal ${signal}`); 124 | setTimeout(() => { 125 | assert(pamDiffCounter === pamCount - 1, `did not get ${pamCount - 1} pam diffs`); 126 | console.timeEnd('=====> testing gray pam diffs with a single region set'); 127 | }, 1000); 128 | }); 129 | 130 | ffmpeg.stdout.pipe(p2p).pipe(pamDiff); 131 | -------------------------------------------------------------------------------- /tests/test_gray4.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const dotenv = require('dotenv'); 4 | 5 | dotenv.config(); 6 | 7 | const argv = require('minimist')(process.argv.slice(2)); 8 | 9 | function getVal(...vals) { 10 | // returns first defined val or undefined 11 | for (let val of vals) { 12 | if (val === undefined) { 13 | continue; 14 | } 15 | return val; 16 | } 17 | return undefined; 18 | } 19 | 20 | // get vals in order of priority, args || env || default 21 | 22 | const nodeEnv = getVal(argv.nodeEnv, process.env.NODE_ENV, 'production'); // production || development 23 | 24 | const sync = getVal(argv.sync, process.env.SYNC, false); // true || false 25 | 26 | const response = getVal(argv.response, process.env.RESPONSE, 'percent'); // percent || bounds || blobs 27 | 28 | const draw = getVal(argv.draw, process.env.DRAW, false); // true || false 29 | 30 | const pool = getVal(argv.pool, process.env.POOL, 0); // 0 || 2 31 | 32 | const { cpus } = require('node:os'); 33 | 34 | const assert = require('node:assert'); 35 | 36 | const { spawn } = require('node:child_process'); 37 | 38 | const P2P = require('pipe2pam'); 39 | 40 | const PamDiff = require('../index'); 41 | 42 | const ffmpegPath = require('../lib/ffmpeg'); 43 | 44 | console.log(`cpu cores available: ${cpus().length}`); 45 | 46 | console.time('=====> testing gray pam diffs with 4 regions set'); 47 | 48 | const pamCount = 10; 49 | 50 | let pamCounter = 0; 51 | 52 | let pamDiffCounter = 0; 53 | 54 | const pamDiffResults = [ 55 | 17, 18, 15, 16, 16, 14, 18, 17, 14, 21, 17, 19, 15, 16, 16, 14, 18, 18, 14, 17, 18, 18, 15, 16, 16, 14, 19, 17, 14, 16, 17, 18, 15, 16, 16, 14, 18, 17, 14, 16, 17, 19, 15, 16, 16, 14, 18, 18, 14, 56 | 17, 18, 18, 15, 16, 16, 14, 19, 17, 14, 15, 17, 18, 15, 16, 16, 14, 18, 17, 14, 17, 17, 19, 15, 16, 16, 14, 18, 18, 14, 16, 18, 18, 15, 16, 16, 14, 19, 17, 14, 15, 17, 18, 15, 16, 16, 14, 18, 17, 57 | 14, 16, 17, 19, 15, 16, 16, 14, 18, 18, 14, 16, 18, 18, 15, 16, 16, 14, 19, 17, 14, 18, 17, 18, 15, 16, 16, 14, 18, 17, 14, 16, 17, 19, 15, 16, 16, 14, 18, 18, 14, 16, 18, 18, 15, 16, 16, 14, 19, 58 | 17, 14, 17, 17, 18, 15, 16, 16, 14, 18, 17, 14, 15, 17, 19, 15, 16, 16, 14, 18, 18, 14, 17, 18, 18, 15, 16, 16, 14, 19, 17, 14, 16, 17, 18, 15, 16, 16, 14, 18, 17, 14, 15, 17, 19, 15, 16, 16, 14, 59 | 18, 18, 14, 16, 18, 18, 15, 16, 16, 14, 19, 17, 14, 16, 17, 18, 15, 16, 16, 14, 18, 17, 14, 18, 17, 19, 15, 16, 16, 14, 18, 18, 14, 16, 18, 18, 15, 16, 16, 14, 19, 17, 14, 16, 17, 18, 15, 16, 16, 60 | 14, 18, 17, 14, 17, 17, 19, 15, 16, 16, 14, 18, 18, 14, 15, 18, 18, 15, 16, 16, 14, 19, 17, 14, 17, 17, 18, 15, 16, 16, 14, 18, 17, 14, 16, 17, 19, 15, 16, 16, 14, 18, 18, 14, 15, 18, 18, 15, 16, 61 | 16, 14, 19, 17, 14, 16, 17, 18, 15, 16, 16, 14, 18, 17, 14, 16, 17, 19, 15, 16, 16, 14, 18, 18, 14, 17, 18, 18, 15, 16, 16, 14, 19, 17, 14, 16, 17, 18, 15, 16, 16, 14, 18, 17, 14, 16, 17, 19, 15, 62 | 16, 16, 14, 18, 18, 14, 17, 18, 18, 15, 16, 16, 14, 19, 17, 14, 15, 17, 18, 15, 16, 16, 14, 18, 17, 14, 17, 17, 19, 15, 16, 16, 14, 18, 18, 14, 16, 18, 18, 15, 16, 16, 14, 19, 17, 14, 15, 17, 18, 63 | 15, 16, 16, 14, 18, 17, 14, 16, 17, 19, 15, 16, 16, 14, 18, 18, 14, 16, 18, 18, 15, 16, 16, 14, 19, 17, 14, 18, 17, 18, 15, 16, 16, 14, 18, 17, 14, 16, 17, 19, 15, 16, 16, 14, 18, 18, 14, 16, 18, 64 | 18, 15, 16, 16, 14, 19, 17, 14, 17, 17, 18, 15, 16, 16, 14, 18, 17, 14, 15, 17, 19, 15, 16, 16, 14, 18, 18, 14, 17, 18, 18, 15, 16, 16, 14, 19, 17, 14, 16, 17, 18, 15, 16, 16, 14, 18, 17, 14, 15, 65 | 17, 19, 15, 16, 16, 14, 18, 18, 14, 16, 18, 18, 15, 16, 16, 14, 19, 17, 14, 16, 17, 18, 15, 16, 16, 14, 18, 17, 14, 18, 17, 19, 15, 16, 16, 14, 18, 18, 14, 16, 18, 18, 15, 16, 16, 14, 19, 17, 14, 66 | 16, 17, 18, 15, 16, 16, 14, 18, 17, 14, 17, 17, 19, 15, 16, 16, 14, 18, 18, 14, 15, 18, 18, 15, 16, 16, 14, 19, 17, 14, 17, 17, 18, 15, 16, 16, 14, 18, 17, 14, 16, 17, 19, 15, 16, 16, 14, 18, 18, 67 | 14, 15, 18, 18, 15, 16, 16, 14, 19, 17, 14, 16, 17, 18, 15, 16, 16, 14, 18, 17, 14, 16, 17, 19, 15, 16, 16, 14, 18, 18, 14, 17, 18, 18, 15, 16, 16, 14, 19, 17, 14, 16, 17, 18, 15, 16, 16, 14, 18, 68 | 17, 14, 16, 17, 19, 15, 16, 16, 14, 18, 18, 14, 17, 18, 18, 15, 16, 16, 14, 19, 17, 14, 15, 17, 18, 15, 16, 16, 14, 18, 17, 14, 17, 17, 19, 15, 16, 16, 14, 18, 18, 14, 16, 18, 18, 15, 16, 16, 14, 69 | 19, 17, 14, 15, 17, 18, 15, 16, 16, 14, 18, 17, 14, 16, 17, 19, 15, 16, 16, 14, 18, 18, 14, 16, 18, 18, 15, 16, 16, 14, 19, 17, 14, 18, 17, 18, 15, 16, 16, 14, 18, 17, 14, 16, 17, 19, 15, 16, 16, 70 | 14, 18, 18, 14, 16, 18, 18, 15, 16, 16, 14, 19, 17, 14, 17, 17, 18, 15, 16, 16, 14, 18, 17, 14, 15, 17, 19, 15, 16, 16, 14, 18, 18, 14, 17, 18, 18, 15, 16, 16, 14, 19, 17, 14, 16, 17, 18, 15, 16, 71 | 16, 14, 18, 17, 14, 15, 17, 19, 15, 16, 16, 14, 18, 18, 14, 16, 18, 18, 15, 16, 16, 14, 19, 17, 14, 16, 17, 18, 15, 16, 16, 14, 18, 17, 14, 18, 17, 19, 15, 16, 16, 14, 18, 18, 14, 16, 18, 18, 15, 72 | 16, 16, 14, 19, 17, 14, 16, 17, 18, 15, 16, 16, 14, 18, 17, 14, 17, 17, 19, 15, 16, 16, 14, 18, 18, 14, 15, 18, 18, 15, 16, 16, 14, 19, 17, 14, 17, 17, 18, 15, 16, 16, 14, 18, 17, 14, 16, 17, 19, 73 | 15, 16, 16, 14, 18, 18, 14, 15, 18, 18, 15, 16, 16, 14, 19, 17, 14, 16, 17, 18, 15, 16, 16, 14, 18, 17, 14, 16, 17, 19, 15, 16, 16, 14, 18, 18, 14, 17, 18, 18, 15, 16, 16, 14, 19, 17, 14, 16, 17, 74 | 18, 15, 16, 16, 14, 18, 17, 14, 16, 17, 19, 15, 16, 16, 14, 18, 18, 14, 17, 18, 18, 15, 16, 16, 14, 19, 17, 14, 15, 17, 18, 15, 16, 16, 14, 18, 17, 14, 17, 17, 19, 15, 16, 16, 14, 18, 18, 14, 16, 75 | 18, 18, 15, 16, 16, 14, 19, 17, 14, 15, 17, 18, 15, 16, 16, 14, 18, 17, 14, 76 | ]; 77 | 78 | const params = [ 79 | /* log info to console */ 80 | '-loglevel', 81 | 'quiet', 82 | // '-stats', 83 | 84 | /* use an artificial video input */ 85 | // '-re', 86 | '-f', 87 | 'lavfi', 88 | '-i', 89 | 'testsrc=size=1920x1080:rate=15', 90 | 91 | /* set output flags */ 92 | '-an', 93 | '-c:v', 94 | 'pam', 95 | '-pix_fmt', 96 | 'gray', 97 | '-f', 98 | 'image2pipe', 99 | '-vf', 100 | 'fps=1,scale=400:225', 101 | '-frames', 102 | pamCount, 103 | 'pipe:1', 104 | ]; 105 | 106 | const p2p = new P2P({ pool }); 107 | 108 | p2p.on('data', data => { 109 | pamCounter++; 110 | }); 111 | 112 | const region1 = { 113 | name: 'region1', 114 | difference: 1, 115 | percent: 1, 116 | polygon: [ 117 | { x: 0, y: 0 }, 118 | { x: 0, y: 224 }, 119 | { x: 99, y: 224 }, 120 | { x: 99, y: 0 }, 121 | ], 122 | }; 123 | 124 | const region2 = { 125 | name: 'region2', 126 | difference: 1, 127 | percent: 1, 128 | polygon: [ 129 | { x: 100, y: 0 }, 130 | { x: 100, y: 224 }, 131 | { x: 199, y: 224 }, 132 | { x: 199, y: 0 }, 133 | ], 134 | }; 135 | 136 | const region3 = { 137 | name: 'region3', 138 | difference: 1, 139 | percent: 1, 140 | polygon: [ 141 | { x: 200, y: 0 }, 142 | { x: 200, y: 224 }, 143 | { x: 299, y: 224 }, 144 | { x: 299, y: 0 }, 145 | ], 146 | }; 147 | 148 | const region4 = { 149 | name: 'region4', 150 | difference: 1, 151 | percent: 1, 152 | polygon: [ 153 | { x: 300, y: 0 }, 154 | { x: 300, y: 224 }, 155 | { x: 399, y: 224 }, 156 | { x: 399, y: 0 }, 157 | ], 158 | }; 159 | 160 | const regions = [region1, region2, region3, region4]; 161 | 162 | const pamDiff = new PamDiff({ regions: regions, sync: sync, response: response, draw: draw, debug: nodeEnv === 'development' }); 163 | 164 | pamDiff.on('diff', data => { 165 | if (data.debug) { 166 | const { name, count, duration } = data.debug; 167 | console.log(`${name}-${count}: ${duration}ms`); 168 | } 169 | // console.log(~~(data.trigger[3].percent)); 170 | assert(data.trigger[3].name === 'region4', 'trigger name is not correct'); 171 | assert(~~data.trigger[3].percent === pamDiffResults[pamDiffCounter++], 'trigger percent is not correct'); 172 | }); 173 | 174 | const ffmpeg = spawn(ffmpegPath, params, { stdio: ['ignore', 'pipe', 'inherit'] }); 175 | 176 | ffmpeg.on('error', error => { 177 | console.log(error); 178 | }); 179 | 180 | ffmpeg.on('exit', (code, signal) => { 181 | assert(code === 0, `FFMPEG exited with code ${code} and signal ${signal}`); 182 | setTimeout(() => { 183 | assert(pamDiffCounter === pamCount - 1, `did not get ${pamCount - 1} pam diffs`); 184 | console.timeEnd('=====> testing gray pam diffs with 4 regions set'); 185 | }, 1000); 186 | }); 187 | 188 | ffmpeg.stdout.pipe(p2p).pipe(pamDiff); 189 | -------------------------------------------------------------------------------- /tests/test_rgb.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const dotenv = require('dotenv'); 4 | 5 | dotenv.config(); 6 | 7 | const argv = require('minimist')(process.argv.slice(2)); 8 | 9 | function getVal(...vals) { 10 | // returns first defined val or undefined 11 | for (let val of vals) { 12 | if (val === undefined) { 13 | continue; 14 | } 15 | return val; 16 | } 17 | return undefined; 18 | } 19 | 20 | // get vals in order of priority, args || env || default 21 | 22 | const nodeEnv = getVal(argv.nodeEnv, process.env.NODE_ENV, 'production'); // production || development 23 | 24 | const sync = getVal(argv.sync, process.env.SYNC, false); // true || false 25 | 26 | const response = getVal(argv.response, process.env.RESPONSE, 'percent'); // percent || bounds || blobs 27 | 28 | const draw = getVal(argv.draw, process.env.DRAW, false); // true || false 29 | 30 | const pool = getVal(argv.pool, process.env.POOL, 0); // 0 || 2 31 | 32 | const { cpus } = require('node:os'); 33 | 34 | const assert = require('node:assert'); 35 | 36 | const { spawn } = require('node:child_process'); 37 | 38 | const P2P = require('pipe2pam'); 39 | 40 | const PamDiff = require('../index'); 41 | 42 | const ffmpegPath = require('../lib/ffmpeg'); 43 | 44 | console.log(`cpu cores available: ${cpus().length}`); 45 | 46 | console.time('=====> testing rgb pam diffs with no region set'); 47 | 48 | const pamCount = 10; 49 | 50 | let pamCounter = 0; 51 | 52 | let pamDiffCounter = 0; 53 | 54 | const pamDiffResults = [14, 14, 13, 13, 13, 13, 14, 14, 13]; 55 | 56 | const params = [ 57 | /* log info to console */ 58 | '-loglevel', 59 | 'quiet', 60 | // '-stats', 61 | 62 | /* use an artificial video input */ 63 | // '-re', 64 | '-f', 65 | 'lavfi', 66 | '-i', 67 | 'testsrc=size=1920x1080:rate=15', 68 | 69 | /* set output flags */ 70 | '-an', 71 | '-c:v', 72 | 'pam', 73 | '-pix_fmt', 74 | 'rgb24', 75 | '-f', 76 | 'image2pipe', 77 | '-vf', 78 | 'fps=1,scale=400:225', 79 | '-frames', 80 | pamCount, 81 | 'pipe:1', 82 | ]; 83 | 84 | const p2p = new P2P({ pool }); 85 | 86 | p2p.on('data', data => { 87 | pamCounter++; 88 | }); 89 | 90 | const pamDiff = new PamDiff({ difference: 1, percent: 1, sync: sync, response: response, draw: draw, debug: nodeEnv === 'development' }); 91 | 92 | pamDiff.on('diff', data => { 93 | if (data.debug) { 94 | const { name, count, duration } = data.debug; 95 | console.log(`${name}-${count}: ${duration}ms`); 96 | } 97 | // console.log(~~(data.trigger[0].percent)); 98 | assert(data.trigger[0].name === 'all', 'trigger name is not correct'); 99 | assert(~~data.trigger[0].percent === pamDiffResults[pamDiffCounter++], 'trigger percent is not correct'); 100 | }); 101 | 102 | const ffmpeg = spawn(ffmpegPath, params, { stdio: ['ignore', 'pipe', 'inherit'] }); 103 | 104 | ffmpeg.on('error', error => { 105 | console.log(error); 106 | }); 107 | 108 | ffmpeg.on('exit', (code, signal) => { 109 | assert(code === 0, `FFMPEG exited with code ${code} and signal ${signal}`); 110 | setTimeout(() => { 111 | assert(pamDiffCounter === pamCount - 1, `did not get ${pamCount - 1} pam diffs`); 112 | console.timeEnd('=====> testing rgb pam diffs with no region set'); 113 | }, 1000); 114 | }); 115 | 116 | ffmpeg.stdout.pipe(p2p).pipe(pamDiff); 117 | -------------------------------------------------------------------------------- /tests/test_rgb2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const dotenv = require('dotenv'); 4 | 5 | dotenv.config(); 6 | 7 | const argv = require('minimist')(process.argv.slice(2)); 8 | 9 | function getVal(...vals) { 10 | // returns first defined val or undefined 11 | for (let val of vals) { 12 | if (val === undefined) { 13 | continue; 14 | } 15 | return val; 16 | } 17 | return undefined; 18 | } 19 | 20 | // get vals in order of priority, args || env || default 21 | 22 | const nodeEnv = getVal(argv.nodeEnv, process.env.NODE_ENV, 'production'); // production || development 23 | 24 | const sync = getVal(argv.sync, process.env.SYNC, false); // true || false 25 | 26 | const response = getVal(argv.response, process.env.RESPONSE, 'percent'); // percent || bounds || blobs 27 | 28 | const draw = getVal(argv.draw, process.env.DRAW, false); // true || false 29 | 30 | const pool = getVal(argv.pool, process.env.POOL, 0); // 0 || 2 31 | 32 | const { cpus } = require('node:os'); 33 | 34 | const assert = require('node:assert'); 35 | 36 | const { spawn } = require('node:child_process'); 37 | 38 | const P2P = require('pipe2pam'); 39 | 40 | const PamDiff = require('../index'); 41 | 42 | const ffmpegPath = require('../lib/ffmpeg'); 43 | 44 | console.log(`cpu cores available: ${cpus().length}`); 45 | 46 | console.time('=====> testing rgb pam diffs with 2 region masks set'); 47 | 48 | const pamCount = 10; 49 | 50 | let pamCounter = 0; 51 | 52 | let pamDiffCounter = 0; 53 | 54 | const pamDiffResults = [14, 15, 14, 14, 14, 13, 15, 14, 13]; 55 | 56 | const params = [ 57 | /* log info to console */ 58 | '-loglevel', 59 | 'quiet', 60 | // '-stats', 61 | 62 | /* use an artificial video input */ 63 | // '-re', 64 | '-f', 65 | 'lavfi', 66 | '-i', 67 | 'testsrc=size=1920x1080:rate=15', 68 | 69 | /* set output flags */ 70 | '-an', 71 | '-c:v', 72 | 'pam', 73 | '-pix_fmt', 74 | 'rgb24', 75 | '-f', 76 | 'image2pipe', 77 | '-vf', 78 | 'fps=1,scale=400:225', 79 | '-frames', 80 | pamCount, 81 | 'pipe:1', 82 | ]; 83 | 84 | const p2p = new P2P({ pool }); 85 | 86 | p2p.on('data', data => { 87 | pamCounter++; 88 | }); 89 | 90 | const region1 = { 91 | polygon: [ 92 | { x: 0, y: 0 }, 93 | { x: 0, y: 224 }, 94 | { x: 99, y: 224 }, 95 | { x: 99, y: 0 }, 96 | ], 97 | }; 98 | 99 | const region2 = { 100 | polygon: [ 101 | { x: 100, y: 0 }, 102 | { x: 100, y: 224 }, 103 | { x: 199, y: 224 }, 104 | { x: 199, y: 0 }, 105 | ], 106 | }; 107 | 108 | const regions = [region1, region2]; 109 | 110 | const pamDiff = new PamDiff({ 111 | difference: 1, 112 | percent: 1, 113 | mask: true, 114 | regions: regions, 115 | sync: sync, 116 | response: response, 117 | draw: draw, 118 | debug: nodeEnv === 'development', 119 | }); 120 | 121 | pamDiff.on('diff', data => { 122 | if (data.debug) { 123 | const { name, count, duration } = data.debug; 124 | console.log(`${name}-${count}: ${duration}ms`); 125 | } 126 | // console.log(~~(data.trigger[0].percent)); 127 | assert(data.trigger[0].name === 'mask', 'trigger name is not correct'); 128 | assert(~~data.trigger[0].percent === pamDiffResults[pamDiffCounter++], 'trigger percent is not correct'); 129 | }); 130 | 131 | const ffmpeg = spawn(ffmpegPath, params, { stdio: ['ignore', 'pipe', 'inherit'] }); 132 | 133 | ffmpeg.on('error', error => { 134 | console.log(error); 135 | }); 136 | 137 | ffmpeg.on('exit', (code, signal) => { 138 | assert(code === 0, `FFMPEG exited with code ${code} and signal ${signal}`); 139 | setTimeout(() => { 140 | assert(pamDiffCounter === pamCount - 1, `did not get ${pamCount - 1} pam diffs`); 141 | console.timeEnd('=====> testing rgb pam diffs with 2 region masks set'); 142 | }, 1000); 143 | }); 144 | 145 | ffmpeg.stdout.pipe(p2p).pipe(pamDiff); 146 | -------------------------------------------------------------------------------- /tests/test_rgb3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const dotenv = require('dotenv'); 4 | 5 | dotenv.config(); 6 | 7 | const argv = require('minimist')(process.argv.slice(2)); 8 | 9 | function getVal(...vals) { 10 | // returns first defined val or undefined 11 | for (let val of vals) { 12 | if (val === undefined) { 13 | continue; 14 | } 15 | return val; 16 | } 17 | return undefined; 18 | } 19 | 20 | // get vals in order of priority, args || env || default 21 | 22 | const nodeEnv = getVal(argv.nodeEnv, process.env.NODE_ENV, 'production'); // production || development 23 | 24 | const sync = getVal(argv.sync, process.env.SYNC, false); // true || false 25 | 26 | const response = getVal(argv.response, process.env.RESPONSE, 'percent'); // percent || bounds || blobs 27 | 28 | const draw = getVal(argv.draw, process.env.DRAW, false); // true || false 29 | 30 | const pool = getVal(argv.pool, process.env.POOL, 0); // 0 || 2 31 | 32 | const { cpus } = require('node:os'); 33 | 34 | const assert = require('node:assert'); 35 | 36 | const { spawn } = require('node:child_process'); 37 | 38 | const P2P = require('pipe2pam'); 39 | 40 | const PamDiff = require('../index'); 41 | 42 | const ffmpegPath = require('../lib/ffmpeg'); 43 | 44 | console.log(`cpu cores available: ${cpus().length}`); 45 | 46 | console.time('=====> testing rgb pam diffs with a single region set'); 47 | 48 | const pamCount = 10; 49 | 50 | let pamCounter = 0; 51 | 52 | let pamDiffCounter = 0; 53 | 54 | const pamDiffResults = [13, 13, 13, 13, 13, 13, 13, 13, 13]; 55 | 56 | const params = [ 57 | /* log info to console */ 58 | '-loglevel', 59 | 'quiet', 60 | // '-stats', 61 | 62 | /* use an artificial video input */ 63 | // '-re', 64 | '-f', 65 | 'lavfi', 66 | '-i', 67 | 'testsrc=size=1920x1080:rate=15', 68 | 69 | /* set output flags */ 70 | '-an', 71 | '-c:v', 72 | 'pam', 73 | '-pix_fmt', 74 | 'rgb24', 75 | '-f', 76 | 'image2pipe', 77 | '-vf', 78 | 'fps=1,scale=400:225', 79 | '-frames', 80 | pamCount, 81 | 'pipe:1', 82 | ]; 83 | 84 | const p2p = new P2P({ pool }); 85 | 86 | p2p.on('data', data => { 87 | pamCounter++; 88 | }); 89 | 90 | const region1 = { 91 | name: 'region1', 92 | difference: 1, 93 | percent: 1, 94 | polygon: [ 95 | { x: 0, y: 0 }, 96 | { x: 0, y: 224 }, 97 | { x: 99, y: 224 }, 98 | { x: 99, y: 0 }, 99 | ], 100 | }; 101 | 102 | const regions = [region1]; 103 | 104 | const pamDiff = new PamDiff({ regions: regions, sync: sync, response: response, draw: draw, debug: nodeEnv === 'development' }); 105 | 106 | pamDiff.on('diff', data => { 107 | if (data.debug) { 108 | const { name, count, duration } = data.debug; 109 | console.log(`${name}-${count}: ${duration}ms`); 110 | } 111 | // console.log(~~(data.trigger[0].percent)); 112 | assert(data.trigger[0].name === 'region1', 'trigger name is not correct'); 113 | assert(~~data.trigger[0].percent === pamDiffResults[pamDiffCounter++], 'trigger percent is not correct'); 114 | }); 115 | 116 | const ffmpeg = spawn(ffmpegPath, params, { stdio: ['ignore', 'pipe', 'inherit'] }); 117 | 118 | ffmpeg.on('error', error => { 119 | console.log(error); 120 | }); 121 | 122 | ffmpeg.on('exit', (code, signal) => { 123 | assert(code === 0, `FFMPEG exited with code ${code} and signal ${signal}`); 124 | setTimeout(() => { 125 | assert(pamDiffCounter === pamCount - 1, `did not get ${pamCount - 1} pam diffs`); 126 | console.timeEnd('=====> testing rgb pam diffs with a single region set'); 127 | }, 1000); 128 | }); 129 | 130 | ffmpeg.stdout.pipe(p2p).pipe(pamDiff); 131 | -------------------------------------------------------------------------------- /tests/test_rgb4.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const dotenv = require('dotenv'); 4 | 5 | dotenv.config(); 6 | 7 | const argv = require('minimist')(process.argv.slice(2)); 8 | 9 | function getVal(...vals) { 10 | // returns first defined val or undefined 11 | for (let val of vals) { 12 | if (val === undefined) { 13 | continue; 14 | } 15 | return val; 16 | } 17 | return undefined; 18 | } 19 | 20 | // get vals in order of priority, args || env || default 21 | 22 | const nodeEnv = getVal(argv.nodeEnv, process.env.NODE_ENV, 'production'); // production || development 23 | 24 | const sync = getVal(argv.sync, process.env.SYNC, false); // true || false 25 | 26 | const response = getVal(argv.response, process.env.RESPONSE, 'percent'); // percent || bounds || blobs 27 | 28 | const draw = getVal(argv.draw, process.env.DRAW, false); // true || false 29 | 30 | const pool = getVal(argv.pool, process.env.POOL, 0); // 0 || 2 31 | 32 | const { cpus } = require('node:os'); 33 | 34 | const assert = require('node:assert'); 35 | 36 | const { spawn } = require('node:child_process'); 37 | 38 | const P2P = require('pipe2pam'); 39 | 40 | const PamDiff = require('../index'); 41 | 42 | const ffmpegPath = require('../lib/ffmpeg'); 43 | 44 | console.log(`cpu cores available: ${cpus().length}`); 45 | 46 | console.time('=====> testing rgb pam diffs with 4 regions set'); 47 | 48 | const pamCount = 10; 49 | 50 | let pamCounter = 0; 51 | 52 | let pamDiffCounter = 0; 53 | 54 | const pamDiffResults = [16, 17, 15, 15, 15, 14, 17, 16, 14]; 55 | 56 | const params = [ 57 | /* log info to console */ 58 | '-loglevel', 59 | 'quiet', 60 | // '-stats', 61 | 62 | /* use an artificial video input */ 63 | // '-re', 64 | '-f', 65 | 'lavfi', 66 | '-i', 67 | 'testsrc=size=1920x1080:rate=15', 68 | 69 | /* set output flags */ 70 | '-an', 71 | '-c:v', 72 | 'pam', 73 | '-pix_fmt', 74 | 'rgb24', 75 | '-f', 76 | 'image2pipe', 77 | '-vf', 78 | 'fps=1,scale=400:225', 79 | '-frames', 80 | pamCount, 81 | 'pipe:1', 82 | ]; 83 | 84 | const p2p = new P2P({ pool }); 85 | 86 | p2p.on('data', data => { 87 | pamCounter++; 88 | }); 89 | 90 | const region1 = { 91 | name: 'region1', 92 | difference: 1, 93 | percent: 1, 94 | polygon: [ 95 | { x: 0, y: 0 }, 96 | { x: 0, y: 224 }, 97 | { x: 99, y: 224 }, 98 | { x: 99, y: 0 }, 99 | ], 100 | }; 101 | 102 | const region2 = { 103 | name: 'region2', 104 | difference: 1, 105 | percent: 1, 106 | polygon: [ 107 | { x: 100, y: 0 }, 108 | { x: 100, y: 224 }, 109 | { x: 199, y: 224 }, 110 | { x: 199, y: 0 }, 111 | ], 112 | }; 113 | 114 | const region3 = { 115 | name: 'region3', 116 | difference: 1, 117 | percent: 1, 118 | polygon: [ 119 | { x: 200, y: 0 }, 120 | { x: 200, y: 224 }, 121 | { x: 299, y: 224 }, 122 | { x: 299, y: 0 }, 123 | ], 124 | }; 125 | 126 | const region4 = { 127 | name: 'region4', 128 | difference: 1, 129 | percent: 1, 130 | polygon: [ 131 | { x: 300, y: 0 }, 132 | { x: 300, y: 224 }, 133 | { x: 399, y: 224 }, 134 | { x: 399, y: 0 }, 135 | ], 136 | }; 137 | 138 | const regions = [region1, region2, region3, region4]; 139 | 140 | const pamDiff = new PamDiff({ regions: regions, sync: sync, response: response, draw: draw, debug: nodeEnv === 'development' }); 141 | 142 | pamDiff.on('diff', data => { 143 | if (data.debug) { 144 | const { name, count, duration } = data.debug; 145 | console.log(`${name}-${count}: ${duration}ms`); 146 | } 147 | // console.log(~~(data.trigger[3].percent)); 148 | assert(data.trigger[3].name === 'region4', 'trigger name is not correct'); 149 | assert(~~data.trigger[3].percent === pamDiffResults[pamDiffCounter++], 'trigger percent is not correct'); 150 | }); 151 | 152 | const ffmpeg = spawn(ffmpegPath, params, { stdio: ['ignore', 'pipe', 'inherit'] }); 153 | 154 | ffmpeg.on('error', error => { 155 | console.log(error); 156 | }); 157 | 158 | ffmpeg.on('exit', (code, signal) => { 159 | assert(code === 0, `FFMPEG exited with code ${code} and signal ${signal}`); 160 | setTimeout(() => { 161 | assert(pamDiffCounter === pamCount - 1, `did not get ${pamCount - 1} pam diffs`); 162 | console.timeEnd('=====> testing rgb pam diffs with 4 regions set'); 163 | }, 1000); 164 | }); 165 | 166 | ffmpeg.stdout.pipe(p2p).pipe(pamDiff); 167 | -------------------------------------------------------------------------------- /tests/test_rgba.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const dotenv = require('dotenv'); 4 | 5 | dotenv.config(); 6 | 7 | const argv = require('minimist')(process.argv.slice(2)); 8 | 9 | function getVal(...vals) { 10 | // returns first defined val or undefined 11 | for (let val of vals) { 12 | if (val === undefined) { 13 | continue; 14 | } 15 | return val; 16 | } 17 | return undefined; 18 | } 19 | 20 | // get vals in order of priority, args || env || default 21 | 22 | const nodeEnv = getVal(argv.nodeEnv, process.env.NODE_ENV, 'production'); // production || development 23 | 24 | const sync = getVal(argv.sync, process.env.SYNC, false); // true || false 25 | 26 | const response = getVal(argv.response, process.env.RESPONSE, 'percent'); // percent || bounds || blobs 27 | 28 | const draw = getVal(argv.draw, process.env.DRAW, false); // true || false 29 | 30 | const pool = getVal(argv.pool, process.env.POOL, 0); // 0 || 2 31 | 32 | const { cpus } = require('node:os'); 33 | 34 | const assert = require('node:assert'); 35 | 36 | const { spawn } = require('node:child_process'); 37 | 38 | const P2P = require('pipe2pam'); 39 | 40 | const PamDiff = require('../index'); 41 | 42 | const ffmpegPath = require('../lib/ffmpeg'); 43 | 44 | console.log(`cpu cores available: ${cpus().length}`); 45 | 46 | console.time('=====> testing rgba pam diffs with no region set'); 47 | 48 | const pamCount = 10; 49 | 50 | let pamCounter = 0; 51 | 52 | let pamDiffCounter = 0; 53 | 54 | const pamDiffResults = [14, 14, 13, 13, 13, 13, 14, 14, 13]; 55 | 56 | const params = [ 57 | /* log info to console */ 58 | '-loglevel', 59 | 'quiet', 60 | // '-stats', 61 | 62 | /* use an artificial video input */ 63 | // '-re', 64 | '-f', 65 | 'lavfi', 66 | '-i', 67 | 'testsrc=size=1920x1080:rate=15', 68 | 69 | /* set output flags */ 70 | '-an', 71 | '-c:v', 72 | 'pam', 73 | '-pix_fmt', 74 | 'rgba', 75 | '-f', 76 | 'image2pipe', 77 | '-vf', 78 | 'fps=1,scale=400:225', 79 | '-frames', 80 | pamCount, 81 | 'pipe:1', 82 | ]; 83 | 84 | const p2p = new P2P({ pool }); 85 | 86 | p2p.on('data', data => { 87 | pamCounter++; 88 | }); 89 | 90 | const pamDiff = new PamDiff({ difference: 1, percent: 1, sync: sync, response: response, draw: draw, debug: nodeEnv === 'development' }); 91 | 92 | pamDiff.on('diff', data => { 93 | if (data.debug) { 94 | const { name, count, duration } = data.debug; 95 | console.log(`${name}-${count}: ${duration}ms`); 96 | } 97 | // console.log(~~(data.trigger[0].percent)); 98 | assert(data.trigger[0].name === 'all', 'trigger name is not correct'); 99 | assert(~~data.trigger[0].percent === pamDiffResults[pamDiffCounter++], 'trigger percent is not correct'); 100 | }); 101 | 102 | const ffmpeg = spawn(ffmpegPath, params, { stdio: ['ignore', 'pipe', 'inherit'] }); 103 | 104 | ffmpeg.on('error', error => { 105 | console.log(error); 106 | }); 107 | 108 | ffmpeg.on('exit', (code, signal) => { 109 | assert(code === 0, `FFMPEG exited with code ${code} and signal ${signal}`); 110 | setTimeout(() => { 111 | assert(pamDiffCounter === pamCount - 1, `did not get ${pamCount - 1} pam diffs`); 112 | console.timeEnd('=====> testing rgba pam diffs with no region set'); 113 | }, 1000); 114 | }); 115 | 116 | ffmpeg.stdout.pipe(p2p).pipe(pamDiff); 117 | -------------------------------------------------------------------------------- /tests/test_rgba2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const dotenv = require('dotenv'); 4 | 5 | dotenv.config(); 6 | 7 | const argv = require('minimist')(process.argv.slice(2)); 8 | 9 | function getVal(...vals) { 10 | // returns first defined val or undefined 11 | for (let val of vals) { 12 | if (val === undefined) { 13 | continue; 14 | } 15 | return val; 16 | } 17 | return undefined; 18 | } 19 | 20 | // get vals in order of priority, args || env || default 21 | 22 | const nodeEnv = getVal(argv.nodeEnv, process.env.NODE_ENV, 'production'); // production || development 23 | 24 | const sync = getVal(argv.sync, process.env.SYNC, false); // true || false 25 | 26 | const response = getVal(argv.response, process.env.RESPONSE, 'percent'); // percent || bounds || blobs 27 | 28 | const draw = getVal(argv.draw, process.env.DRAW, false); // true || false 29 | 30 | const pool = getVal(argv.pool, process.env.POOL, 0); // 0 || 2 31 | 32 | const { cpus } = require('node:os'); 33 | 34 | const assert = require('node:assert'); 35 | 36 | const { spawn } = require('node:child_process'); 37 | 38 | const P2P = require('pipe2pam'); 39 | 40 | const PamDiff = require('../index'); 41 | 42 | const ffmpegPath = require('../lib/ffmpeg'); 43 | 44 | console.log(`cpu cores available: ${cpus().length}`); 45 | 46 | console.time('=====> testing rgba pam diffs with 2 region masks set'); 47 | 48 | const pamCount = 10; 49 | 50 | let pamCounter = 0; 51 | 52 | let pamDiffCounter = 0; 53 | 54 | const pamDiffResults = [14, 15, 14, 14, 14, 13, 15, 14, 13]; 55 | 56 | const params = [ 57 | /* log info to console */ 58 | '-loglevel', 59 | 'quiet', 60 | // '-stats', 61 | 62 | /* use an artificial video input */ 63 | // '-re', 64 | '-f', 65 | 'lavfi', 66 | '-i', 67 | 'testsrc=size=1920x1080:rate=15', 68 | 69 | /* set output flags */ 70 | '-an', 71 | '-c:v', 72 | 'pam', 73 | '-pix_fmt', 74 | 'rgba', 75 | '-f', 76 | 'image2pipe', 77 | '-vf', 78 | 'fps=1,scale=400:225', 79 | '-frames', 80 | pamCount, 81 | 'pipe:1', 82 | ]; 83 | 84 | const p2p = new P2P({ pool }); 85 | 86 | p2p.on('data', data => { 87 | pamCounter++; 88 | }); 89 | 90 | const region1 = { 91 | polygon: [ 92 | { x: 0, y: 0 }, 93 | { x: 0, y: 224 }, 94 | { x: 99, y: 224 }, 95 | { x: 99, y: 0 }, 96 | ], 97 | }; 98 | 99 | const region2 = { 100 | polygon: [ 101 | { x: 100, y: 0 }, 102 | { x: 100, y: 224 }, 103 | { x: 199, y: 224 }, 104 | { x: 199, y: 0 }, 105 | ], 106 | }; 107 | 108 | const regions = [region1, region2]; 109 | 110 | const pamDiff = new PamDiff({ 111 | difference: 1, 112 | percent: 1, 113 | mask: true, 114 | regions: regions, 115 | sync: sync, 116 | response: response, 117 | draw: draw, 118 | debug: nodeEnv === 'development', 119 | }); 120 | 121 | pamDiff.on('diff', data => { 122 | if (data.debug) { 123 | const { name, count, duration } = data.debug; 124 | console.log(`${name}-${count}: ${duration}ms`); 125 | } 126 | // console.log(~~(data.trigger[0].percent)); 127 | assert(data.trigger[0].name === 'mask', 'trigger name is not correct'); 128 | assert(~~data.trigger[0].percent === pamDiffResults[pamDiffCounter++], 'trigger percent is not correct'); 129 | }); 130 | 131 | const ffmpeg = spawn(ffmpegPath, params, { stdio: ['ignore', 'pipe', 'inherit'] }); 132 | 133 | ffmpeg.on('error', error => { 134 | console.log(error); 135 | }); 136 | 137 | ffmpeg.on('exit', (code, signal) => { 138 | assert(code === 0, `FFMPEG exited with code ${code} and signal ${signal}`); 139 | setTimeout(() => { 140 | assert(pamDiffCounter === pamCount - 1, `did not get ${pamCount - 1} pam diffs`); 141 | console.timeEnd('=====> testing rgba pam diffs with 2 region masks set'); 142 | }, 1000); 143 | }); 144 | 145 | ffmpeg.stdout.pipe(p2p).pipe(pamDiff); 146 | -------------------------------------------------------------------------------- /tests/test_rgba3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const dotenv = require('dotenv'); 4 | 5 | dotenv.config(); 6 | 7 | const argv = require('minimist')(process.argv.slice(2)); 8 | 9 | function getVal(...vals) { 10 | // returns first defined val or undefined 11 | for (let val of vals) { 12 | if (val === undefined) { 13 | continue; 14 | } 15 | return val; 16 | } 17 | return undefined; 18 | } 19 | 20 | // get vals in order of priority, args || env || default 21 | 22 | const nodeEnv = getVal(argv.nodeEnv, process.env.NODE_ENV, 'production'); // production || development 23 | 24 | const sync = getVal(argv.sync, process.env.SYNC, false); // true || false 25 | 26 | const response = getVal(argv.response, process.env.RESPONSE, 'percent'); // percent || bounds || blobs 27 | 28 | const draw = getVal(argv.draw, process.env.DRAW, false); // true || false 29 | 30 | const pool = getVal(argv.pool, process.env.POOL, 0); // 0 || 2 31 | 32 | const { cpus } = require('node:os'); 33 | 34 | const assert = require('node:assert'); 35 | 36 | const { spawn } = require('node:child_process'); 37 | 38 | const P2P = require('pipe2pam'); 39 | 40 | const PamDiff = require('../index'); 41 | 42 | const ffmpegPath = require('../lib/ffmpeg'); 43 | 44 | console.log(`cpu cores available: ${cpus().length}`); 45 | 46 | console.time('=====> testing rgba pam diffs with a single region set'); 47 | 48 | const pamCount = 10; 49 | 50 | let pamCounter = 0; 51 | 52 | let pamDiffCounter = 0; 53 | 54 | const pamDiffResults = [13, 13, 13, 13, 13, 13, 13, 13, 13]; 55 | 56 | const params = [ 57 | /* log info to console */ 58 | '-loglevel', 59 | 'quiet', 60 | // '-stats', 61 | 62 | /* use an artificial video input */ 63 | // '-re', 64 | '-f', 65 | 'lavfi', 66 | '-i', 67 | 'testsrc=size=1920x1080:rate=15', 68 | 69 | /* set output flags */ 70 | '-an', 71 | '-c:v', 72 | 'pam', 73 | '-pix_fmt', 74 | 'rgba', 75 | '-f', 76 | 'image2pipe', 77 | '-vf', 78 | 'fps=1,scale=400:225', 79 | '-frames', 80 | pamCount, 81 | 'pipe:1', 82 | ]; 83 | 84 | const p2p = new P2P({ pool }); 85 | 86 | p2p.on('data', data => { 87 | pamCounter++; 88 | }); 89 | 90 | const region1 = { 91 | name: 'region1', 92 | difference: 1, 93 | percent: 1, 94 | polygon: [ 95 | { x: 0, y: 0 }, 96 | { x: 0, y: 224 }, 97 | { x: 99, y: 224 }, 98 | { x: 99, y: 0 }, 99 | ], 100 | }; 101 | 102 | const regions = [region1]; 103 | 104 | const pamDiff = new PamDiff({ regions: regions, sync: sync, response: response, draw: draw, debug: nodeEnv === 'development' }); 105 | 106 | pamDiff.on('diff', data => { 107 | if (data.debug) { 108 | const { name, count, duration } = data.debug; 109 | console.log(`${name}-${count}: ${duration}ms`); 110 | } 111 | // console.log(~~(data.trigger[0].percent)); 112 | assert(data.trigger[0].name === 'region1', 'trigger name is not correct'); 113 | assert(~~data.trigger[0].percent === pamDiffResults[pamDiffCounter++], 'trigger percent is not correct'); 114 | }); 115 | 116 | const ffmpeg = spawn(ffmpegPath, params, { stdio: ['ignore', 'pipe', 'inherit'] }); 117 | 118 | ffmpeg.on('error', error => { 119 | console.log(error); 120 | }); 121 | 122 | ffmpeg.on('exit', (code, signal) => { 123 | assert(code === 0, `FFMPEG exited with code ${code} and signal ${signal}`); 124 | setTimeout(() => { 125 | assert(pamDiffCounter === pamCount - 1, `did not get ${pamCount - 1} pam diffs`); 126 | console.timeEnd('=====> testing rgba pam diffs with a single region set'); 127 | }, 1000); 128 | }); 129 | 130 | ffmpeg.stdout.pipe(p2p).pipe(pamDiff); 131 | -------------------------------------------------------------------------------- /tests/test_rgba4.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const dotenv = require('dotenv'); 4 | 5 | dotenv.config(); 6 | 7 | const argv = require('minimist')(process.argv.slice(2)); 8 | 9 | function getVal(...vals) { 10 | // returns first defined val or undefined 11 | for (let val of vals) { 12 | if (val === undefined) { 13 | continue; 14 | } 15 | return val; 16 | } 17 | return undefined; 18 | } 19 | 20 | // get vals in order of priority, args || env || default 21 | 22 | const nodeEnv = getVal(argv.nodeEnv, process.env.NODE_ENV, 'production'); // production || development 23 | 24 | const sync = getVal(argv.sync, process.env.SYNC, false); // true || false 25 | 26 | const response = getVal(argv.response, process.env.RESPONSE, 'percent'); // percent || bounds || blobs 27 | 28 | const draw = getVal(argv.draw, process.env.DRAW, false); // true || false 29 | 30 | const pool = getVal(argv.pool, process.env.POOL, 0); // 0 || 2 31 | 32 | const { cpus } = require('node:os'); 33 | 34 | const assert = require('node:assert'); 35 | 36 | const { spawn } = require('node:child_process'); 37 | 38 | const P2P = require('pipe2pam'); 39 | 40 | const PamDiff = require('../index'); 41 | 42 | const ffmpegPath = require('../lib/ffmpeg'); 43 | 44 | console.log(`cpu cores available: ${cpus().length}`); 45 | 46 | console.time('=====> testing rgba pam diffs with 4 regions set'); 47 | 48 | const pamCount = 10; 49 | 50 | let pamCounter = 0; 51 | 52 | let pamDiffCounter = 0; 53 | 54 | const pamDiffResults = [16, 17, 15, 15, 15, 14, 17, 16, 14]; 55 | 56 | const params = [ 57 | /* log info to console */ 58 | '-loglevel', 59 | 'quiet', 60 | // '-stats', 61 | 62 | /* use an artificial video input */ 63 | // '-re', 64 | '-f', 65 | 'lavfi', 66 | '-i', 67 | 'testsrc=size=1920x1080:rate=15', 68 | 69 | /* set output flags */ 70 | '-an', 71 | '-c:v', 72 | 'pam', 73 | '-pix_fmt', 74 | 'rgba', 75 | '-f', 76 | 'image2pipe', 77 | '-vf', 78 | 'fps=1,scale=400:225', 79 | '-frames', 80 | pamCount, 81 | 'pipe:1', 82 | ]; 83 | 84 | const p2p = new P2P({ pool }); 85 | 86 | p2p.on('data', data => { 87 | pamCounter++; 88 | }); 89 | 90 | const region1 = { 91 | name: 'region1', 92 | difference: 1, 93 | percent: 1, 94 | polygon: [ 95 | { x: 0, y: 0 }, 96 | { x: 0, y: 224 }, 97 | { x: 99, y: 224 }, 98 | { x: 99, y: 0 }, 99 | ], 100 | }; 101 | 102 | const region2 = { 103 | name: 'region2', 104 | difference: 1, 105 | percent: 1, 106 | polygon: [ 107 | { x: 100, y: 0 }, 108 | { x: 100, y: 224 }, 109 | { x: 199, y: 224 }, 110 | { x: 199, y: 0 }, 111 | ], 112 | }; 113 | 114 | const region3 = { 115 | name: 'region3', 116 | difference: 1, 117 | percent: 1, 118 | polygon: [ 119 | { x: 200, y: 0 }, 120 | { x: 200, y: 224 }, 121 | { x: 299, y: 224 }, 122 | { x: 299, y: 0 }, 123 | ], 124 | }; 125 | 126 | const region4 = { 127 | name: 'region4', 128 | difference: 1, 129 | percent: 1, 130 | polygon: [ 131 | { x: 300, y: 0 }, 132 | { x: 300, y: 224 }, 133 | { x: 399, y: 224 }, 134 | { x: 399, y: 0 }, 135 | ], 136 | }; 137 | 138 | const regions = [region1, region2, region3, region4]; 139 | 140 | const pamDiff = new PamDiff({ regions: regions, sync: sync, response: response, draw: draw, debug: nodeEnv === 'development' }); 141 | 142 | pamDiff.on('diff', data => { 143 | if (data.debug) { 144 | const { name, count, duration } = data.debug; 145 | console.log(`${name}-${count}: ${duration}ms`); 146 | } 147 | // console.log(~~(data.trigger[3].percent)); 148 | assert(data.trigger[3].name === 'region4', 'trigger name is not correct'); 149 | assert(~~data.trigger[3].percent === pamDiffResults[pamDiffCounter++], 'trigger percent is not correct'); 150 | }); 151 | 152 | const ffmpeg = spawn(ffmpegPath, params, { stdio: ['ignore', 'pipe', 'inherit'] }); 153 | 154 | ffmpeg.on('error', error => { 155 | console.log(error); 156 | }); 157 | 158 | ffmpeg.on('exit', (code, signal) => { 159 | assert(code === 0, `FFMPEG exited with code ${code} and signal ${signal}`); 160 | setTimeout(() => { 161 | assert(pamDiffCounter === pamCount - 1, `did not get ${pamCount - 1} pam diffs`); 162 | console.timeEnd('=====> testing rgba pam diffs with 4 regions set'); 163 | }, 1000); 164 | }); 165 | 166 | ffmpeg.stdout.pipe(p2p).pipe(pamDiff); 167 | --------------------------------------------------------------------------------