├── public_html ├── avc.wasm ├── stream.js ├── index.html ├── Player.js ├── YUVCanvas.js ├── mp4.js └── Decoder.js ├── package.json ├── app.js ├── LICENSE ├── .gitignore ├── README.md └── lib ├── nodetello_webclient.js └── nodetello.js /public_html/avc.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SovGVD/nodetello/HEAD/public_html/avc.wasm -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodetello", 3 | "version": "0.2.2", 4 | "description": "Ryze Tello Node.JS lib based on PyTello", 5 | "main": "app.js", 6 | "directories": { 7 | "lib": "lib" 8 | }, 9 | "scripts": { 10 | "start": "node app.js", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "keywords": [ 14 | "drone", 15 | "ryze", 16 | "tello", 17 | "node.js", 18 | "video stream", 19 | "h264" 20 | ], 21 | "author": "Gleb Devyatkin", 22 | "license": "MIT", 23 | "dependencies": { 24 | "ws": "^5.2.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //const cluster = require('cluster'); 4 | //const doThreads = require('os').cpus().length; 5 | 6 | 7 | const NodeTello = require('./lib/nodetello.js'); 8 | const NodeTello_webclient = require('./lib/nodetello_webclient.js'); 9 | 10 | //var webclient = new NodeTello_webclient('ffmpeg'); // in-browser video 11 | var webclient = new NodeTello_webclient('mplayer'); // open mplayer window with low latency video 12 | webclient.init(); 13 | 14 | 15 | var drone = new NodeTello(); 16 | drone.save_video_path = "./video/"; 17 | drone.save_photo_path = "./photo/"; 18 | drone.tello_video_output = function (h264) { webclient.h264encoder_in(h264) }; 19 | drone.tello_telemetry_config = { 20 | px: "MVO.PX", 21 | py: "MVO.PY", 22 | pz: "MVO.PZ" 23 | }; 24 | drone.tello_telemetry_output = function (data) { webclient.telemetry_in(data); }; 25 | drone.init(); 26 | 27 | webclient.tello_cmd = function (data) { drone.tello_cmd(data); } 28 | -------------------------------------------------------------------------------- /public_html/stream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Stream = (function stream() { 4 | function constructor(url) { 5 | this.url = url; 6 | } 7 | 8 | constructor.prototype = { 9 | readAll: function(progress, complete) { 10 | var xhr = new XMLHttpRequest(); 11 | var async = true; 12 | xhr.open("GET", this.url, async); 13 | xhr.responseType = "arraybuffer"; 14 | if (progress) { 15 | xhr.onprogress = function (event) { 16 | progress(xhr.response, event.loaded, event.total); 17 | }; 18 | } 19 | xhr.onreadystatechange = function (event) { 20 | if (xhr.readyState === 4) { 21 | complete(xhr.response); 22 | // var byteArray = new Uint8Array(xhr.response); 23 | // var array = Array.prototype.slice.apply(byteArray); 24 | // complete(array); 25 | } 26 | } 27 | xhr.send(null); 28 | } 29 | }; 30 | return constructor; 31 | })(); 32 | 33 | 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Gleb Devyatkin 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | log.log 64 | video.h264 65 | video.mp4 66 | video.base64.raw 67 | tests/ 68 | video/* 69 | test/ 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ryze Tello drone Node.JS library 2 | Based on [PyTello](https://bitbucket.org/PingguSoft/pytello), [TelloLib](https://github.com/Kragrathea/TelloLib) and [the tello Go](https://github.com/SMerrony/tello) 3 | 4 | ## WARNING 5 | 6 | Software in development and provided "as is". Use at your own risk. Safety first! Remove props before test! 7 | 8 | ## How to 9 | ``` 10 | const NodeTello = require('./lib/nodetello.js'); 11 | const NodeTello_webclient = require('./lib/nodetello_webclient.js'); 12 | 13 | // init web client 14 | //var webclient = new NodeTello_webclient('ffmpeg'); // in-browser video 15 | var webclient = new NodeTello_webclient('mplayer'); // open mplayer window with low latency video 16 | 17 | webclient.init(); 18 | 19 | // init drone 20 | var drone = new NodeTello(); 21 | 22 | // set video path 23 | drone.save_video_path = "./video/"; 24 | // set callback for video feed 25 | drone.tello_video_output = function (h264) { webclient.h264encoder_in(h264); }; 26 | // config drone telemetry (drone -> node.js -> browser) 27 | drone.tello_telemetry_config = { 28 | px: "MVO.PX", 29 | py: "MVO.PY", 30 | pz: "MVO.PZ" 31 | }; 32 | // set callback for telemetry feed 33 | drone.tello_telemetry_output = function (data) { webclient.telemetry(data); }; 34 | 35 | // lets go! 36 | drone.init(); 37 | 38 | // browser -> webclient -> tello commands 39 | webclient.tello_cmd = function (data) { drone.tello_cmd(data); } 40 | ``` 41 | 42 | ### Init 43 | Init process is basic at that moment: 44 | - connect to drone 45 | - get some settings/values (version and altitude limit) 46 | - set settings (stick position) 47 | - init video feed 48 | - init log and status receive and parse 49 | - start video transcode (required ffmpeg!) 50 | - start http server and websocket 51 | 52 | ## Web client 53 | Web client must be available on http://127.0.0.1:8080 after `npm start` and show almost fine video from drone with 0.5 seconds lag. 54 | 55 | ## Convert video 56 | Video feeds stored to `./video/TIMESTAMP.h264` and must be redecode, e.g. `ffmpeg -i TIMESTAMP.h264 -crf 20 video.mp4` 57 | 58 | ## Flight 59 | 60 | ### Keyboard 61 | - `Shift` + `Space` - Takeoff 62 | - `Space` - Land 63 | - `ArrowUp` and `ArrowDown` - Pitch (forward/backward) 64 | - `ArrowLeft` and `ArrowRight` - Roll (left/right) 65 | - `W` and `S` - Throttle (Up/Down) 66 | - `A` and `D` - Yaw (Rotate) 67 | 68 | Speed is limited = 0.5 69 | 70 | ### Gamepad/Joystick 71 | WARNING, used SHANWAN PS3/PC Gamepad (Vendor: 2563 Product: 0575) in Linux 72 | 73 | - `Left Stick` - trottle and yaw (MODE2) 74 | - `Right Stick` - pitch and roll (MODE2) 75 | - `X`, `Y`, `A`, `B` - land 76 | - `start` - takeoff 77 | 78 | Speed is limited = 0.5 79 | 80 | ## Flight data and log 81 | 82 | ### Flight data 83 | - altitude 84 | - version 85 | - speed 86 | - fly_time 87 | - wifi 88 | - other... 89 | 90 | ### Log 91 | - 29 - MVO (position) 92 | - 2048 - IMU + ANGLES (pitch, roll, yaw) 93 | - 16 - ULTRASONIC (basicaly duplicate IMU data) 94 | - 112 95 | - 1000 96 | - 1001 97 | - 1002 98 | - 1710 99 | - 1712 100 | - 2064 101 | - 2208 102 | - 10086 103 | - 10085 104 | - 32768 105 | 106 | -------------------------------------------------------------------------------- /lib/nodetello_webclient.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('http'); 4 | const fs = require('fs'); 5 | const { spawn } = require('child_process'); 6 | const WebSocket = require('ws'); 7 | 8 | 9 | var nodetello_webclient = function (videobackend) { 10 | this.videobackend = videobackend; 11 | this.path = './public_html/'; 12 | this.http_port = 8080; 13 | this.ws_video_port = 8081; 14 | this.ws_telemetry_port = 8082; 15 | this.file2mime = { 16 | ".html" : { t:'text/html', e:'utf8' }, 17 | ".js" : { t:'application/javascript', e:'utf8' }, 18 | ".wasm" : { t:'application/wasm', e:'binary' }, 19 | ".ico" : { t:'image/x-icon', e:'binary' }, 20 | ".css" : { t:'text/css', e:'utf8' }, 21 | ".map" : { t:'application/json', e:'utf8' }, 22 | }; 23 | this.headers = { 24 | "png" : Buffer([137, 80, 78, 71, 13, 10, 26, 10]), // png header 25 | "jpg" : Buffer([255, 216, 255]), // jpeg header 26 | "h264_baseline" : Buffer([0,0,0,1]), // h264 NAL unit 27 | }; 28 | this.wss_video = false; 29 | this.wss_telemetry = false; 30 | this._ws_video_client = false; 31 | this._ws_telemetry_client = false; 32 | if (this.videobackend == 'ffmpeg') { 33 | this.h264encoder_spawn = { 34 | "command" : 'ffmpeg', // for mac just change to ./ffmpeg and put ffmpeg binary into the same folder 35 | //"args" : [ '-fflags', 'nobuffer', '-f', 'h264', '-i', "-", '-c:v', 'libx264', '-b:v', '3M', '-profile', 'baseline', '-preset', 'ultrafast', '-pass', '1', '-coder', '0', '-bf', '0', '-flags', '-loop', '-wpredp', '0', '-an', '-f', 'h264', '-'] 36 | //"args" : [ '-f', 'h264', '-i', "-", '-r', '35', '-c:v', 'libx264', '-b:v', '3M', '-profile', 'baseline', '-preset', 'ultrafast', '-tune', 'zerolatency', '-vsync', '0', '-async', '1', '-bsf:v', 'h264_mp4toannexb', '-x264-params','keyint=15:scenecut=0', '-an', '-f', 'h264', '-'] 37 | //"args" : [ '-f', 'h264', '-i', "-", '-r', '30', '-c:v', 'libx264', '-b:v', '3M', '-profile', 'baseline', '-preset', 'ultrafast', '-tune', 'zerolatency', '-vsync', '0', '-async', '1', '-bsf:v', 'h264_mp4toannexb', '-an', '-f', 'h264', '-'] 38 | "args" : [ '-fflags', 'nobuffer', '-f', 'h264', '-i', "-", '-r', '30', '-c:v', 'libx264', '-b:v', '2M', '-profile', 'baseline', '-preset', 'ultrafast', '-tune', 'zerolatency', '-vsync', '0', '-async', '0', '-bsf:v', 'h264_mp4toannexb', '-x264-params','keyint=5:scenecut=0', '-an', '-f', 'h264', '-'] 39 | //"args" : [ '-f', 'h264', '-i', "-", '-r', '30', '-c:v', 'libx264', '-b:v', '3M', '-profile', 'baseline', '-preset', 'ultrafast', '-tune', 'zerolatency', '-async', '1', '-an', '-f', 'h264', '-'] 40 | }; 41 | } else if (this.videobackend == 'mplayer') { 42 | this.h264encoder_spawn = { 43 | "command" : 'mplayer', 44 | "args" : [ '-gui', '-nolirc' ,'-fps', '35', '-really-quiet', '-' ] 45 | }; 46 | } 47 | this.h264encoder = false; 48 | this.h264chunks = []; 49 | this.h264unit = false; 50 | 51 | this.tello_cmd = false; 52 | 53 | this.init = function () { 54 | this.wss_init(); 55 | this.h264encoder_init(); 56 | http.createServer(function (req, res) {this._http_res(req, res)}.bind(this)).listen(this.http_port); 57 | } 58 | 59 | this.h264encoder_init = function () { 60 | this.h264encoder = spawn( this.h264encoder_spawn.command, this.h264encoder_spawn.args); 61 | 62 | this.h264encoder.stderr.on('data', function (data) { 63 | //console.log("ffmpeg error", data.toString()); 64 | }.bind(this)); 65 | 66 | this.h264encoder.stdout.on('data', function (data) { 67 | var idx = data.indexOf(this.headers['h264_baseline']); 68 | if (idx>-1 && this.h264chunks.length>0) { 69 | this.h264chunks.push(data.slice(0,idx)); 70 | if (this._ws_video_client!==false) { 71 | try { 72 | this._ws_video_client.send(Buffer.concat(this.h264chunks).toString('binary')); 73 | } catch (e) { this._ws_video_client = false } 74 | } 75 | this.h264chunks=[]; 76 | this.h264chunks.push(data.slice(idx)); 77 | } else { 78 | this.h264chunks.push(data); 79 | } 80 | }.bind(this)); 81 | } 82 | this.h264encoder_in = function (h264chunk) { 83 | this.h264encoder.stdin.write(h264chunk); 84 | } 85 | 86 | this.telemetry_in = function (data) { 87 | if (this._ws_telemetry_client !== false) { 88 | try { 89 | this._ws_telemetry_client.send(data); 90 | } catch (e) { this._ws_telemetry_client = false; } 91 | } 92 | } 93 | 94 | 95 | this.wss_init = function () { 96 | // Video Feed 97 | this.wss_video = new WebSocket.Server({ port: this.ws_video_port }); 98 | this.wss_video.on('connection', function connection(ws) { 99 | ws.on('message', function (message) { 100 | this._ws_video_client = ws; 101 | try { 102 | // nothing to do 103 | } catch (e) { 104 | ws.send("false"); 105 | console.log("WSVIDEOERROR:",e); 106 | } 107 | }.bind(this)); 108 | }.bind(this)); 109 | 110 | // Telemetry Feed 111 | this.wss_telemetry = new WebSocket.Server({ port: this.ws_telemetry_port }); 112 | this.wss_telemetry.on('connection', function connection(ws) { 113 | ws.on('message', function (message) { 114 | this._ws_telemetry_client = ws; 115 | try { 116 | if (message[0] == '{') { 117 | this.tello_cmd(JSON.parse(message)); 118 | } else { 119 | console.log("WebClient",message); 120 | } 121 | } catch (e) { 122 | ws.send("false"); 123 | console.log("WSTELEMETRYERROR:",e,"Message",message); 124 | } 125 | }.bind(this)); 126 | }.bind(this)); 127 | } 128 | 129 | this._http_res = function (req, res) { 130 | var url = req.url.split("/"); url.shift(); url = (url.join("/")).replace(new RegExp("\\.\\.",'g'),""); 131 | if (url=='') url="index.html"; 132 | url = this.path+url; 133 | var type = url.split("."); 134 | type="."+type.pop(); 135 | console.log("HTTPREQ:", req.url, "as", url, "type", type); 136 | type = (typeof this.file2mime[type] !== undefined)?this.file2mime[type]:'text/html'; 137 | fs.readFile(url, { encoding: type.e }, function (type, res, err, data) { 138 | if (err) { 139 | //console.log("HTTP404:", err); 140 | res.writeHead(404, {'Content-Type': 'text/html'}); 141 | res.end("404 Not Found"); 142 | } else { 143 | console.log("HTTP200:", type); 144 | res.writeHead(200, {'Content-Type': type.t}); 145 | res.end(data, type.e); 146 | } 147 | }.bind(this, type, res)); 148 | } 149 | } 150 | 151 | module.exports = nodetello_webclient; 152 | -------------------------------------------------------------------------------- /public_html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Tello client. NodeTello. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | 41 |
42 |
43 |
44 |
45 |
46 |
47 | 48 | 315 | 316 | 317 | -------------------------------------------------------------------------------- /public_html/Player.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | usage: 5 | 6 | p = new Player({ 7 | useWorker: , 8 | workerFile: // give path to Decoder.js 9 | webgl: true | false | "auto" // defaults to "auto" 10 | }); 11 | 12 | // canvas property represents the canvas node 13 | // put it somewhere in the dom 14 | p.canvas; 15 | 16 | p.webgl; // contains the used rendering mode. if you pass auto to webgl you can see what auto detection resulted in 17 | 18 | p.decode(); 19 | 20 | 21 | */ 22 | 23 | 24 | 25 | // universal module definition 26 | (function (root, factory) { 27 | if (typeof define === 'function' && define.amd) { 28 | // AMD. Register as an anonymous module. 29 | define(["./Decoder", "./YUVCanvas"], factory); 30 | } else if (typeof exports === 'object') { 31 | // Node. Does not work with strict CommonJS, but 32 | // only CommonJS-like environments that support module.exports, 33 | // like Node. 34 | module.exports = factory(require("./Decoder"), require("./YUVCanvas")); 35 | } else { 36 | // Browser globals (root is window) 37 | root.Player = factory(root.Decoder, root.YUVCanvas); 38 | } 39 | }(this, function (Decoder, WebGLCanvas) { 40 | "use strict"; 41 | 42 | 43 | var nowValue = Decoder.nowValue; 44 | 45 | 46 | var Player = function(parOptions){ 47 | var self = this; 48 | this._config = parOptions || {}; 49 | 50 | this.render = true; 51 | if (this._config.render === false){ 52 | this.render = false; 53 | }; 54 | 55 | this.nowValue = nowValue; 56 | 57 | this._config.workerFile = this._config.workerFile || "Decoder.js"; 58 | if (this._config.preserveDrawingBuffer){ 59 | this._config.contextOptions = this._config.contextOptions || {}; 60 | this._config.contextOptions.preserveDrawingBuffer = true; 61 | }; 62 | 63 | var webgl = "auto"; 64 | if (this._config.webgl === true){ 65 | webgl = true; 66 | }else if (this._config.webgl === false){ 67 | webgl = false; 68 | }; 69 | 70 | if (webgl == "auto"){ 71 | webgl = true; 72 | try{ 73 | if (!window.WebGLRenderingContext) { 74 | // the browser doesn't even know what WebGL is 75 | webgl = false; 76 | } else { 77 | var canvas = document.createElement('canvas'); 78 | var ctx = canvas.getContext("webgl"); 79 | if (!ctx) { 80 | // browser supports WebGL but initialization failed. 81 | webgl = false; 82 | }; 83 | }; 84 | }catch(e){ 85 | webgl = false; 86 | }; 87 | }; 88 | 89 | this.webgl = webgl; 90 | 91 | // choose functions 92 | if (this.webgl){ 93 | this.createCanvasObj = this.createCanvasWebGL; 94 | this.renderFrame = this.renderFrameWebGL; 95 | }else{ 96 | this.createCanvasObj = this.createCanvasRGB; 97 | this.renderFrame = this.renderFrameRGB; 98 | }; 99 | 100 | 101 | var lastWidth; 102 | var lastHeight; 103 | var onPictureDecoded = function(buffer, width, height, infos) { 104 | self.onPictureDecoded(buffer, width, height, infos); 105 | 106 | var startTime = nowValue(); 107 | 108 | if (!buffer || !self.render) { 109 | return; 110 | }; 111 | 112 | self.renderFrame({ 113 | canvasObj: self.canvasObj, 114 | data: buffer, 115 | width: width, 116 | height: height 117 | }); 118 | 119 | if (self.onRenderFrameComplete){ 120 | self.onRenderFrameComplete({ 121 | data: buffer, 122 | width: width, 123 | height: height, 124 | infos: infos, 125 | canvasObj: self.canvasObj 126 | }); 127 | }; 128 | 129 | }; 130 | 131 | // provide size 132 | 133 | if (!this._config.size){ 134 | this._config.size = {}; 135 | }; 136 | this._config.size.width = this._config.size.width || 200; 137 | this._config.size.height = this._config.size.height || 200; 138 | 139 | if (this._config.useWorker){ 140 | var worker = new Worker(this._config.workerFile); 141 | this.worker = worker; 142 | worker.addEventListener('message', function(e) { 143 | var data = e.data; 144 | if (data.consoleLog){ 145 | console.log(data.consoleLog); 146 | return; 147 | }; 148 | 149 | onPictureDecoded.call(self, new Uint8Array(data.buf, 0, data.length), data.width, data.height, data.infos); 150 | 151 | }, false); 152 | 153 | worker.postMessage({type: "Broadway.js - Worker init", options: { 154 | rgb: !webgl, 155 | memsize: this.memsize, 156 | reuseMemory: this._config.reuseMemory ? true : false 157 | }}); 158 | 159 | if (this._config.transferMemory){ 160 | this.decode = function(parData, parInfo){ 161 | // no copy 162 | // instead we are transfering the ownership of the buffer 163 | // dangerous!!! 164 | 165 | worker.postMessage({buf: parData.buffer, offset: parData.byteOffset, length: parData.length, info: parInfo}, [parData.buffer]); // Send data to our worker. 166 | }; 167 | 168 | }else{ 169 | this.decode = function(parData, parInfo){ 170 | // Copy the sample so that we only do a structured clone of the 171 | // region of interest 172 | var copyU8 = new Uint8Array(parData.length); 173 | copyU8.set( parData, 0, parData.length ); 174 | worker.postMessage({buf: copyU8.buffer, offset: 0, length: parData.length, info: parInfo}, [copyU8.buffer]); // Send data to our worker. 175 | }; 176 | 177 | }; 178 | 179 | if (this._config.reuseMemory){ 180 | this.recycleMemory = function(parArray){ 181 | //this.beforeRecycle(); 182 | worker.postMessage({reuse: parArray.buffer}, [parArray.buffer]); // Send data to our worker. 183 | //this.afterRecycle(); 184 | }; 185 | } 186 | 187 | }else{ 188 | 189 | this.decoder = new Decoder({ 190 | rgb: !webgl 191 | }); 192 | this.decoder.onPictureDecoded = onPictureDecoded; 193 | 194 | this.decode = function(parData, parInfo){ 195 | self.decoder.decode(parData, parInfo); 196 | }; 197 | 198 | }; 199 | 200 | 201 | 202 | if (this.render){ 203 | this.canvasObj = this.createCanvasObj({ 204 | contextOptions: this._config.contextOptions 205 | }); 206 | this.canvas = this.canvasObj.canvas; 207 | }; 208 | 209 | this.domNode = this.canvas; 210 | 211 | lastWidth = this._config.size.width; 212 | lastHeight = this._config.size.height; 213 | 214 | }; 215 | 216 | Player.prototype = { 217 | 218 | onPictureDecoded: function(buffer, width, height, infos){}, 219 | 220 | // call when memory of decoded frames is not used anymore 221 | recycleMemory: function(buf){ 222 | }, 223 | /*beforeRecycle: function(){}, 224 | afterRecycle: function(){},*/ 225 | 226 | // for both functions options is: 227 | // 228 | // width 229 | // height 230 | // enableScreenshot 231 | // 232 | // returns a object that has a property canvas which is a html5 canvas 233 | createCanvasWebGL: function(options){ 234 | var canvasObj = this._createBasicCanvasObj(options); 235 | canvasObj.contextOptions = options.contextOptions; 236 | return canvasObj; 237 | }, 238 | 239 | createCanvasRGB: function(options){ 240 | var canvasObj = this._createBasicCanvasObj(options); 241 | return canvasObj; 242 | }, 243 | 244 | // part that is the same for webGL and RGB 245 | _createBasicCanvasObj: function(options){ 246 | options = options || {}; 247 | 248 | var obj = {}; 249 | var width = options.width; 250 | if (!width){ 251 | width = this._config.size.width; 252 | }; 253 | var height = options.height; 254 | if (!height){ 255 | height = this._config.size.height; 256 | }; 257 | obj.canvas = document.createElement('canvas'); 258 | obj.canvas.width = width; 259 | obj.canvas.height = height; 260 | obj.canvas.style.backgroundColor = "#0D0E1B"; 261 | 262 | 263 | return obj; 264 | }, 265 | 266 | // options: 267 | // 268 | // canvas 269 | // data 270 | renderFrameWebGL: function(options){ 271 | 272 | var canvasObj = options.canvasObj; 273 | 274 | var width = options.width || canvasObj.canvas.width; 275 | var height = options.height || canvasObj.canvas.height; 276 | 277 | if (canvasObj.canvas.width !== width || canvasObj.canvas.height !== height || !canvasObj.webGLCanvas){ 278 | canvasObj.canvas.width = width; 279 | canvasObj.canvas.height = height; 280 | canvasObj.webGLCanvas = new WebGLCanvas({ 281 | canvas: canvasObj.canvas, 282 | contextOptions: canvasObj.contextOptions, 283 | width: width, 284 | height: height 285 | }); 286 | }; 287 | 288 | var ylen = width * height; 289 | var uvlen = (width / 2) * (height / 2); 290 | 291 | canvasObj.webGLCanvas.drawNextOutputPicture({ 292 | yData: options.data.subarray(0, ylen), 293 | uData: options.data.subarray(ylen, ylen + uvlen), 294 | vData: options.data.subarray(ylen + uvlen, ylen + uvlen + uvlen) 295 | }); 296 | 297 | var self = this; 298 | self.recycleMemory(options.data); 299 | 300 | }, 301 | renderFrameRGB: function(options){ 302 | var canvasObj = options.canvasObj; 303 | 304 | var width = options.width || canvasObj.canvas.width; 305 | var height = options.height || canvasObj.canvas.height; 306 | 307 | if (canvasObj.canvas.width !== width || canvasObj.canvas.height !== height){ 308 | canvasObj.canvas.width = width; 309 | canvasObj.canvas.height = height; 310 | }; 311 | 312 | var ctx = canvasObj.ctx; 313 | var imgData = canvasObj.imgData; 314 | 315 | if (!ctx){ 316 | canvasObj.ctx = canvasObj.canvas.getContext('2d'); 317 | ctx = canvasObj.ctx; 318 | 319 | canvasObj.imgData = ctx.createImageData(width, height); 320 | imgData = canvasObj.imgData; 321 | }; 322 | 323 | imgData.data.set(options.data); 324 | ctx.putImageData(imgData, 0, 0); 325 | var self = this; 326 | self.recycleMemory(options.data); 327 | 328 | } 329 | 330 | }; 331 | 332 | return Player; 333 | 334 | })); 335 | 336 | -------------------------------------------------------------------------------- /public_html/YUVCanvas.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2015 Paperspace Co. All rights reserved. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to 6 | // deal in the Software without restriction, including without limitation the 7 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 8 | // sell copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | // IN THE SOFTWARE. 21 | // 22 | 23 | 24 | // universal module definition 25 | (function (root, factory) { 26 | if (typeof define === 'function' && define.amd) { 27 | // AMD. Register as an anonymous module. 28 | define([], factory); 29 | } else if (typeof exports === 'object') { 30 | // Node. Does not work with strict CommonJS, but 31 | // only CommonJS-like environments that support module.exports, 32 | // like Node. 33 | module.exports = factory(); 34 | } else { 35 | // Browser globals (root is window) 36 | root.YUVCanvas = factory(); 37 | } 38 | }(this, function () { 39 | 40 | 41 | /** 42 | * This class can be used to render output pictures from an H264bsdDecoder to a canvas element. 43 | * If available the content is rendered using WebGL. 44 | */ 45 | function YUVCanvas(parOptions) { 46 | 47 | parOptions = parOptions || {}; 48 | 49 | this.canvasElement = parOptions.canvas || document.createElement("canvas"); 50 | this.contextOptions = parOptions.contextOptions; 51 | 52 | this.type = parOptions.type || "yuv420"; 53 | 54 | this.customYUV444 = parOptions.customYUV444; 55 | 56 | this.conversionType = parOptions.conversionType || "rec601"; 57 | 58 | this.width = parOptions.width || 640; 59 | this.height = parOptions.height || 320; 60 | 61 | this.animationTime = parOptions.animationTime || 0; 62 | 63 | this.canvasElement.width = this.width; 64 | this.canvasElement.height = this.height; 65 | 66 | this.initContextGL(); 67 | 68 | if(this.contextGL) { 69 | this.initProgram(); 70 | this.initBuffers(); 71 | this.initTextures(); 72 | }; 73 | 74 | 75 | /** 76 | * Draw the next output picture using WebGL 77 | */ 78 | if (this.type === "yuv420"){ 79 | this.drawNextOuptutPictureGL = function(par) { 80 | var gl = this.contextGL; 81 | var texturePosBuffer = this.texturePosBuffer; 82 | var uTexturePosBuffer = this.uTexturePosBuffer; 83 | var vTexturePosBuffer = this.vTexturePosBuffer; 84 | 85 | var yTextureRef = this.yTextureRef; 86 | var uTextureRef = this.uTextureRef; 87 | var vTextureRef = this.vTextureRef; 88 | 89 | var yData = par.yData; 90 | var uData = par.uData; 91 | var vData = par.vData; 92 | 93 | var width = this.width; 94 | var height = this.height; 95 | 96 | var yDataPerRow = par.yDataPerRow || width; 97 | var yRowCnt = par.yRowCnt || height; 98 | 99 | var uDataPerRow = par.uDataPerRow || (width / 2); 100 | var uRowCnt = par.uRowCnt || (height / 2); 101 | 102 | var vDataPerRow = par.vDataPerRow || uDataPerRow; 103 | var vRowCnt = par.vRowCnt || uRowCnt; 104 | 105 | gl.viewport(0, 0, width, height); 106 | 107 | var tTop = 0; 108 | var tLeft = 0; 109 | var tBottom = height / yRowCnt; 110 | var tRight = width / yDataPerRow; 111 | var texturePosValues = new Float32Array([tRight, tTop, tLeft, tTop, tRight, tBottom, tLeft, tBottom]); 112 | 113 | gl.bindBuffer(gl.ARRAY_BUFFER, texturePosBuffer); 114 | gl.bufferData(gl.ARRAY_BUFFER, texturePosValues, gl.DYNAMIC_DRAW); 115 | 116 | if (this.customYUV444){ 117 | tBottom = height / uRowCnt; 118 | tRight = width / uDataPerRow; 119 | }else{ 120 | tBottom = (height / 2) / uRowCnt; 121 | tRight = (width / 2) / uDataPerRow; 122 | }; 123 | var uTexturePosValues = new Float32Array([tRight, tTop, tLeft, tTop, tRight, tBottom, tLeft, tBottom]); 124 | 125 | gl.bindBuffer(gl.ARRAY_BUFFER, uTexturePosBuffer); 126 | gl.bufferData(gl.ARRAY_BUFFER, uTexturePosValues, gl.DYNAMIC_DRAW); 127 | 128 | 129 | if (this.customYUV444){ 130 | tBottom = height / vRowCnt; 131 | tRight = width / vDataPerRow; 132 | }else{ 133 | tBottom = (height / 2) / vRowCnt; 134 | tRight = (width / 2) / vDataPerRow; 135 | }; 136 | var vTexturePosValues = new Float32Array([tRight, tTop, tLeft, tTop, tRight, tBottom, tLeft, tBottom]); 137 | 138 | gl.bindBuffer(gl.ARRAY_BUFFER, vTexturePosBuffer); 139 | gl.bufferData(gl.ARRAY_BUFFER, vTexturePosValues, gl.DYNAMIC_DRAW); 140 | 141 | 142 | gl.activeTexture(gl.TEXTURE0); 143 | gl.bindTexture(gl.TEXTURE_2D, yTextureRef); 144 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, yDataPerRow, yRowCnt, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, yData); 145 | 146 | gl.activeTexture(gl.TEXTURE1); 147 | gl.bindTexture(gl.TEXTURE_2D, uTextureRef); 148 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, uDataPerRow, uRowCnt, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, uData); 149 | 150 | gl.activeTexture(gl.TEXTURE2); 151 | gl.bindTexture(gl.TEXTURE_2D, vTextureRef); 152 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, vDataPerRow, vRowCnt, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, vData); 153 | 154 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 155 | }; 156 | 157 | }else if (this.type === "yuv422"){ 158 | this.drawNextOuptutPictureGL = function(par) { 159 | var gl = this.contextGL; 160 | var texturePosBuffer = this.texturePosBuffer; 161 | 162 | var textureRef = this.textureRef; 163 | 164 | var data = par.data; 165 | 166 | var width = this.width; 167 | var height = this.height; 168 | 169 | var dataPerRow = par.dataPerRow || (width * 2); 170 | var rowCnt = par.rowCnt || height; 171 | 172 | gl.viewport(0, 0, width, height); 173 | 174 | var tTop = 0; 175 | var tLeft = 0; 176 | var tBottom = height / rowCnt; 177 | var tRight = width / (dataPerRow / 2); 178 | var texturePosValues = new Float32Array([tRight, tTop, tLeft, tTop, tRight, tBottom, tLeft, tBottom]); 179 | 180 | gl.bindBuffer(gl.ARRAY_BUFFER, texturePosBuffer); 181 | gl.bufferData(gl.ARRAY_BUFFER, texturePosValues, gl.DYNAMIC_DRAW); 182 | 183 | gl.uniform2f(gl.getUniformLocation(this.shaderProgram, 'resolution'), dataPerRow, height); 184 | 185 | gl.activeTexture(gl.TEXTURE0); 186 | gl.bindTexture(gl.TEXTURE_2D, textureRef); 187 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, dataPerRow, rowCnt, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, data); 188 | 189 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 190 | }; 191 | }; 192 | 193 | }; 194 | 195 | /** 196 | * Returns true if the canvas supports WebGL 197 | */ 198 | YUVCanvas.prototype.isWebGL = function() { 199 | return this.contextGL; 200 | }; 201 | 202 | /** 203 | * Create the GL context from the canvas element 204 | */ 205 | YUVCanvas.prototype.initContextGL = function() { 206 | var canvas = this.canvasElement; 207 | var gl = null; 208 | 209 | var validContextNames = ["webgl", "experimental-webgl", "moz-webgl", "webkit-3d"]; 210 | var nameIndex = 0; 211 | 212 | while(!gl && nameIndex < validContextNames.length) { 213 | var contextName = validContextNames[nameIndex]; 214 | 215 | try { 216 | if (this.contextOptions){ 217 | gl = canvas.getContext(contextName, this.contextOptions); 218 | }else{ 219 | gl = canvas.getContext(contextName); 220 | }; 221 | } catch (e) { 222 | gl = null; 223 | } 224 | 225 | if(!gl || typeof gl.getParameter !== "function") { 226 | gl = null; 227 | } 228 | 229 | ++nameIndex; 230 | }; 231 | 232 | this.contextGL = gl; 233 | }; 234 | 235 | /** 236 | * Initialize GL shader program 237 | */ 238 | YUVCanvas.prototype.initProgram = function() { 239 | var gl = this.contextGL; 240 | 241 | // vertex shader is the same for all types 242 | var vertexShaderScript; 243 | var fragmentShaderScript; 244 | 245 | if (this.type === "yuv420"){ 246 | 247 | vertexShaderScript = [ 248 | 'attribute vec4 vertexPos;', 249 | 'attribute vec4 texturePos;', 250 | 'attribute vec4 uTexturePos;', 251 | 'attribute vec4 vTexturePos;', 252 | 'varying vec2 textureCoord;', 253 | 'varying vec2 uTextureCoord;', 254 | 'varying vec2 vTextureCoord;', 255 | 256 | 'void main()', 257 | '{', 258 | ' gl_Position = vertexPos;', 259 | ' textureCoord = texturePos.xy;', 260 | ' uTextureCoord = uTexturePos.xy;', 261 | ' vTextureCoord = vTexturePos.xy;', 262 | '}' 263 | ].join('\n'); 264 | 265 | fragmentShaderScript = [ 266 | 'precision highp float;', 267 | 'varying highp vec2 textureCoord;', 268 | 'varying highp vec2 uTextureCoord;', 269 | 'varying highp vec2 vTextureCoord;', 270 | 'uniform sampler2D ySampler;', 271 | 'uniform sampler2D uSampler;', 272 | 'uniform sampler2D vSampler;', 273 | 'uniform mat4 YUV2RGB;', 274 | 275 | 'void main(void) {', 276 | ' highp float y = texture2D(ySampler, textureCoord).r;', 277 | ' highp float u = texture2D(uSampler, uTextureCoord).r;', 278 | ' highp float v = texture2D(vSampler, vTextureCoord).r;', 279 | ' gl_FragColor = vec4(y, u, v, 1) * YUV2RGB;', 280 | '}' 281 | ].join('\n'); 282 | 283 | }else if (this.type === "yuv422"){ 284 | vertexShaderScript = [ 285 | 'attribute vec4 vertexPos;', 286 | 'attribute vec4 texturePos;', 287 | 'varying vec2 textureCoord;', 288 | 289 | 'void main()', 290 | '{', 291 | ' gl_Position = vertexPos;', 292 | ' textureCoord = texturePos.xy;', 293 | '}' 294 | ].join('\n'); 295 | 296 | fragmentShaderScript = [ 297 | 'precision highp float;', 298 | 'varying highp vec2 textureCoord;', 299 | 'uniform sampler2D sampler;', 300 | 'uniform highp vec2 resolution;', 301 | 'uniform mat4 YUV2RGB;', 302 | 303 | 'void main(void) {', 304 | 305 | ' highp float texPixX = 1.0 / resolution.x;', 306 | ' highp float logPixX = 2.0 / resolution.x;', // half the resolution of the texture 307 | ' highp float logHalfPixX = 4.0 / resolution.x;', // half of the logical resolution so every 4th pixel 308 | ' highp float steps = floor(textureCoord.x / logPixX);', 309 | ' highp float uvSteps = floor(textureCoord.x / logHalfPixX);', 310 | ' highp float y = texture2D(sampler, vec2((logPixX * steps) + texPixX, textureCoord.y)).r;', 311 | ' highp float u = texture2D(sampler, vec2((logHalfPixX * uvSteps), textureCoord.y)).r;', 312 | ' highp float v = texture2D(sampler, vec2((logHalfPixX * uvSteps) + texPixX + texPixX, textureCoord.y)).r;', 313 | 314 | //' highp float y = texture2D(sampler, textureCoord).r;', 315 | //' gl_FragColor = vec4(y, u, v, 1) * YUV2RGB;', 316 | ' gl_FragColor = vec4(y, u, v, 1.0) * YUV2RGB;', 317 | '}' 318 | ].join('\n'); 319 | }; 320 | 321 | var YUV2RGB = []; 322 | 323 | if (this.conversionType == "rec709") { 324 | // ITU-T Rec. 709 325 | YUV2RGB = [ 326 | 1.16438, 0.00000, 1.79274, -0.97295, 327 | 1.16438, -0.21325, -0.53291, 0.30148, 328 | 1.16438, 2.11240, 0.00000, -1.13340, 329 | 0, 0, 0, 1, 330 | ]; 331 | } else { 332 | // assume ITU-T Rec. 601 333 | YUV2RGB = [ 334 | 1.16438, 0.00000, 1.59603, -0.87079, 335 | 1.16438, -0.39176, -0.81297, 0.52959, 336 | 1.16438, 2.01723, 0.00000, -1.08139, 337 | 0, 0, 0, 1 338 | ]; 339 | }; 340 | 341 | var vertexShader = gl.createShader(gl.VERTEX_SHADER); 342 | gl.shaderSource(vertexShader, vertexShaderScript); 343 | gl.compileShader(vertexShader); 344 | if(!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { 345 | console.log('Vertex shader failed to compile: ' + gl.getShaderInfoLog(vertexShader)); 346 | } 347 | 348 | var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); 349 | gl.shaderSource(fragmentShader, fragmentShaderScript); 350 | gl.compileShader(fragmentShader); 351 | if(!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { 352 | console.log('Fragment shader failed to compile: ' + gl.getShaderInfoLog(fragmentShader)); 353 | } 354 | 355 | var program = gl.createProgram(); 356 | gl.attachShader(program, vertexShader); 357 | gl.attachShader(program, fragmentShader); 358 | gl.linkProgram(program); 359 | if(!gl.getProgramParameter(program, gl.LINK_STATUS)) { 360 | console.log('Program failed to compile: ' + gl.getProgramInfoLog(program)); 361 | } 362 | 363 | gl.useProgram(program); 364 | 365 | var YUV2RGBRef = gl.getUniformLocation(program, 'YUV2RGB'); 366 | gl.uniformMatrix4fv(YUV2RGBRef, false, YUV2RGB); 367 | 368 | this.shaderProgram = program; 369 | }; 370 | 371 | /** 372 | * Initialize vertex buffers and attach to shader program 373 | */ 374 | YUVCanvas.prototype.initBuffers = function() { 375 | var gl = this.contextGL; 376 | var program = this.shaderProgram; 377 | 378 | var vertexPosBuffer = gl.createBuffer(); 379 | gl.bindBuffer(gl.ARRAY_BUFFER, vertexPosBuffer); 380 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 1, -1, 1, 1, -1, -1, -1]), gl.STATIC_DRAW); 381 | 382 | var vertexPosRef = gl.getAttribLocation(program, 'vertexPos'); 383 | gl.enableVertexAttribArray(vertexPosRef); 384 | gl.vertexAttribPointer(vertexPosRef, 2, gl.FLOAT, false, 0, 0); 385 | 386 | if (this.animationTime){ 387 | 388 | var animationTime = this.animationTime; 389 | var timePassed = 0; 390 | var stepTime = 15; 391 | 392 | var aniFun = function(){ 393 | 394 | timePassed += stepTime; 395 | var mul = ( 1 * timePassed ) / animationTime; 396 | 397 | if (timePassed >= animationTime){ 398 | mul = 1; 399 | }else{ 400 | setTimeout(aniFun, stepTime); 401 | }; 402 | 403 | var neg = -1 * mul; 404 | var pos = 1 * mul; 405 | 406 | var vertexPosBuffer = gl.createBuffer(); 407 | gl.bindBuffer(gl.ARRAY_BUFFER, vertexPosBuffer); 408 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([pos, pos, neg, pos, pos, neg, neg, neg]), gl.STATIC_DRAW); 409 | 410 | var vertexPosRef = gl.getAttribLocation(program, 'vertexPos'); 411 | gl.enableVertexAttribArray(vertexPosRef); 412 | gl.vertexAttribPointer(vertexPosRef, 2, gl.FLOAT, false, 0, 0); 413 | 414 | try{ 415 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 416 | }catch(e){}; 417 | 418 | }; 419 | aniFun(); 420 | 421 | }; 422 | 423 | 424 | 425 | var texturePosBuffer = gl.createBuffer(); 426 | gl.bindBuffer(gl.ARRAY_BUFFER, texturePosBuffer); 427 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 0, 0, 0, 1, 1, 0, 1]), gl.STATIC_DRAW); 428 | 429 | var texturePosRef = gl.getAttribLocation(program, 'texturePos'); 430 | gl.enableVertexAttribArray(texturePosRef); 431 | gl.vertexAttribPointer(texturePosRef, 2, gl.FLOAT, false, 0, 0); 432 | 433 | this.texturePosBuffer = texturePosBuffer; 434 | 435 | if (this.type === "yuv420"){ 436 | var uTexturePosBuffer = gl.createBuffer(); 437 | gl.bindBuffer(gl.ARRAY_BUFFER, uTexturePosBuffer); 438 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 0, 0, 0, 1, 1, 0, 1]), gl.STATIC_DRAW); 439 | 440 | var uTexturePosRef = gl.getAttribLocation(program, 'uTexturePos'); 441 | gl.enableVertexAttribArray(uTexturePosRef); 442 | gl.vertexAttribPointer(uTexturePosRef, 2, gl.FLOAT, false, 0, 0); 443 | 444 | this.uTexturePosBuffer = uTexturePosBuffer; 445 | 446 | 447 | var vTexturePosBuffer = gl.createBuffer(); 448 | gl.bindBuffer(gl.ARRAY_BUFFER, vTexturePosBuffer); 449 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 0, 0, 0, 1, 1, 0, 1]), gl.STATIC_DRAW); 450 | 451 | var vTexturePosRef = gl.getAttribLocation(program, 'vTexturePos'); 452 | gl.enableVertexAttribArray(vTexturePosRef); 453 | gl.vertexAttribPointer(vTexturePosRef, 2, gl.FLOAT, false, 0, 0); 454 | 455 | this.vTexturePosBuffer = vTexturePosBuffer; 456 | }; 457 | 458 | }; 459 | 460 | /** 461 | * Initialize GL textures and attach to shader program 462 | */ 463 | YUVCanvas.prototype.initTextures = function() { 464 | var gl = this.contextGL; 465 | var program = this.shaderProgram; 466 | 467 | if (this.type === "yuv420"){ 468 | 469 | var yTextureRef = this.initTexture(); 470 | var ySamplerRef = gl.getUniformLocation(program, 'ySampler'); 471 | gl.uniform1i(ySamplerRef, 0); 472 | this.yTextureRef = yTextureRef; 473 | 474 | var uTextureRef = this.initTexture(); 475 | var uSamplerRef = gl.getUniformLocation(program, 'uSampler'); 476 | gl.uniform1i(uSamplerRef, 1); 477 | this.uTextureRef = uTextureRef; 478 | 479 | var vTextureRef = this.initTexture(); 480 | var vSamplerRef = gl.getUniformLocation(program, 'vSampler'); 481 | gl.uniform1i(vSamplerRef, 2); 482 | this.vTextureRef = vTextureRef; 483 | 484 | }else if (this.type === "yuv422"){ 485 | // only one texture for 422 486 | var textureRef = this.initTexture(); 487 | var samplerRef = gl.getUniformLocation(program, 'sampler'); 488 | gl.uniform1i(samplerRef, 0); 489 | this.textureRef = textureRef; 490 | 491 | }; 492 | }; 493 | 494 | /** 495 | * Create and configure a single texture 496 | */ 497 | YUVCanvas.prototype.initTexture = function() { 498 | var gl = this.contextGL; 499 | 500 | var textureRef = gl.createTexture(); 501 | gl.bindTexture(gl.TEXTURE_2D, textureRef); 502 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 503 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 504 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 505 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 506 | gl.bindTexture(gl.TEXTURE_2D, null); 507 | 508 | return textureRef; 509 | }; 510 | 511 | /** 512 | * Draw picture data to the canvas. 513 | * If this object is using WebGL, the data must be an I420 formatted ArrayBuffer, 514 | * Otherwise, data must be an RGBA formatted ArrayBuffer. 515 | */ 516 | YUVCanvas.prototype.drawNextOutputPicture = function(width, height, croppingParams, data) { 517 | var gl = this.contextGL; 518 | 519 | if(gl) { 520 | this.drawNextOuptutPictureGL(width, height, croppingParams, data); 521 | } else { 522 | this.drawNextOuptutPictureRGBA(width, height, croppingParams, data); 523 | } 524 | }; 525 | 526 | 527 | 528 | /** 529 | * Draw next output picture using ARGB data on a 2d canvas. 530 | */ 531 | YUVCanvas.prototype.drawNextOuptutPictureRGBA = function(width, height, croppingParams, data) { 532 | var canvas = this.canvasElement; 533 | 534 | var croppingParams = null; 535 | 536 | var argbData = data; 537 | 538 | var ctx = canvas.getContext('2d'); 539 | var imageData = ctx.getImageData(0, 0, width, height); 540 | imageData.data.set(argbData); 541 | 542 | if(croppingParams === null) { 543 | ctx.putImageData(imageData, 0, 0); 544 | } else { 545 | ctx.putImageData(imageData, -croppingParams.left, -croppingParams.top, 0, 0, croppingParams.width, croppingParams.height); 546 | } 547 | }; 548 | 549 | return YUVCanvas; 550 | 551 | })); 552 | -------------------------------------------------------------------------------- /public_html/mp4.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | function assert(condition, message) { 5 | if (!condition) { 6 | error(message); 7 | } 8 | }; 9 | 10 | 11 | /** 12 | * Represents a 2-dimensional size value. 13 | */ 14 | var Size = (function size() { 15 | function constructor(w, h) { 16 | this.w = w; 17 | this.h = h; 18 | } 19 | constructor.prototype = { 20 | toString: function () { 21 | return "(" + this.w + ", " + this.h + ")"; 22 | }, 23 | getHalfSize: function() { 24 | return new Size(this.w >>> 1, this.h >>> 1); 25 | }, 26 | length: function() { 27 | return this.w * this.h; 28 | } 29 | }; 30 | return constructor; 31 | })(); 32 | 33 | 34 | 35 | 36 | 37 | var Bytestream = (function BytestreamClosure() { 38 | function constructor(arrayBuffer, start, length) { 39 | this.bytes = new Uint8Array(arrayBuffer); 40 | this.start = start || 0; 41 | this.pos = this.start; 42 | this.end = (start + length) || this.bytes.length; 43 | } 44 | constructor.prototype = { 45 | get length() { 46 | return this.end - this.start; 47 | }, 48 | get position() { 49 | return this.pos; 50 | }, 51 | get remaining() { 52 | return this.end - this.pos; 53 | }, 54 | readU8Array: function (length) { 55 | if (this.pos > this.end - length) 56 | return null; 57 | var res = this.bytes.subarray(this.pos, this.pos + length); 58 | this.pos += length; 59 | return res; 60 | }, 61 | readU32Array: function (rows, cols, names) { 62 | cols = cols || 1; 63 | if (this.pos > this.end - (rows * cols) * 4) 64 | return null; 65 | if (cols == 1) { 66 | var array = new Uint32Array(rows); 67 | for (var i = 0; i < rows; i++) { 68 | array[i] = this.readU32(); 69 | } 70 | return array; 71 | } else { 72 | var array = new Array(rows); 73 | for (var i = 0; i < rows; i++) { 74 | var row = null; 75 | if (names) { 76 | row = {}; 77 | for (var j = 0; j < cols; j++) { 78 | row[names[j]] = this.readU32(); 79 | } 80 | } else { 81 | row = new Uint32Array(cols); 82 | for (var j = 0; j < cols; j++) { 83 | row[j] = this.readU32(); 84 | } 85 | } 86 | array[i] = row; 87 | } 88 | return array; 89 | } 90 | }, 91 | read8: function () { 92 | return this.readU8() << 24 >> 24; 93 | }, 94 | readU8: function () { 95 | if (this.pos >= this.end) 96 | return null; 97 | return this.bytes[this.pos++]; 98 | }, 99 | read16: function () { 100 | return this.readU16() << 16 >> 16; 101 | }, 102 | readU16: function () { 103 | if (this.pos >= this.end - 1) 104 | return null; 105 | var res = this.bytes[this.pos + 0] << 8 | this.bytes[this.pos + 1]; 106 | this.pos += 2; 107 | return res; 108 | }, 109 | read24: function () { 110 | return this.readU24() << 8 >> 8; 111 | }, 112 | readU24: function () { 113 | var pos = this.pos; 114 | var bytes = this.bytes; 115 | if (pos > this.end - 3) 116 | return null; 117 | var res = bytes[pos + 0] << 16 | bytes[pos + 1] << 8 | bytes[pos + 2]; 118 | this.pos += 3; 119 | return res; 120 | }, 121 | peek32: function (advance) { 122 | var pos = this.pos; 123 | var bytes = this.bytes; 124 | if (pos > this.end - 4) 125 | return null; 126 | var res = bytes[pos + 0] << 24 | bytes[pos + 1] << 16 | bytes[pos + 2] << 8 | bytes[pos + 3]; 127 | if (advance) { 128 | this.pos += 4; 129 | } 130 | return res; 131 | }, 132 | read32: function () { 133 | return this.peek32(true); 134 | }, 135 | readU32: function () { 136 | return this.peek32(true) >>> 0; 137 | }, 138 | read4CC: function () { 139 | var pos = this.pos; 140 | if (pos > this.end - 4) 141 | return null; 142 | var res = ""; 143 | for (var i = 0; i < 4; i++) { 144 | res += String.fromCharCode(this.bytes[pos + i]); 145 | } 146 | this.pos += 4; 147 | return res; 148 | }, 149 | readFP16: function () { 150 | return this.read32() / 65536; 151 | }, 152 | readFP8: function () { 153 | return this.read16() / 256; 154 | }, 155 | readISO639: function () { 156 | var bits = this.readU16(); 157 | var res = ""; 158 | for (var i = 0; i < 3; i++) { 159 | var c = (bits >>> (2 - i) * 5) & 0x1f; 160 | res += String.fromCharCode(c + 0x60); 161 | } 162 | return res; 163 | }, 164 | readUTF8: function (length) { 165 | var res = ""; 166 | for (var i = 0; i < length; i++) { 167 | res += String.fromCharCode(this.readU8()); 168 | } 169 | return res; 170 | }, 171 | readPString: function (max) { 172 | var len = this.readU8(); 173 | assert (len <= max); 174 | var res = this.readUTF8(len); 175 | this.reserved(max - len - 1, 0); 176 | return res; 177 | }, 178 | skip: function (length) { 179 | this.seek(this.pos + length); 180 | }, 181 | reserved: function (length, value) { 182 | for (var i = 0; i < length; i++) { 183 | assert (this.readU8() == value); 184 | } 185 | }, 186 | seek: function (index) { 187 | if (index < 0 || index > this.end) { 188 | error("Index out of bounds (bounds: [0, " + this.end + "], index: " + index + ")."); 189 | } 190 | this.pos = index; 191 | }, 192 | subStream: function (start, length) { 193 | return new Bytestream(this.bytes.buffer, start, length); 194 | } 195 | }; 196 | return constructor; 197 | })(); 198 | 199 | 200 | var PARANOID = true; // Heavy-weight assertions. 201 | 202 | /** 203 | * Reads an mp4 file and constructs a object graph that corresponds to the box/atom 204 | * structure of the file. Mp4 files are based on the ISO Base Media format, which in 205 | * turn is based on the Apple Quicktime format. The Quicktime spec is available at: 206 | * http://developer.apple.com/library/mac/#documentation/QuickTime/QTFF. An mp4 spec 207 | * also exists, but I cannot find it freely available. 208 | * 209 | * Mp4 files contain a tree of boxes (or atoms in Quicktime). The general structure 210 | * is as follows (in a pseudo regex syntax): 211 | * 212 | * Box / Atom Structure: 213 | * 214 | * [size type [version flags] field* box*] 215 | * <32> <4C> <--8--> <24-> <-?-> 216 | * <------------- box size ------------> 217 | * 218 | * The box size indicates the entire size of the box and its children, we can use it 219 | * to skip over boxes that are of no interest. Each box has a type indicated by a 220 | * four character code (4C), this describes how the box should be parsed and is also 221 | * used as an object key name in the resulting box tree. For example, the expression: 222 | * "moov.trak[0].mdia.minf" can be used to access individual boxes in the tree based 223 | * on their 4C name. If two or more boxes with the same 4C name exist in a box, then 224 | * an array is built with that name. 225 | * 226 | */ 227 | var MP4Reader = (function reader() { 228 | var BOX_HEADER_SIZE = 8; 229 | var FULL_BOX_HEADER_SIZE = BOX_HEADER_SIZE + 4; 230 | 231 | function constructor(stream) { 232 | this.stream = stream; 233 | this.tracks = {}; 234 | } 235 | 236 | constructor.prototype = { 237 | readBoxes: function (stream, parent) { 238 | while (stream.peek32()) { 239 | var child = this.readBox(stream); 240 | if (child.type in parent) { 241 | var old = parent[child.type]; 242 | if (!(old instanceof Array)) { 243 | parent[child.type] = [old]; 244 | } 245 | parent[child.type].push(child); 246 | } else { 247 | parent[child.type] = child; 248 | } 249 | } 250 | }, 251 | readBox: function readBox(stream) { 252 | var box = { offset: stream.position }; 253 | 254 | function readHeader() { 255 | box.size = stream.readU32(); 256 | box.type = stream.read4CC(); 257 | } 258 | 259 | function readFullHeader() { 260 | box.version = stream.readU8(); 261 | box.flags = stream.readU24(); 262 | } 263 | 264 | function remainingBytes() { 265 | return box.size - (stream.position - box.offset); 266 | } 267 | 268 | function skipRemainingBytes () { 269 | stream.skip(remainingBytes()); 270 | } 271 | 272 | var readRemainingBoxes = function () { 273 | var subStream = stream.subStream(stream.position, remainingBytes()); 274 | this.readBoxes(subStream, box); 275 | stream.skip(subStream.length); 276 | }.bind(this); 277 | 278 | readHeader(); 279 | 280 | switch (box.type) { 281 | case 'ftyp': 282 | box.name = "File Type Box"; 283 | box.majorBrand = stream.read4CC(); 284 | box.minorVersion = stream.readU32(); 285 | box.compatibleBrands = new Array((box.size - 16) / 4); 286 | for (var i = 0; i < box.compatibleBrands.length; i++) { 287 | box.compatibleBrands[i] = stream.read4CC(); 288 | } 289 | break; 290 | case 'moov': 291 | box.name = "Movie Box"; 292 | readRemainingBoxes(); 293 | break; 294 | case 'mvhd': 295 | box.name = "Movie Header Box"; 296 | readFullHeader(); 297 | assert (box.version == 0); 298 | box.creationTime = stream.readU32(); 299 | box.modificationTime = stream.readU32(); 300 | box.timeScale = stream.readU32(); 301 | box.duration = stream.readU32(); 302 | box.rate = stream.readFP16(); 303 | box.volume = stream.readFP8(); 304 | stream.skip(10); 305 | box.matrix = stream.readU32Array(9); 306 | stream.skip(6 * 4); 307 | box.nextTrackId = stream.readU32(); 308 | break; 309 | case 'trak': 310 | box.name = "Track Box"; 311 | readRemainingBoxes(); 312 | this.tracks[box.tkhd.trackId] = new Track(this, box); 313 | break; 314 | case 'tkhd': 315 | box.name = "Track Header Box"; 316 | readFullHeader(); 317 | assert (box.version == 0); 318 | box.creationTime = stream.readU32(); 319 | box.modificationTime = stream.readU32(); 320 | box.trackId = stream.readU32(); 321 | stream.skip(4); 322 | box.duration = stream.readU32(); 323 | stream.skip(8); 324 | box.layer = stream.readU16(); 325 | box.alternateGroup = stream.readU16(); 326 | box.volume = stream.readFP8(); 327 | stream.skip(2); 328 | box.matrix = stream.readU32Array(9); 329 | box.width = stream.readFP16(); 330 | box.height = stream.readFP16(); 331 | break; 332 | case 'mdia': 333 | box.name = "Media Box"; 334 | readRemainingBoxes(); 335 | break; 336 | case 'mdhd': 337 | box.name = "Media Header Box"; 338 | readFullHeader(); 339 | assert (box.version == 0); 340 | box.creationTime = stream.readU32(); 341 | box.modificationTime = stream.readU32(); 342 | box.timeScale = stream.readU32(); 343 | box.duration = stream.readU32(); 344 | box.language = stream.readISO639(); 345 | stream.skip(2); 346 | break; 347 | case 'hdlr': 348 | box.name = "Handler Reference Box"; 349 | readFullHeader(); 350 | stream.skip(4); 351 | box.handlerType = stream.read4CC(); 352 | stream.skip(4 * 3); 353 | var bytesLeft = box.size - 32; 354 | if (bytesLeft > 0) { 355 | box.name = stream.readUTF8(bytesLeft); 356 | } 357 | break; 358 | case 'minf': 359 | box.name = "Media Information Box"; 360 | readRemainingBoxes(); 361 | break; 362 | case 'stbl': 363 | box.name = "Sample Table Box"; 364 | readRemainingBoxes(); 365 | break; 366 | case 'stsd': 367 | box.name = "Sample Description Box"; 368 | readFullHeader(); 369 | box.sd = []; 370 | var entries = stream.readU32(); 371 | readRemainingBoxes(); 372 | break; 373 | case 'avc1': 374 | stream.reserved(6, 0); 375 | box.dataReferenceIndex = stream.readU16(); 376 | assert (stream.readU16() == 0); // Version 377 | assert (stream.readU16() == 0); // Revision Level 378 | stream.readU32(); // Vendor 379 | stream.readU32(); // Temporal Quality 380 | stream.readU32(); // Spatial Quality 381 | box.width = stream.readU16(); 382 | box.height = stream.readU16(); 383 | box.horizontalResolution = stream.readFP16(); 384 | box.verticalResolution = stream.readFP16(); 385 | assert (stream.readU32() == 0); // Reserved 386 | box.frameCount = stream.readU16(); 387 | box.compressorName = stream.readPString(32); 388 | box.depth = stream.readU16(); 389 | assert (stream.readU16() == 0xFFFF); // Color Table Id 390 | readRemainingBoxes(); 391 | break; 392 | case 'mp4a': 393 | stream.reserved(6, 0); 394 | box.dataReferenceIndex = stream.readU16(); 395 | box.version = stream.readU16(); 396 | stream.skip(2); 397 | stream.skip(4); 398 | box.channelCount = stream.readU16(); 399 | box.sampleSize = stream.readU16(); 400 | box.compressionId = stream.readU16(); 401 | box.packetSize = stream.readU16(); 402 | box.sampleRate = stream.readU32() >>> 16; 403 | 404 | // TODO: Parse other version levels. 405 | assert (box.version == 0); 406 | readRemainingBoxes(); 407 | break; 408 | case 'esds': 409 | box.name = "Elementary Stream Descriptor"; 410 | readFullHeader(); 411 | // TODO: Do we really need to parse this? 412 | skipRemainingBytes(); 413 | break; 414 | case 'avcC': 415 | box.name = "AVC Configuration Box"; 416 | box.configurationVersion = stream.readU8(); 417 | box.avcProfileIndication = stream.readU8(); 418 | box.profileCompatibility = stream.readU8(); 419 | box.avcLevelIndication = stream.readU8(); 420 | box.lengthSizeMinusOne = stream.readU8() & 3; 421 | assert (box.lengthSizeMinusOne == 3, "TODO"); 422 | var count = stream.readU8() & 31; 423 | box.sps = []; 424 | for (var i = 0; i < count; i++) { 425 | box.sps.push(stream.readU8Array(stream.readU16())); 426 | } 427 | var count = stream.readU8() & 31; 428 | box.pps = []; 429 | for (var i = 0; i < count; i++) { 430 | box.pps.push(stream.readU8Array(stream.readU16())); 431 | } 432 | skipRemainingBytes(); 433 | break; 434 | case 'btrt': 435 | box.name = "Bit Rate Box"; 436 | box.bufferSizeDb = stream.readU32(); 437 | box.maxBitrate = stream.readU32(); 438 | box.avgBitrate = stream.readU32(); 439 | break; 440 | case 'stts': 441 | box.name = "Decoding Time to Sample Box"; 442 | readFullHeader(); 443 | box.table = stream.readU32Array(stream.readU32(), 2, ["count", "delta"]); 444 | break; 445 | case 'stss': 446 | box.name = "Sync Sample Box"; 447 | readFullHeader(); 448 | box.samples = stream.readU32Array(stream.readU32()); 449 | break; 450 | case 'stsc': 451 | box.name = "Sample to Chunk Box"; 452 | readFullHeader(); 453 | box.table = stream.readU32Array(stream.readU32(), 3, 454 | ["firstChunk", "samplesPerChunk", "sampleDescriptionId"]); 455 | break; 456 | case 'stsz': 457 | box.name = "Sample Size Box"; 458 | readFullHeader(); 459 | box.sampleSize = stream.readU32(); 460 | var count = stream.readU32(); 461 | if (box.sampleSize == 0) { 462 | box.table = stream.readU32Array(count); 463 | } 464 | break; 465 | case 'stco': 466 | box.name = "Chunk Offset Box"; 467 | readFullHeader(); 468 | box.table = stream.readU32Array(stream.readU32()); 469 | break; 470 | case 'smhd': 471 | box.name = "Sound Media Header Box"; 472 | readFullHeader(); 473 | box.balance = stream.readFP8(); 474 | stream.reserved(2, 0); 475 | break; 476 | case 'mdat': 477 | box.name = "Media Data Box"; 478 | assert (box.size >= 8, "Cannot parse large media data yet."); 479 | box.data = stream.readU8Array(remainingBytes()); 480 | break; 481 | default: 482 | skipRemainingBytes(); 483 | break; 484 | }; 485 | return box; 486 | }, 487 | read: function () { 488 | var start = (new Date).getTime(); 489 | this.file = {}; 490 | this.readBoxes(this.stream, this.file); 491 | console.info("Parsed stream in " + ((new Date).getTime() - start) + " ms"); 492 | }, 493 | traceSamples: function () { 494 | var video = this.tracks[1]; 495 | var audio = this.tracks[2]; 496 | 497 | console.info("Video Samples: " + video.getSampleCount()); 498 | console.info("Audio Samples: " + audio.getSampleCount()); 499 | 500 | var vi = 0; 501 | var ai = 0; 502 | 503 | for (var i = 0; i < 100; i++) { 504 | var vo = video.sampleToOffset(vi); 505 | var ao = audio.sampleToOffset(ai); 506 | 507 | var vs = video.sampleToSize(vi, 1); 508 | var as = audio.sampleToSize(ai, 1); 509 | 510 | if (vo < ao) { 511 | console.info("V Sample " + vi + " Offset : " + vo + ", Size : " + vs); 512 | vi ++; 513 | } else { 514 | console.info("A Sample " + ai + " Offset : " + ao + ", Size : " + as); 515 | ai ++; 516 | } 517 | } 518 | } 519 | }; 520 | return constructor; 521 | })(); 522 | 523 | var Track = (function track () { 524 | function constructor(file, trak) { 525 | this.file = file; 526 | this.trak = trak; 527 | } 528 | 529 | constructor.prototype = { 530 | getSampleSizeTable: function () { 531 | return this.trak.mdia.minf.stbl.stsz.table; 532 | }, 533 | getSampleCount: function () { 534 | return this.getSampleSizeTable().length; 535 | }, 536 | /** 537 | * Computes the size of a range of samples, returns zero if length is zero. 538 | */ 539 | sampleToSize: function (start, length) { 540 | var table = this.getSampleSizeTable(); 541 | var size = 0; 542 | for (var i = start; i < start + length; i++) { 543 | size += table[i]; 544 | } 545 | return size; 546 | }, 547 | /** 548 | * Computes the chunk that contains the specified sample, as well as the offset of 549 | * the sample in the computed chunk. 550 | */ 551 | sampleToChunk: function (sample) { 552 | 553 | /* Samples are grouped in chunks which may contain a variable number of samples. 554 | * The sample-to-chunk table in the stsc box describes how samples are arranged 555 | * in chunks. Each table row corresponds to a set of consecutive chunks with the 556 | * same number of samples and description ids. For example, the following table: 557 | * 558 | * +-------------+-------------------+----------------------+ 559 | * | firstChunk | samplesPerChunk | sampleDescriptionId | 560 | * +-------------+-------------------+----------------------+ 561 | * | 1 | 3 | 23 | 562 | * | 3 | 1 | 23 | 563 | * | 5 | 1 | 24 | 564 | * +-------------+-------------------+----------------------+ 565 | * 566 | * describes 5 chunks with a total of (2 * 3) + (2 * 1) + (1 * 1) = 9 samples, 567 | * each chunk containing samples 3, 3, 1, 1, 1 in chunk order, or 568 | * chunks 1, 1, 1, 2, 2, 2, 3, 4, 5 in sample order. 569 | * 570 | * This function determines the chunk that contains a specified sample by iterating 571 | * over every entry in the table. It also returns the position of the sample in the 572 | * chunk which can be used to compute the sample's exact position in the file. 573 | * 574 | * TODO: Determine if we should memoize this function. 575 | */ 576 | 577 | var table = this.trak.mdia.minf.stbl.stsc.table; 578 | 579 | if (table.length === 1) { 580 | var row = table[0]; 581 | assert (row.firstChunk === 1); 582 | return { 583 | index: Math.floor(sample / row.samplesPerChunk), 584 | offset: sample % row.samplesPerChunk 585 | }; 586 | } 587 | 588 | var totalChunkCount = 0; 589 | for (var i = 0; i < table.length; i++) { 590 | var row = table[i]; 591 | if (i > 0) { 592 | var previousRow = table[i - 1]; 593 | var previousChunkCount = row.firstChunk - previousRow.firstChunk; 594 | var previousSampleCount = previousRow.samplesPerChunk * previousChunkCount; 595 | if (sample >= previousSampleCount) { 596 | sample -= previousSampleCount; 597 | if (i == table.length - 1) { 598 | return { 599 | index: totalChunkCount + previousChunkCount + Math.floor(sample / row.samplesPerChunk), 600 | offset: sample % row.samplesPerChunk 601 | }; 602 | } 603 | } else { 604 | return { 605 | index: totalChunkCount + Math.floor(sample / previousRow.samplesPerChunk), 606 | offset: sample % previousRow.samplesPerChunk 607 | }; 608 | } 609 | totalChunkCount += previousChunkCount; 610 | } 611 | } 612 | assert(false); 613 | }, 614 | chunkToOffset: function (chunk) { 615 | var table = this.trak.mdia.minf.stbl.stco.table; 616 | return table[chunk]; 617 | }, 618 | sampleToOffset: function (sample) { 619 | var res = this.sampleToChunk(sample); 620 | var offset = this.chunkToOffset(res.index); 621 | return offset + this.sampleToSize(sample - res.offset, res.offset); 622 | }, 623 | /** 624 | * Computes the sample at the specified time. 625 | */ 626 | timeToSample: function (time) { 627 | /* In the time-to-sample table samples are grouped by their duration. The count field 628 | * indicates the number of consecutive samples that have the same duration. For example, 629 | * the following table: 630 | * 631 | * +-------+-------+ 632 | * | count | delta | 633 | * +-------+-------+ 634 | * | 4 | 3 | 635 | * | 2 | 1 | 636 | * | 3 | 2 | 637 | * +-------+-------+ 638 | * 639 | * describes 9 samples with a total time of (4 * 3) + (2 * 1) + (3 * 2) = 20. 640 | * 641 | * This function determines the sample at the specified time by iterating over every 642 | * entry in the table. 643 | * 644 | * TODO: Determine if we should memoize this function. 645 | */ 646 | var table = this.trak.mdia.minf.stbl.stts.table; 647 | var sample = 0; 648 | for (var i = 0; i < table.length; i++) { 649 | var delta = table[i].count * table[i].delta; 650 | if (time >= delta) { 651 | time -= delta; 652 | sample += table[i].count; 653 | } else { 654 | return sample + Math.floor(time / table[i].delta); 655 | } 656 | } 657 | }, 658 | /** 659 | * Gets the total time of the track. 660 | */ 661 | getTotalTime: function () { 662 | if (PARANOID) { 663 | var table = this.trak.mdia.minf.stbl.stts.table; 664 | var duration = 0; 665 | for (var i = 0; i < table.length; i++) { 666 | duration += table[i].count * table[i].delta; 667 | } 668 | assert (this.trak.mdia.mdhd.duration == duration); 669 | } 670 | return this.trak.mdia.mdhd.duration; 671 | }, 672 | getTotalTimeInSeconds: function () { 673 | return this.timeToSeconds(this.getTotalTime()); 674 | }, 675 | getTimeScale: function () { 676 | return this.trak.mdia.mdhd.timeScale; 677 | }, 678 | /** 679 | * Converts time units to real time (seconds). 680 | */ 681 | timeToSeconds: function (time) { 682 | return time / this.getTimeScale(); 683 | }, 684 | /** 685 | * Converts real time (seconds) to time units. 686 | */ 687 | secondsToTime: function (seconds) { 688 | return seconds * this.getTimeScale(); 689 | }, 690 | foo: function () { 691 | /* 692 | for (var i = 0; i < this.getSampleCount(); i++) { 693 | var res = this.sampleToChunk(i); 694 | console.info("Sample " + i + " -> " + res.index + " % " + res.offset + 695 | " @ " + this.chunkToOffset(res.index) + 696 | " @@ " + this.sampleToOffset(i)); 697 | } 698 | console.info("Total Time: " + this.timeToSeconds(this.getTotalTime())); 699 | var total = this.getTotalTimeInSeconds(); 700 | for (var i = 50; i < total; i += 0.1) { 701 | // console.info("Time: " + i.toFixed(2) + " " + this.secondsToTime(i)); 702 | 703 | console.info("Time: " + i.toFixed(2) + " " + this.timeToSample(this.secondsToTime(i))); 704 | } 705 | */ 706 | }, 707 | /** 708 | * AVC samples contain one or more NAL units each of which have a length prefix. 709 | * This function returns an array of NAL units without their length prefixes. 710 | */ 711 | getSampleNALUnits: function (sample) { 712 | var bytes = this.file.stream.bytes; 713 | var offset = this.sampleToOffset(sample); 714 | var end = offset + this.sampleToSize(sample, 1); 715 | var nalUnits = []; 716 | while(end - offset > 0) { 717 | var length = (new Bytestream(bytes.buffer, offset)).readU32(); 718 | nalUnits.push(bytes.subarray(offset + 4, offset + length + 4)); 719 | offset = offset + length + 4; 720 | } 721 | return nalUnits; 722 | } 723 | }; 724 | return constructor; 725 | })(); 726 | 727 | 728 | // Only add setZeroTimeout to the window object, and hide everything 729 | // else in a closure. (http://dbaron.org/log/20100309-faster-timeouts) 730 | (function() { 731 | var timeouts = []; 732 | var messageName = "zero-timeout-message"; 733 | 734 | // Like setTimeout, but only takes a function argument. There's 735 | // no time argument (always zero) and no arguments (you have to 736 | // use a closure). 737 | function setZeroTimeout(fn) { 738 | timeouts.push(fn); 739 | window.postMessage(messageName, "*"); 740 | } 741 | 742 | function handleMessage(event) { 743 | if (event.source == window && event.data == messageName) { 744 | event.stopPropagation(); 745 | if (timeouts.length > 0) { 746 | var fn = timeouts.shift(); 747 | fn(); 748 | } 749 | } 750 | } 751 | 752 | window.addEventListener("message", handleMessage, true); 753 | 754 | // Add the one thing we want added to the window object. 755 | window.setZeroTimeout = setZeroTimeout; 756 | })(); 757 | 758 | var MP4Player = (function reader() { 759 | var defaultConfig = { 760 | filter: "original", 761 | filterHorLuma: "optimized", 762 | filterVerLumaEdge: "optimized", 763 | getBoundaryStrengthsA: "optimized" 764 | }; 765 | 766 | function constructor(stream, useWorkers, webgl, render) { 767 | this.stream = stream; 768 | this.useWorkers = useWorkers; 769 | this.webgl = webgl; 770 | this.render = render; 771 | 772 | this.statistics = { 773 | videoStartTime: 0, 774 | videoPictureCounter: 0, 775 | windowStartTime: 0, 776 | windowPictureCounter: 0, 777 | fps: 0, 778 | fpsMin: 1000, 779 | fpsMax: -1000, 780 | webGLTextureUploadTime: 0 781 | }; 782 | 783 | this.onStatisticsUpdated = function () {}; 784 | 785 | this.avc = new Player({ 786 | useWorker: useWorkers, 787 | reuseMemory: true, 788 | webgl: webgl, 789 | size: { 790 | width: 640, 791 | height: 368 792 | } 793 | }); 794 | 795 | this.webgl = this.avc.webgl; 796 | 797 | var self = this; 798 | this.avc.onPictureDecoded = function(){ 799 | updateStatistics.call(self); 800 | }; 801 | 802 | this.canvas = this.avc.canvas; 803 | } 804 | 805 | function updateStatistics() { 806 | var s = this.statistics; 807 | s.videoPictureCounter += 1; 808 | s.windowPictureCounter += 1; 809 | var now = Date.now(); 810 | if (!s.videoStartTime) { 811 | s.videoStartTime = now; 812 | } 813 | var videoElapsedTime = now - s.videoStartTime; 814 | s.elapsed = videoElapsedTime / 1000; 815 | if (videoElapsedTime < 1000) { 816 | return; 817 | } 818 | 819 | if (!s.windowStartTime) { 820 | s.windowStartTime = now; 821 | return; 822 | } else if ((now - s.windowStartTime) > 1000) { 823 | var windowElapsedTime = now - s.windowStartTime; 824 | var fps = (s.windowPictureCounter / windowElapsedTime) * 1000; 825 | s.windowStartTime = now; 826 | s.windowPictureCounter = 0; 827 | 828 | if (fps < s.fpsMin) s.fpsMin = fps; 829 | if (fps > s.fpsMax) s.fpsMax = fps; 830 | s.fps = fps; 831 | } 832 | 833 | var fps = (s.videoPictureCounter / videoElapsedTime) * 1000; 834 | s.fpsSinceStart = fps; 835 | this.onStatisticsUpdated(this.statistics); 836 | return; 837 | } 838 | 839 | constructor.prototype = { 840 | readAll: function(callback) { 841 | console.info("MP4Player::readAll()"); 842 | this.stream.readAll(null, function (buffer) { 843 | this.reader = new MP4Reader(new Bytestream(buffer)); 844 | this.reader.read(); 845 | var video = this.reader.tracks[1]; 846 | this.size = new Size(video.trak.tkhd.width, video.trak.tkhd.height); 847 | console.info("MP4Player::readAll(), length: " + this.reader.stream.length); 848 | if (callback) callback(); 849 | }.bind(this)); 850 | }, 851 | play: function() { 852 | var reader = this.reader; 853 | 854 | if (!reader) { 855 | this.readAll(this.play.bind(this)); 856 | return; 857 | }; 858 | 859 | var video = reader.tracks[1]; 860 | var audio = reader.tracks[2]; 861 | 862 | var avc = reader.tracks[1].trak.mdia.minf.stbl.stsd.avc1.avcC; 863 | var sps = avc.sps[0]; 864 | var pps = avc.pps[0]; 865 | 866 | /* Decode Sequence & Picture Parameter Sets */ 867 | this.avc.decode(sps); 868 | this.avc.decode(pps); 869 | 870 | /* Decode Pictures */ 871 | var pic = 0; 872 | setTimeout(function foo() { 873 | var avc = this.avc; 874 | video.getSampleNALUnits(pic).forEach(function (nal) { 875 | avc.decode(nal); 876 | }); 877 | pic ++; 878 | if (pic < 3000) { 879 | setTimeout(foo.bind(this), 1); 880 | }; 881 | }.bind(this), 1); 882 | } 883 | }; 884 | 885 | return constructor; 886 | })(); 887 | 888 | var Broadway = (function broadway() { 889 | function constructor(div) { 890 | var src = div.attributes.src ? div.attributes.src.value : undefined; 891 | var width = div.attributes.width ? div.attributes.width.value : 640; 892 | var height = div.attributes.height ? div.attributes.height.value : 480; 893 | 894 | var controls = document.createElement('div'); 895 | controls.setAttribute('style', "z-index: 100; position: absolute; bottom: 0px; background-color: rgba(0,0,0,0.8); height: 30px; width: 100%; text-align: left;"); 896 | this.info = document.createElement('div'); 897 | this.info.setAttribute('style', "font-size: 14px; font-weight: bold; padding: 6px; color: lime;"); 898 | controls.appendChild(this.info); 899 | div.appendChild(controls); 900 | 901 | var useWorkers = div.attributes.workers ? div.attributes.workers.value == "true" : false; 902 | var render = div.attributes.render ? div.attributes.render.value == "true" : false; 903 | 904 | var webgl = "auto"; 905 | if (div.attributes.webgl){ 906 | if (div.attributes.webgl.value == "true"){ 907 | webgl = true; 908 | }; 909 | if (div.attributes.webgl.value == "false"){ 910 | webgl = false; 911 | }; 912 | }; 913 | 914 | var infoStrPre = "Click canvas to load and play - "; 915 | var infoStr = ""; 916 | if (useWorkers){ 917 | infoStr += "worker thread "; 918 | }else{ 919 | infoStr += "main thread "; 920 | }; 921 | 922 | this.player = new MP4Player(new Stream(src), useWorkers, webgl, render); 923 | this.canvas = this.player.canvas; 924 | this.canvas.onclick = function () { 925 | this.play(); 926 | }.bind(this); 927 | div.appendChild(this.canvas); 928 | 929 | 930 | infoStr += " - webgl: " + this.player.webgl; 931 | this.info.innerHTML = infoStrPre + infoStr; 932 | 933 | 934 | this.score = null; 935 | this.player.onStatisticsUpdated = function (statistics) { 936 | if (statistics.videoPictureCounter % 10 != 0) { 937 | return; 938 | } 939 | var info = ""; 940 | if (statistics.fps) { 941 | info += " fps: " + statistics.fps.toFixed(2); 942 | } 943 | if (statistics.fpsSinceStart) { 944 | info += " avg: " + statistics.fpsSinceStart.toFixed(2); 945 | } 946 | var scoreCutoff = 1200; 947 | if (statistics.videoPictureCounter < scoreCutoff) { 948 | this.score = scoreCutoff - statistics.videoPictureCounter; 949 | } else if (statistics.videoPictureCounter == scoreCutoff) { 950 | this.score = statistics.fpsSinceStart.toFixed(2); 951 | } 952 | // info += " score: " + this.score; 953 | 954 | this.info.innerHTML = infoStr + info; 955 | }.bind(this); 956 | } 957 | constructor.prototype = { 958 | play: function () { 959 | this.player.play(); 960 | } 961 | }; 962 | return constructor; 963 | })(); 964 | -------------------------------------------------------------------------------- /lib/nodetello.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* Ryze Tello drone Node.JS 4 | * based on PyTello https://bitbucket.org/PingguSoft/pytello 5 | * sovgvd@gmail.com 6 | */ 7 | 8 | // TODO, make it better... sometime... 9 | 10 | const dgram = require('dgram'); 11 | const fs = require('fs'); 12 | 13 | 14 | var nodetello = function () { 15 | 16 | this.tello_control = dgram.createSocket('udp4'); 17 | this.tello_video = dgram.createSocket('udp4'); 18 | this.tello_video_output = false; // callback function 19 | this.tello_telemetry_output = false; // callback function 20 | this.tello_telemetry_config = false; // fields to request 21 | this.save_video_path = false; 22 | this.save_photo_path = false; 23 | this.DEG = Math.PI / 180; 24 | 25 | this.TYPE = { 26 | BYTE : 1, 27 | SHORT: 2, 28 | FLOAT: 3, 29 | }; 30 | 31 | // exposure values 32 | this.EV = { 33 | '-3.0' : -9, 34 | '-2.7' : -8, 35 | '-2.3' : -7, 36 | '-2.0' : -6, 37 | '-1.7' : -5, 38 | '-1.3' : -4, 39 | '-1.0' : -3, 40 | '-0.7' : -2, 41 | '-0.3' : -1, 42 | '0.0' : 0, 43 | '+0.3' : 1, 44 | '+0.7' : 2, 45 | '+1.0' : 3, 46 | '+1.3' : 4, 47 | '+1.7' : 5, 48 | '+2.0' : 6, 49 | '+2.3' : 7, 50 | '+2.7' : 8, 51 | '+3.0' : 9 52 | }; 53 | 54 | this.status = { 55 | altitude: { 56 | max: null, 57 | }, 58 | version: null, 59 | speed: { 60 | north: null, 61 | east: null, 62 | ground: null, 63 | vertical: null, 64 | }, 65 | fly_time: { 66 | time: null, 67 | }, 68 | wifi: { 69 | signal: null, 70 | interference: null, 71 | }, 72 | LOG: { 73 | }, 74 | state: { 75 | imu: { 76 | state: null, 77 | calibration: null, 78 | }, 79 | pressure: null, 80 | down_visual: null, 81 | power: null, 82 | battery: { 83 | state: null, 84 | percentage: null, 85 | left: null, 86 | low: null, 87 | lower: null, 88 | voltage: null, 89 | }, 90 | gravity: null, 91 | wind: null, 92 | em: { 93 | state: null, 94 | sky: null, 95 | ground: null, 96 | open: null, 97 | }, 98 | temperature_height: null, 99 | factory_mode: null, 100 | fly_mode: null, 101 | drone_hover: null, 102 | outage_recording: null, 103 | throw_fly_timer: null, 104 | camera: null, 105 | front: { 106 | in: null, 107 | out: null, 108 | lsc: null, 109 | } 110 | } 111 | } 112 | 113 | this.tello = { 114 | control: { 115 | host: '192.168.10.1', 116 | port: 8889 117 | }, 118 | video: { 119 | port: 6037 120 | }, 121 | commands: { 122 | TELLO_CMD_CONN: 1, 123 | TELLO_CMD_CONN_ACK: 2, 124 | TELLO_CMD_SSID: 17, 125 | TELLO_CMD_SET_SSID: 18, 126 | TELLO_CMD_SSID_PASS: 19, 127 | TELLO_CMD_SET_SSID_PASS: 20, 128 | TELLO_CMD_REGION: 21, 129 | TELLO_CMD_SET_REGION: 22, 130 | TELLO_CMD_REQ_VIDEO_SPS_PPS: 37, 131 | TELLO_CMD_TAKE_PICTURE: 48, 132 | TELLO_CMD_SWITCH_PICTURE_VIDEO: 49, 133 | TELLO_CMD_START_RECORDING: 50, 134 | TELLO_CMD_SET_EV: 52, 135 | TELLO_CMD_DATE_TIME: 70, 136 | TELLO_CMD_STICK: 80, 137 | TELLO_CMD_LOG_HEADER_WRITE: 4176, 138 | TELLO_CMD_LOG_DATA_WRITE: 4177, 139 | TELLO_CMD_LOG_CONFIGURATION: 4178, 140 | TELLO_CMD_WIFI_SIGNAL: 26, 141 | TELLO_CMD_VIDEO_BIT_RATE: 40, 142 | TELLO_CMD_LIGHT_STRENGTH: 53, 143 | TELLO_CMD_VERSION_STRING: 69, 144 | TELLO_CMD_ACTIVATION_TIME: 71, 145 | TELLO_CMD_LOADER_VERSION: 73, 146 | TELLO_CMD_STATUS: 86, 147 | TELLO_CMD_ALT_LIMIT: 4182, 148 | TELLO_CMD_LOW_BATT_THRESHOLD: 4183, 149 | TELLO_CMD_ATT_ANGLE: 4185, 150 | TELLO_CMD_SET_JPEG_QUALITY: 55, 151 | TELLO_CMD_TAKEOFF: 84, 152 | TELLO_CMD_LANDING: 85, 153 | TELLO_CMD_SET_ALT_LIMIT: 88, 154 | TELLO_CMD_FLIP: 92, 155 | TELLO_CMD_THROW_FLY: 93, 156 | TELLO_CMD_PALM_LANDING: 94, 157 | TELLO_CMD_PLANE_CALIBRATION: 4180, 158 | TELLO_CMD_SET_LOW_BATTERY_THRESHOLD: 4181, 159 | TELLO_CMD_SET_ATTITUDE_ANGLE: 4184, 160 | TELLO_CMD_ERROR1: 67, 161 | TELLO_CMD_ERROR2: 68, 162 | TELLO_CMD_FILE_SIZE: 98, 163 | TELLO_CMD_FILE_DATA: 99, 164 | TELLO_CMD_FILE_COMPLETE: 100, 165 | TELLO_CMD_HANDLE_IMU_ANGLE: 90, 166 | TELLO_CMD_SET_VIDEO_BIT_RATE: 32, 167 | TELLO_CMD_SET_DYN_ADJ_RATE: 33, 168 | TELLO_CMD_SET_EIS: 36, 169 | TELLO_CMD_SMART_VIDEO_START: 128, 170 | TELLO_CMD_SMART_VIDEO_STATUS: 129, 171 | TELLO_CMD_BOUNCE: 4179 172 | }, 173 | log: { 174 | SEPARATOR: 85, // U symbol 175 | // Full log spec https://github.com/Kragrathea/TelloLib/blob/master/TelloLib/parsedRecSpecs.json 176 | LOGREC: { 177 | 16: { 178 | NAME: "USONIC", 179 | length: 4, 180 | fields: { 181 | H: { 182 | offset: 0, 183 | type: this.TYPE.SHORT 184 | }, 185 | FLAG: { 186 | offset: 2, 187 | type: this.TYPE.BYTE 188 | }, 189 | CNT: { 190 | offset: 3, 191 | type: this.TYPE.BYTE 192 | } 193 | } 194 | }, 195 | 29: { 196 | NAME: "MVO", 197 | length: 80, 198 | fields: { 199 | VX: { 200 | offset: 2, 201 | type: this.TYPE.SHORT 202 | }, 203 | VY: { 204 | offset: 4, 205 | type: this.TYPE.SHORT 206 | }, 207 | VZ: { 208 | offset: 6, 209 | type: this.TYPE.SHORT 210 | }, 211 | PX: { 212 | offset: 8, 213 | type: this.TYPE.FLOAT 214 | }, 215 | PY: { 216 | offset: 12, 217 | type: this.TYPE.FLOAT 218 | }, 219 | PZ: { 220 | offset: 16, 221 | type: this.TYPE.FLOAT 222 | }, 223 | HEIGHT: { 224 | offset: 68, 225 | type: this.TYPE.FLOAT 226 | } 227 | } 228 | }, 229 | 2048: { 230 | NAME: "IMU", 231 | length: 120, 232 | postprocess: { 233 | ANGLE: { 234 | func: false, 235 | } 236 | }, 237 | fields: { 238 | ACC_X: { 239 | offset: 20, 240 | type: this.TYPE.FLOAT 241 | }, 242 | ACC_Y: { 243 | offset: 24, 244 | type: this.TYPE.FLOAT 245 | }, 246 | ACC_Z: { 247 | offset: 28, 248 | type: this.TYPE.FLOAT 249 | }, 250 | GYRO_X: { 251 | offset: 32, 252 | type: this.TYPE.FLOAT 253 | }, 254 | GYRO_Y: { 255 | offset: 36, 256 | type: this.TYPE.FLOAT 257 | }, 258 | GYRO_Z: { 259 | offset: 40, 260 | type: this.TYPE.FLOAT 261 | }, 262 | PRESS: { 263 | offset: 44, 264 | type: this.TYPE.FLOAT 265 | }, 266 | Q_W: { 267 | offset: 48, 268 | type: this.TYPE.FLOAT 269 | }, 270 | Q_X: { 271 | offset: 52, 272 | type: this.TYPE.FLOAT 273 | }, 274 | Q_Y: { 275 | offset: 56, 276 | type: this.TYPE.FLOAT 277 | }, 278 | Q_Z: { 279 | offset: 60, 280 | type: this.TYPE.FLOAT 281 | }, 282 | ag_x: { 283 | offset: 64, 284 | type: this.TYPE.FLOAT 285 | }, 286 | ag_y: { 287 | offset: 68, 288 | type: this.TYPE.FLOAT 289 | }, 290 | ag_z: { 291 | offset: 72, 292 | type: this.TYPE.FLOAT 293 | }, 294 | vg_x: { 295 | offset: 76, 296 | type: this.TYPE.FLOAT 297 | }, 298 | vg_y: { 299 | offset: 80, 300 | type: this.TYPE.FLOAT 301 | }, 302 | vg_z: { 303 | offset: 84, 304 | type: this.TYPE.FLOAT 305 | }, 306 | gb_x: { 307 | offset: 88, 308 | type: this.TYPE.FLOAT 309 | }, 310 | gb_y: { 311 | offset: 92, 312 | type: this.TYPE.FLOAT 313 | }, 314 | gb_z: { 315 | offset: 96, 316 | type: this.TYPE.FLOAT 317 | }, 318 | m_x: { 319 | offset: 100, 320 | type: this.TYPE.SHORT 321 | }, 322 | m_y: { 323 | offset: 102, 324 | type: this.TYPE.SHORT 325 | }, 326 | m_z: { 327 | offset: 104, 328 | type: this.TYPE.SHORT 329 | }, 330 | temp_x: { 331 | offset: 106, 332 | type: this.TYPE.SHORT 333 | }, 334 | temp_y: { 335 | offset: 108, 336 | type: this.TYPE.SHORT 337 | }, 338 | temp_z: { 339 | offset: 110, 340 | type: this.TYPE.SHORT 341 | }, 342 | } 343 | }, 344 | } 345 | }, 346 | smart_video: { 347 | TELLO_SMART_VIDEO_STOP: 0x00, 348 | TELLO_SMART_VIDEO_START: 0x01, 349 | TELLO_SMART_VIDEO_360: 0x01, 350 | TELLO_SMART_VIDEO_CIRCLE: 0x02, 351 | TELLO_SMART_VIDEO_UP_OUT: 0x03, 352 | }, 353 | TBL_CRC16: [ 354 | 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, 355 | 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, 356 | 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, 357 | 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, 358 | 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 359 | 0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, 360 | 0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, 361 | 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70, 362 | 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, 363 | 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 364 | 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 365 | 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134, 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, 366 | 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3, 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, 367 | 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, 368 | 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, 369 | 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78 370 | ], 371 | TBL_CRC8: [ 372 | 0x00, 0x5e, 0xbc, 0xe2, 0x61, 0x3f, 0xdd, 0x83, 0xc2, 0x9c, 0x7e, 0x20, 0xa3, 0xfd, 0x1f, 0x41, 373 | 0x9d, 0xc3, 0x21, 0x7f, 0xfc, 0xa2, 0x40, 0x1e, 0x5f, 0x01, 0xe3, 0xbd, 0x3e, 0x60, 0x82, 0xdc, 374 | 0x23, 0x7d, 0x9f, 0xc1, 0x42, 0x1c, 0xfe, 0xa0, 0xe1, 0xbf, 0x5d, 0x03, 0x80, 0xde, 0x3c, 0x62, 375 | 0xbe, 0xe0, 0x02, 0x5c, 0xdf, 0x81, 0x63, 0x3d, 0x7c, 0x22, 0xc0, 0x9e, 0x1d, 0x43, 0xa1, 0xff, 376 | 0x46, 0x18, 0xfa, 0xa4, 0x27, 0x79, 0x9b, 0xc5, 0x84, 0xda, 0x38, 0x66, 0xe5, 0xbb, 0x59, 0x07, 377 | 0xdb, 0x85, 0x67, 0x39, 0xba, 0xe4, 0x06, 0x58, 0x19, 0x47, 0xa5, 0xfb, 0x78, 0x26, 0xc4, 0x9a, 378 | 0x65, 0x3b, 0xd9, 0x87, 0x04, 0x5a, 0xb8, 0xe6, 0xa7, 0xf9, 0x1b, 0x45, 0xc6, 0x98, 0x7a, 0x24, 379 | 0xf8, 0xa6, 0x44, 0x1a, 0x99, 0xc7, 0x25, 0x7b, 0x3a, 0x64, 0x86, 0xd8, 0x5b, 0x05, 0xe7, 0xb9, 380 | 0x8c, 0xd2, 0x30, 0x6e, 0xed, 0xb3, 0x51, 0x0f, 0x4e, 0x10, 0xf2, 0xac, 0x2f, 0x71, 0x93, 0xcd, 381 | 0x11, 0x4f, 0xad, 0xf3, 0x70, 0x2e, 0xcc, 0x92, 0xd3, 0x8d, 0x6f, 0x31, 0xb2, 0xec, 0x0e, 0x50, 382 | 0xaf, 0xf1, 0x13, 0x4d, 0xce, 0x90, 0x72, 0x2c, 0x6d, 0x33, 0xd1, 0x8f, 0x0c, 0x52, 0xb0, 0xee, 383 | 0x32, 0x6c, 0x8e, 0xd0, 0x53, 0x0d, 0xef, 0xb1, 0xf0, 0xae, 0x4c, 0x12, 0x91, 0xcf, 0x2d, 0x73, 384 | 0xca, 0x94, 0x76, 0x28, 0xab, 0xf5, 0x17, 0x49, 0x08, 0x56, 0xb4, 0xea, 0x69, 0x37, 0xd5, 0x8b, 385 | 0x57, 0x09, 0xeb, 0xb5, 0x36, 0x68, 0x8a, 0xd4, 0x95, 0xcb, 0x29, 0x77, 0xf4, 0xaa, 0x48, 0x16, 386 | 0xe9, 0xb7, 0x55, 0x0b, 0x88, 0xd6, 0x34, 0x6a, 0x2b, 0x75, 0x97, 0xc9, 0x4a, 0x14, 0xf6, 0xa8, 387 | 0x74, 0x2a, 0xc8, 0x96, 0x15, 0x4b, 0xa9, 0xf7, 0xb6, 0xe8, 0x0a, 0x54, 0xd7, 0x89, 0x6b, 0x35, 388 | ] 389 | } 390 | 391 | 392 | this.seqID = 0; 393 | 394 | this.stickData = { 395 | roll : 0, 396 | pitch : 0, 397 | throttle : 0, 398 | yaw : 0, 399 | fast:0 400 | }; 401 | /* 402 | stick data is float -1,+1 403 | MODE 2 404 | 405 | throlle (lY) pitch (Ry) 406 | | | 407 | ---+--- yaw (lX) ---+--- roll (Rx) 408 | | | 409 | 410 | 411 | */ 412 | this.rcCtr = 0; 413 | this.statusCtr = 0; 414 | this.video_stream_file = false; 415 | this.failsafe = 0; 416 | this.armed = false; 417 | 418 | this._calcCRC16 = function (buf, size) { 419 | var i = 0 420 | var seed = 0x3692 421 | while (size > 0) { 422 | seed = this.tello.TBL_CRC16[(seed ^ buf[i]) & 0xff] ^ (seed >> 8) 423 | i++ 424 | size-- 425 | }; 426 | return seed 427 | } 428 | 429 | this._calcCRC8 = function (buf, size) { 430 | var i = 0 431 | var seed = 0x77 432 | while (size > 0) { 433 | seed = this.tello.TBL_CRC8[(seed ^ buf[i]) & 0xff] 434 | i++ 435 | size-- 436 | } 437 | return seed 438 | } 439 | 440 | this._buildPacket = function (pacType, cmdID, seq, data, data_len) { 441 | var size = 11 + (data_len?data_len:(data?data.length:0)); 442 | var bb = Buffer.alloc(size) 443 | bb.writeUInt8(0xCC,0) 444 | bb.writeUInt16LE(size << 3,1) 445 | var crc8 = this._calcCRC8([...bb], 3) 446 | bb.writeUInt8(crc8,3); 447 | bb.writeUInt8(pacType,4); 448 | bb.writeUInt16LE(cmdID,5); 449 | bb.writeUInt16LE(seq,7); 450 | if (data && !data_len) { 451 | if (Buffer.isBuffer(data)) { 452 | // TODO all should be buffer!!! 453 | data.copy(bb, 9); 454 | } else { 455 | bb.write(data,9); 456 | } 457 | } else if (data) { 458 | for (var i=0; i= 11) { 472 | var mark = bb.readUInt8(0); 473 | if (mark == 0xCC) { // start of packet 474 | var size = bb.readUInt16LE(1) >> 3; 475 | var crc8 = bb.readUInt8(3); 476 | var calcCRC8 = this._calcCRC8([...bb], 3); 477 | if (crc8 != calcCRC8) { 478 | console.log('wrong CRC8',crc8, calcCRC8); // TODO error handler 479 | } 480 | var pacType = bb.readUInt8(4); 481 | var cmdID = bb.readUInt16LE(5); 482 | this.seqID = bb.readUInt16LE(7); 483 | var dataSize = size - 11; 484 | var data = false; 485 | var crc16 = bb.readUInt16LE(size - 2); 486 | var calcCRC16 = this._calcCRC16([...bb], size - 2); 487 | if (crc16 != calcCRC16) { 488 | console.log ('wrong CRC16', crc16, calcCRC16); // TODO error handler 489 | } 490 | //console.log("package", mark, pacType, cmdID, dataSize); 491 | if (cmdID == this.tello.commands.TELLO_CMD_WIFI_SIGNAL) { 492 | this.status.wifi.signal = bb.readUInt8(9); 493 | this.status.wifi.interference = bb.readUInt8(10); 494 | 495 | } else if (cmdID == this.tello.commands.TELLO_CMD_LIGHT_STRENGTH) { 496 | this.status.light = bb.readUInt8(9); 497 | 498 | } else if (cmdID == this.tello.commands.TELLO_CMD_STATUS) { 499 | this.parseStatus(bb); 500 | 501 | } else if (cmdID == this.tello.commands.TELLO_CMD_LOG_HEADER_WRITE) { // log header, we need request log 502 | var payload = Buffer.alloc(3); 503 | payload[1]=bb[9]; 504 | payload[2]=bb[10]; 505 | this._sendCmd(0x50, this.tello.commands.TELLO_CMD_LOG_HEADER_WRITE, payload, 3); 506 | 507 | } else if (cmdID == this.tello.commands.TELLO_CMD_LOG_DATA_WRITE) { 508 | try { 509 | this.parseLog (bb, size); 510 | } catch (e) { 511 | console.log("Log parse error", e, bb, size); 512 | } 513 | 514 | } else { 515 | console.log("DBG", "unknown CMD", cmdID); 516 | } 517 | } else { 518 | if (mark == 0x63) { 519 | var ack = Buffer.alloc(11); 520 | ack.write('conn_ack:'); 521 | ack.writeUInt16LE(this.tello.video.port,9); 522 | if (ack.toString() == bb.toString()) { 523 | cmdID = this.tello.commands.TELLO_CMD_CONN_ACK; 524 | this.initVideo(); 525 | } else { 526 | console.log('wrong video port !!', ack, bb); // TODO error handler 527 | } 528 | } else { 529 | console.log ('wrong mark !! ', mark); // TODO error handler 530 | } 531 | } 532 | } else { 533 | console.log('wrong packet', bb); 534 | } 535 | return cmdID 536 | } 537 | 538 | this._b = function (b) { 539 | return parseInt(b, 10).toString(2); 540 | } 541 | 542 | this._sendCmd = function(pacType, cmdID, data, data_len) { 543 | var bb = false 544 | var pp = false 545 | var payload = false 546 | var out = false 547 | var seq = 0 548 | var len = 0 549 | var ts = new Date(); 550 | 551 | if (cmdID == this.tello.commands.TELLO_CMD_CONN) { 552 | out = Buffer.alloc(11) 553 | out.write('conn_req:') 554 | out.writeUInt16LE(this.tello.video.port,9); 555 | this.seqID++ 556 | } else if (cmdID == this.tello.commands.TELLO_CMD_STICK) { 557 | // https://github.com/hanyazou/TelloPy/blob/develop-0.6.0/tellopy/_internal/tello.py 558 | bb = Buffer.alloc(11) 559 | 560 | // TODO... this string manipulation is soooo slow! 561 | var tmp_roll = "00000000000"+(parseInt(1024 + 660 * this.stickData.roll) & 0x07ff).toString(2); tmp_roll=tmp_roll.substr(-11); 562 | var tmp_pitch = "00000000000"+(parseInt(1024 + 660 * this.stickData.pitch) & 0x07ff).toString(2); tmp_pitch=tmp_pitch.substr(-11); 563 | var tmp_throttle = "00000000000"+(parseInt(1024 + 660 * this.stickData.throttle) & 0x07ff).toString(2); tmp_throttle=tmp_throttle.substr(-11); 564 | var tmp_yaw = "00000000000"+(parseInt(1024 + 660 * this.stickData.yaw) & 0x07ff).toString(2); tmp_yaw=tmp_yaw.substr(-11); 565 | //var tmp_boost = "00000000000"+(parseInt(1024 + 660 * 0)).toString(2); 566 | 567 | // well... JS is 32 bit only and for this thing we need 64bit (11*4 bit + 4 more for 48bit value)... lets do this as strings 568 | // !!!!string should be 48bits (chars)!!!! 569 | var rc = "0000"+tmp_yaw.toString(2)+tmp_throttle.toString(2)+tmp_pitch.toString(2)+tmp_roll.toString(2); 570 | 571 | bb.writeUInt8(parseInt(rc.substr( -8,8),2), 0); 572 | bb.writeUInt8(parseInt(rc.substr(-16,8),2), 1); 573 | bb.writeUInt8(parseInt(rc.substr(-24,8),2), 2); 574 | bb.writeUInt8(parseInt(rc.substr(-32,8),2), 3); 575 | bb.writeUInt8(parseInt(rc.substr(-40,8),2), 4); 576 | bb.writeUInt8(parseInt(rc.substr(-48,8),2), 5); 577 | 578 | bb.writeUInt8(ts.getHours(),6); 579 | bb.writeUInt8(ts.getMinutes(),7); 580 | bb.writeUInt8(ts.getSeconds(),8); 581 | bb.writeUInt16LE(ts.getMilliseconds()*1000 & 0xffff,9); 582 | //console.log("DBG", [tmp_roll.length, tmp_pitch.length, tmp_throttle.length, tmp_yaw.length], rc, rc.length, bb); 583 | //console.log("DBG", rc, '=>', rc.substr(-48,8), rc.substr(-40,8), rc.substr(-32,8), rc.substr(-24,8), rc.substr(-16,8), rc.substr(-8,8)); 584 | seq = 0 585 | 586 | } else if (cmdID == this.tello.commands.TELLO_CMD_DATE_TIME) { 587 | seq = parseInt(this.seqID) 588 | bb = Buffer.alloc(15) 589 | bb.writeUInt8(0x00,0) 590 | bb.writeUInt16LE(ts.getYear(),1) 591 | bb.writeUInt16LE(ts.getMonth(),3) 592 | bb.writeUInt16LE(ts.getDay(),5) 593 | bb.writeUInt16LE(ts.getHours(),7) 594 | bb.writeUInt16LE(ts.getMinutes(),9) 595 | bb.writeUInt16LE(ts.getSeconds(),11) 596 | bb.writeUInt16LE(ts.getMilliseconds()*1000 & 0xffff,13) 597 | this.seqID++ 598 | } else if (cmdID == this.tello.commands.TELLO_CMD_REQ_VIDEO_SPS_PPS) { 599 | seq = 0 600 | } else { 601 | seq = parseInt(this.seqID) 602 | this.seqID++ 603 | } 604 | 605 | if (bb) { 606 | payload = bb; 607 | } else { 608 | payload = data; 609 | } 610 | 611 | if (!out) out = this._buildPacket(pacType, cmdID, seq, payload, data_len) 612 | 613 | /*if (cmdID != this.tello.commands.TELLO_CMD_STICK) { 614 | console.log("SEND:", cmdID, bb, payload.length, out); // TODO DEBUG handler 615 | }*/ 616 | this.tello_control.send(out,0,out.length, this.tello.control.port, this.tello.control.host, function(err, bytes) { 617 | // TODO error handler 618 | //if (err) throw err; 619 | //console.log('UDP message sent', bytes,err); 620 | //client.close();) 621 | }); 622 | } 623 | 624 | this._cmdRX =function (msg, addr) { 625 | var data = Buffer.from(msg); 626 | var payload = false; 627 | 628 | var size = msg.length; 629 | 630 | var cmdID = this._parsePacket(data); 631 | //console.log("command = ", cmdID); 632 | var payload = data.slice(9,size); 633 | 634 | if (cmdID == this.tello.commands.TELLO_CMD_CONN_ACK) { 635 | console.log("TELLO_CMD_CONN_ACK") 636 | console.log('Connection successful!'); 637 | } else if (cmdID == this.tello.commands.TELLO_CMD_DATE_TIME) { 638 | console.log("TELLO_CMD_DATE_TIME") 639 | this._sendCmd(0x50, this.tello.commands.TELLO_CMD_DATE_TIME, false) 640 | } else if (cmdID == this.tello.commands.TELLO_CMD_STATUS) { 641 | //console.log("TELLO_CMD_STATUS", this.statusCtr); 642 | if (this.statusCtr == 3) { 643 | this._sendCmd(0x60, this.tello.commands.TELLO_CMD_REQ_VIDEO_SPS_PPS, false) 644 | this._sendCmd(0x48, this.tello.commands.TELLO_CMD_VERSION_STRING, false) 645 | this._sendCmd(0x48, this.tello.commands.TELLO_CMD_SET_VIDEO_BIT_RATE, false) 646 | this._sendCmd(0x48, this.tello.commands.TELLO_CMD_ALT_LIMIT, false) 647 | this._sendCmd(0x48, this.tello.commands.TELLO_CMD_LOW_BATT_THRESHOLD, false) 648 | this._sendCmd(0x48, this.tello.commands.TELLO_CMD_ATT_ANGLE, false) 649 | this._sendCmd(0x48, this.tello.commands.TELLO_CMD_REGION, false) 650 | this._sendCmd(0x48, this.tello.commands.TELLO_CMD_SET_EV, 0x00,1); 651 | } 652 | this.statusCtr++ 653 | } else if (cmdID == this.tello.commands.TELLO_CMD_VERSION_STRING) { 654 | console.log("TELLO_CMD_VERSION_STRING") 655 | if (size >= 42) { 656 | this.status.version = data.slice(10,30).toString(); 657 | console.log ('Version:', this.status.version); 658 | } 659 | } else if (cmdID == this.tello.commands.TELLO_CMD_SMART_VIDEO_START) { 660 | console.log("TELLO_CMD_SMART_VIDEO_START") 661 | if (payload.length > 0) { 662 | console.log('smart video start'); 663 | } 664 | } else if (cmdID == this.tello.commands.TELLO_CMD_ALT_LIMIT) { 665 | console.log("TELLO_CMD_ALT_LIMIT") 666 | if (payload.length > 0) { 667 | //payload.get_ULInt8() # 0x00 668 | this.status.altitude.max = payload.readUInt16LE(1) 669 | console.log ('Altitude limit:',this.status.altitude.max, 'meters'); 670 | 671 | // TODO 672 | //if height != self.NEW_ALT_LIMIT: 673 | // print 'set new alt limit : {0:2d} meter'.format(self.NEW_ALT_LIMIT) 674 | // self._sendCmd(0x68, self.TELLO_CMD_SET_ALT_LIMIT, bytearray([self.NEW_ALT_LIMIT & 0xff, (self.NEW_ALT_LIMIT >> 8) & 0xff])); 675 | } 676 | 677 | } else if (cmdID == this.tello.commands.TELLO_CMD_SMART_VIDEO_STATUS) { 678 | console.log("TELLO_CMD_SMART_VIDEO_STATUS") 679 | if (payload.length > 0) { 680 | var resp = payload.readUInt8() 681 | var dummy = resp & 0x07 682 | var start = (resp >> 3) & 0x03 683 | var mode = (resp >> 5) & 0x07 684 | console.log ('smart video status - mode:, start:', mode, start); 685 | this._sendCmd(0x50, this.tello.commands.TELLO_CMD_SMART_VIDEO_STATUS, 0x00); 686 | } 687 | } 688 | } 689 | 690 | 691 | this._timerTask = function() { 692 | this._sendCmd(0x60, this.tello.commands.TELLO_CMD_STICK, false); 693 | this.rcCtr++; 694 | 695 | // every 0.5 seconds increase failsafe value 696 | if (this.rcCtr % 25 == 0) { 697 | this.failsafe++; 698 | } 699 | 700 | if (this.failsafe>2) { 701 | // no data more than 0.5*2 seconds, set failsafe 702 | if (this.armed == true) { 703 | console.log("FAILSAFE!!!") 704 | this.tello_cmd({cmd: { cmd: "land", value: 0 }}); 705 | } 706 | // set sticks data to default (middle) 707 | this.stickData = { 708 | roll : 0, 709 | pitch : 0, 710 | throttle : 0, 711 | yaw : 0, 712 | fast:0 713 | }; 714 | 715 | } 716 | 717 | //every 1sec as this task run every 20ms, so 50*20 = 1000 = 1 sec 718 | if (this.rcCtr % 50 == 0) { 719 | this._sendCmd(0x60, this.tello.commands.TELLO_CMD_REQ_VIDEO_SPS_PPS, false) 720 | } 721 | } 722 | 723 | 724 | this.tello_cmd = function (data) { 725 | // JSON with command 726 | /* 727 | { 728 | cmd: { 729 | cmd: "photo" | "ev" | "quality.photo", 730 | value: ??? | see below | ??? 731 | } 732 | } 733 | 734 | EV: 735 | -3.0, -2.7, -2.3, -2.0, -1.7, -1.3, -1.0, -0.7, -0.3, 0, 0.3, 0.7, 1.0, 1.3, 1.7, 2.0, 2.3, 2.7, 3.0 736 | */ 737 | //console.log("TELLO_CMD:", data); 738 | if (data.cmd.cmd == 'ev') { 739 | console.log("TELLO_CMD SET_EV:", this.EV[data.cmd.value]); 740 | var payload = Buffer.alloc(1); 741 | payload[0]=this.EV[data.cmd.value]; 742 | this._sendCmd(0x48, this.tello.commands.TELLO_CMD_SET_EV, payload, 1); 743 | } 744 | if (data.cmd.cmd == 'takeoff') { 745 | this.armed = true; // TODO check logs insted of this, NOT SAFE!!! 746 | console.log("TELLO_CMD_TAKEOFF"); 747 | this._sendCmd(0x68, this.tello.commands.TELLO_CMD_TAKEOFF, false); 748 | } 749 | if (data.cmd.cmd == 'land') { 750 | this.armed = false; // TODO check logs insted of this, NOT SAFE!!! 751 | console.log("TELLO_CMD_LANDING"); 752 | var payload = Buffer.alloc(1); 753 | payload[0]=0; 754 | this._sendCmd(0x68, this.tello.commands.TELLO_CMD_LANDING, payload, 1); 755 | } 756 | if (data.cmd.cmd == 'stick') { 757 | this.stickData.roll = data.cmd.value.roll; if (this.stickData.roll>1 || this.stickData.roll<-1) this.stickData.roll = 0; 758 | this.stickData.pitch = data.cmd.value.pitch; if (this.stickData.pitch>1 || this.stickData.pitch<-1) this.stickData.pitch = 0; 759 | this.stickData.throttle = data.cmd.value.throttle; if (this.stickData.throttle>1 || this.stickData.throttle<-1) this.stickData.throttle = 0; 760 | this.stickData.yaw = data.cmd.value.yaw; if (this.stickData.yaw>1 || this.stickData.yaw<-1) this.stickData.yaw = 0; 761 | } 762 | 763 | if (data.cmd.cmd == 'ping') { 764 | this.failsafe = 0; 765 | } 766 | } 767 | 768 | 769 | this.init = function () { 770 | 771 | if (this.save_video_path!==false) { 772 | this.video_stream_file = fs.createWriteStream(this.save_video_path+"/"+((new Date()).getTime())+'.video.h264'); 773 | } 774 | 775 | this.tello_control.on('error', function (err){ 776 | console.log(`tello_control error:\n${err.stack}`); 777 | this.tello_control.close(); 778 | }.bind(this)); 779 | 780 | this.tello_control.on('message', function (msg, rinfo) { 781 | this._cmdRX(msg,rinfo); 782 | }.bind(this)); 783 | 784 | this.tello_control.on('listening', function () { 785 | var tello_address = this.tello_control.address(); 786 | console.log(`tello_control listening ${tello_address.address}:${tello_address.port}`); 787 | }.bind(this)); 788 | 789 | this._sendCmd(0, this.tello.commands.TELLO_CMD_CONN); 790 | 791 | setInterval(function () { this._timerTask(); }.bind(this),20); 792 | 793 | this.tello_video.on('error', function (err) { 794 | console.log(`tello_video error:\n${err.stack}`); 795 | setTimeout(function() { this.initVideo(); }.bind(this),1000); 796 | }.bind(this)); 797 | 798 | this.tello_video.on('message', function (msg, rinfo) { 799 | if (this.save_video_path!==false) { 800 | this.video_stream_file.write(msg.slice(2)); 801 | } 802 | if (this.tello_video_output!=false) { 803 | this.tello_video_output(msg.slice(2)); // video feed callback 804 | } 805 | }.bind(this)); 806 | 807 | this.tello_video.on('listening', function () { 808 | var tello_video_address = this.tello_video.address(); 809 | console.log(`tello_video listening ${tello_video_address.address}:${tello_video_address.port}`); 810 | }.bind(this)); 811 | } 812 | 813 | this.initVideo = function() { 814 | this.tello_video.bind(this.tello.video.port); 815 | } 816 | 817 | this.parseStatus = function (bb) { 818 | //flight data msg (https://github.com/hanyazou/TelloPy/blob/master/tellopy/_internal/protocol.py) 0x56 819 | // just skip first 9 bytes 820 | this.status.speed.vertical = bb.readInt16LE(9); 821 | this.status.speed.north = bb.readInt16LE(11); 822 | this.status.speed.east = bb.readInt16LE(13); 823 | this.status.speed.ground = bb.readInt16LE(15); 824 | this.status.fly_time.time = bb.readInt16LE(17); 825 | 826 | var tmp = bb.readInt8(19); 827 | this.status.state.imu.state = (tmp >> 0) & 0x1; 828 | this.status.state.pressure = (tmp >> 1) & 0x1; 829 | this.status.state.down_visual = (tmp >> 2) & 0x1; 830 | this.status.state.power = (tmp >> 3) & 0x1; 831 | this.status.state.battery.state = (tmp >> 4) & 0x1; 832 | this.status.state.gravity = (tmp >> 5) & 0x1; 833 | this.status.state.wind = (tmp >> 7) & 0x1; 834 | 835 | this.status.state.imu.calibration = bb.readInt8(20); 836 | this.status.state.battery.percentage = bb.readInt8(21); 837 | this.status.state.battery.left = bb.readInt16LE(22); 838 | this.status.state.battery.voltage = bb.readInt16LE(24); 839 | 840 | tmp = bb.readInt8(26); 841 | this.status.state.em.sky = (tmp >> 0) & 0x1; 842 | this.status.state.em.ground = (tmp >> 1) & 0x1; 843 | this.status.state.em.open = (tmp >> 2) & 0x1; 844 | this.status.state.drone_hover = (tmp >> 3) & 0x1; 845 | this.status.state.outage_recording = (tmp >> 4) & 0x1; 846 | this.status.state.battery.low = (tmp >> 5) & 0x1; 847 | this.status.state.battery.lower = (tmp >> 6) & 0x1; 848 | this.status.state.factory_mode = (tmp >> 7) & 0x1; 849 | 850 | this.status.state.fly_mode = bb.readInt8(27); 851 | this.status.state.throw_fly_timer = bb.readInt8(28); 852 | this.status.state.camera = bb.readInt8(29); 853 | this.status.state.em.state = bb.readInt8(30); 854 | 855 | tmp = bb.readInt8(31); 856 | this.status.state.front.in = (tmp >> 0) & 0x1; 857 | this.status.state.front.out = (tmp >> 1) & 0x1; 858 | this.status.state.front.lsc = (tmp >> 2) & 0x1; 859 | 860 | this.status.state.temperature_height = bb.readInt8(32); 861 | //console.log("state", this.status); 862 | } 863 | 864 | this.parseLog = function (bb, size) { 865 | // https://github.com/Kragrathea/TelloLib/blob/master/TelloLib/TelloLog.cs 866 | // https://github.com/SMerrony/tello/blob/master/flog.go 867 | var logPosition = 0; 868 | var logFieldName = ""; 869 | var logObject = false; 870 | while (logPosition < (size-2)) { 871 | if (bb.readUInt8(logPosition) != this.tello.log.SEPARATOR) { 872 | logPosition++; 873 | continue; 874 | } 875 | var len = bb.readUInt8(logPosition + 1); // TODO, could be: RangeError: Index out of range 876 | if (bb.readUInt8(logPosition + 2) != 0) { 877 | logPosition++; 878 | break; 879 | } 880 | var recSpecId = bb.readUInt16LE(logPosition + 4); 881 | var xorBuf = Buffer.alloc(256); 882 | var xorValue = bb[logPosition + 6]; 883 | if (typeof this.tello.log.LOGREC[recSpecId] != 'undefined') { 884 | //console.log("DBG", " found:", this.tello.log.LOGREC[recSpecId].NAME); 885 | for (var i = 0; i < len; i++) { // Decrypt payload 886 | xorBuf[i] = (bb[logPosition + i] ^ xorValue); 887 | } 888 | var baseOffset = 10; 889 | for (var i in this.tello.log.LOGREC[recSpecId].fields) { if (typeof this.tello.log.LOGREC[recSpecId].fields[i] == 'object') { 890 | logObject = this.tello.log.LOGREC[recSpecId].fields[i]; 891 | logFieldName = this.tello.log.LOGREC[recSpecId].NAME+"."+i; 892 | switch (logObject.type) { 893 | case this.TYPE.FLOAT: 894 | this.status[logFieldName] = xorBuf.readFloatLE(baseOffset + logObject.offset); 895 | break; 896 | case this.TYPE.SHORT: 897 | this.status[logFieldName] = xorBuf.readInt16LE(baseOffset + logObject.offset); 898 | break; 899 | case this.TYPE.BYTE: 900 | this.status[logFieldName] = xorBuf[baseOffset + logObject.offset]; 901 | break; 902 | default: 903 | console.log("DBG", "unknown type", logObject.type); 904 | } 905 | }} 906 | if (typeof this.tello.log.LOGREC[recSpecId].postprocess == 'object') { 907 | for (var i in this.tello.log.LOGREC[recSpecId].postprocess) { 908 | if (typeof this.tello.log.LOGREC[recSpecId].postprocess[i].func == 'function') { 909 | this.tello.log.LOGREC[recSpecId].postprocess[i].func(); 910 | } 911 | } 912 | } 913 | //console.log("STATUS:", this.status); 914 | this.sendLog(); 915 | } else { 916 | //console.log("DBG", "unknown log type", recSpecId, "datasize = ", dataSize); 917 | } 918 | logPosition += len; 919 | } 920 | } 921 | this.sendLog = function () { 922 | // prepare telemetry output based on configuration 923 | var output = {}; 924 | if (this.tello_telemetry_config !== false) { 925 | for (var i in this.tello_telemetry_config) { 926 | if (typeof this.status[this.tello_telemetry_config[i]] != 'undefined') { 927 | output[i] = this.status[this.tello_telemetry_config[i]]; 928 | } 929 | } 930 | this.tello_telemetry_output(JSON.stringify(output)); 931 | } 932 | } 933 | 934 | this.QtoDeg = function () { 935 | // https://github.com/SMerrony/tello/blob/master/flog.go 936 | try { 937 | var sqX = Math.pow(this.status['IMU.Q_X'],2); 938 | var sqY = Math.pow(this.status['IMU.Q_Y'],2); 939 | var sqZ = Math.pow(this.status['IMU.Q_Z'],2); 940 | 941 | var sinR = 2 * (this.status['IMU.Q_W'] * this.status['IMU.Q_X'] + this.status['IMU.Q_Y'] * this.status['IMU.Q_Z']); 942 | var cosR = 1 - 2 * (sqX + sqY); 943 | this.status['IMU.ANGLE_ROLL'] = parseFloat(Math.atan2(sinR, cosR) / this.DEG); 944 | 945 | var sinP = 2 * (this.status['IMU.Q_W'] * this.status['IMU.Q_Y'] - this.status['IMU.Q_Z'] * this.status['IMU.Q_X']); 946 | if (sinP > 1) { 947 | sinP = 1; 948 | } else if (sinP < -1) { 949 | sinP = -1; 950 | } 951 | this.status['IMU.ANGLE_PITCH'] = parseFloat(Math.asin(sinP) / this.DEG); 952 | 953 | var sinY = 2 * (this.status['IMU.Q_W'] * this.status['IMU.Q_Z'] + this.status['IMU.Q_X'] * this.status['IMU.Q_Y']); 954 | var cosY = 1 - 2 * (sqY + sqZ); 955 | this.status['IMU.ANGLE_YAW'] = parseFloat(Math.atan2(sinY, cosY) / this.DEG); 956 | } catch (e) { 957 | console.log("ERROR","QtoDeg", e); 958 | } 959 | } 960 | this.tello.log.LOGREC[2048].postprocess.ANGLE.func = function () { this.QtoDeg(); }.bind(this); 961 | } 962 | 963 | module.exports = nodetello; 964 | -------------------------------------------------------------------------------- /public_html/Decoder.js: -------------------------------------------------------------------------------- 1 | // universal module definition 2 | (function (root, factory) { 3 | if (typeof define === 'function' && define.amd) { 4 | // AMD. Register as an anonymous module. 5 | define([], factory); 6 | } else if (typeof exports === 'object') { 7 | // Node. Does not work with strict CommonJS, but 8 | // only CommonJS-like environments that support module.exports, 9 | // like Node. 10 | module.exports = factory(); 11 | } else { 12 | // Browser globals (root is window) 13 | root.Decoder = factory(); 14 | } 15 | }(this, function () { 16 | 17 | var global; 18 | 19 | function initglobal(){ 20 | global = this; 21 | if (!global){ 22 | if (typeof window != "undefined"){ 23 | global = window; 24 | }else if (typeof self != "undefined"){ 25 | global = self; 26 | }; 27 | }; 28 | }; 29 | initglobal(); 30 | 31 | 32 | function error(message) { 33 | console.error(message); 34 | console.trace(); 35 | }; 36 | 37 | 38 | function assert(condition, message) { 39 | if (!condition) { 40 | error(message); 41 | }; 42 | }; 43 | 44 | 45 | 46 | 47 | var getModule = function(par_broadwayOnHeadersDecoded, par_broadwayOnPictureDecoded){ 48 | 49 | 50 | /*var ModuleX = { 51 | 'print': function(text) { console.log('stdout: ' + text); }, 52 | 'printErr': function(text) { console.log('stderr: ' + text); } 53 | };*/ 54 | 55 | 56 | /* 57 | 58 | The reason why this is all packed into one file is that this file can also function as worker. 59 | you can integrate the file into your build system and provide the original file to be loaded into a worker. 60 | 61 | */ 62 | 63 | //var Module = (function(){ 64 | 65 | 66 | var Module=typeof Module!=="undefined"?Module:{};var moduleOverrides={};var key;for(key in Module){if(Module.hasOwnProperty(key)){moduleOverrides[key]=Module[key]}}Module["arguments"]=[];Module["thisProgram"]="./this.program";Module["quit"]=(function(status,toThrow){throw toThrow});Module["preRun"]=[];Module["postRun"]=[];var ENVIRONMENT_IS_WEB=false;var ENVIRONMENT_IS_WORKER=false;var ENVIRONMENT_IS_NODE=false;var ENVIRONMENT_IS_SHELL=false;if(Module["ENVIRONMENT"]){if(Module["ENVIRONMENT"]==="WEB"){ENVIRONMENT_IS_WEB=true}else if(Module["ENVIRONMENT"]==="WORKER"){ENVIRONMENT_IS_WORKER=true}else if(Module["ENVIRONMENT"]==="NODE"){ENVIRONMENT_IS_NODE=true}else if(Module["ENVIRONMENT"]==="SHELL"){ENVIRONMENT_IS_SHELL=true}else{throw new Error("Module['ENVIRONMENT'] value is not valid. must be one of: WEB|WORKER|NODE|SHELL.")}}else{ENVIRONMENT_IS_WEB=typeof window==="object";ENVIRONMENT_IS_WORKER=typeof importScripts==="function";ENVIRONMENT_IS_NODE=typeof process==="object"&&typeof null==="function"&&!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_WORKER;ENVIRONMENT_IS_SHELL=!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_NODE&&!ENVIRONMENT_IS_WORKER}if(ENVIRONMENT_IS_NODE){var nodeFS;var nodePath;Module["read"]=function shell_read(filename,binary){var ret;if(!nodeFS)nodeFS=(null)("fs");if(!nodePath)nodePath=(null)("path");filename=nodePath["normalize"](filename);ret=nodeFS["readFileSync"](filename);return binary?ret:ret.toString()};Module["readBinary"]=function readBinary(filename){var ret=Module["read"](filename,true);if(!ret.buffer){ret=new Uint8Array(ret)}assert(ret.buffer);return ret};if(process["argv"].length>1){Module["thisProgram"]=process["argv"][1].replace(/\\/g,"/")}Module["arguments"]=process["argv"].slice(2);if(typeof module!=="undefined"){module["exports"]=Module}process["on"]("uncaughtException",(function(ex){if(!(ex instanceof ExitStatus)){throw ex}}));process["on"]("unhandledRejection",(function(reason,p){process["exit"](1)}));Module["inspect"]=(function(){return"[Emscripten Module object]"})}else if(ENVIRONMENT_IS_SHELL){if(typeof read!="undefined"){Module["read"]=function shell_read(f){return read(f)}}Module["readBinary"]=function readBinary(f){var data;if(typeof readbuffer==="function"){return new Uint8Array(readbuffer(f))}data=read(f,"binary");assert(typeof data==="object");return data};if(typeof scriptArgs!="undefined"){Module["arguments"]=scriptArgs}else if(typeof arguments!="undefined"){Module["arguments"]=arguments}if(typeof quit==="function"){Module["quit"]=(function(status,toThrow){quit(status)})}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){Module["read"]=function shell_read(url){var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.send(null);return xhr.responseText};if(ENVIRONMENT_IS_WORKER){Module["readBinary"]=function readBinary(url){var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}Module["readAsync"]=function readAsync(url,onload,onerror){var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=function xhr_onload(){if(xhr.status==200||xhr.status==0&&xhr.response){onload(xhr.response);return}onerror()};xhr.onerror=onerror;xhr.send(null)};Module["setWindowTitle"]=(function(title){document.title=title})}else{throw new Error("not compiled for this environment")}Module["print"]=typeof console!=="undefined"?console.log.bind(console):typeof print!=="undefined"?print:null;Module["printErr"]=typeof printErr!=="undefined"?printErr:typeof console!=="undefined"&&console.warn.bind(console)||Module["print"];Module.print=Module["print"];Module.printErr=Module["printErr"];for(key in moduleOverrides){if(moduleOverrides.hasOwnProperty(key)){Module[key]=moduleOverrides[key]}}moduleOverrides=undefined;var STACK_ALIGN=16;function staticAlloc(size){assert(!staticSealed);var ret=STATICTOP;STATICTOP=STATICTOP+size+15&-16;return ret}function alignMemory(size,factor){if(!factor)factor=STACK_ALIGN;var ret=size=Math.ceil(size/factor)*factor;return ret}var asm2wasmImports={"f64-rem":(function(x,y){return x%y}),"debugger":(function(){debugger})};var functionPointers=new Array(0);var GLOBAL_BASE=1024;var ABORT=0;var EXITSTATUS=0;function assert(condition,text){if(!condition){abort("Assertion failed: "+text)}}function Pointer_stringify(ptr,length){if(length===0||!ptr)return"";var hasUtf=0;var t;var i=0;while(1){t=HEAPU8[ptr+i>>0];hasUtf|=t;if(t==0&&!length)break;i++;if(length&&i==length)break}if(!length)length=i;var ret="";if(hasUtf<128){var MAX_CHUNK=1024;var curr;while(length>0){curr=String.fromCharCode.apply(String,HEAPU8.subarray(ptr,ptr+Math.min(length,MAX_CHUNK)));ret=ret?ret+curr:curr;ptr+=MAX_CHUNK;length-=MAX_CHUNK}return ret}return UTF8ToString(ptr)}var UTF8Decoder=typeof TextDecoder!=="undefined"?new TextDecoder("utf8"):undefined;function UTF8ArrayToString(u8Array,idx){var endPtr=idx;while(u8Array[endPtr])++endPtr;if(endPtr-idx>16&&u8Array.subarray&&UTF8Decoder){return UTF8Decoder.decode(u8Array.subarray(idx,endPtr))}else{var u0,u1,u2,u3,u4,u5;var str="";while(1){u0=u8Array[idx++];if(!u0)return str;if(!(u0&128)){str+=String.fromCharCode(u0);continue}u1=u8Array[idx++]&63;if((u0&224)==192){str+=String.fromCharCode((u0&31)<<6|u1);continue}u2=u8Array[idx++]&63;if((u0&240)==224){u0=(u0&15)<<12|u1<<6|u2}else{u3=u8Array[idx++]&63;if((u0&248)==240){u0=(u0&7)<<18|u1<<12|u2<<6|u3}else{u4=u8Array[idx++]&63;if((u0&252)==248){u0=(u0&3)<<24|u1<<18|u2<<12|u3<<6|u4}else{u5=u8Array[idx++]&63;u0=(u0&1)<<30|u1<<24|u2<<18|u3<<12|u4<<6|u5}}}if(u0<65536){str+=String.fromCharCode(u0)}else{var ch=u0-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}}}}function UTF8ToString(ptr){return UTF8ArrayToString(HEAPU8,ptr)}var UTF16Decoder=typeof TextDecoder!=="undefined"?new TextDecoder("utf-16le"):undefined;var WASM_PAGE_SIZE=65536;var ASMJS_PAGE_SIZE=16777216;function alignUp(x,multiple){if(x%multiple>0){x+=multiple-x%multiple}return x}var buffer,HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateGlobalBuffer(buf){Module["buffer"]=buffer=buf}function updateGlobalBufferViews(){Module["HEAP8"]=HEAP8=new Int8Array(buffer);Module["HEAP16"]=HEAP16=new Int16Array(buffer);Module["HEAP32"]=HEAP32=new Int32Array(buffer);Module["HEAPU8"]=HEAPU8=new Uint8Array(buffer);Module["HEAPU16"]=HEAPU16=new Uint16Array(buffer);Module["HEAPU32"]=HEAPU32=new Uint32Array(buffer);Module["HEAPF32"]=HEAPF32=new Float32Array(buffer);Module["HEAPF64"]=HEAPF64=new Float64Array(buffer)}var STATIC_BASE,STATICTOP,staticSealed;var STACK_BASE,STACKTOP,STACK_MAX;var DYNAMIC_BASE,DYNAMICTOP_PTR;STATIC_BASE=STATICTOP=STACK_BASE=STACKTOP=STACK_MAX=DYNAMIC_BASE=DYNAMICTOP_PTR=0;staticSealed=false;function abortOnCannotGrowMemory(){abort("Cannot enlarge memory arrays. Either (1) compile with -s TOTAL_MEMORY=X with X higher than the current value "+TOTAL_MEMORY+", (2) compile with -s ALLOW_MEMORY_GROWTH=1 which allows increasing the size at runtime, or (3) if you want malloc to return NULL (0) instead of this abort, compile with -s ABORTING_MALLOC=0 ")}function enlargeMemory(){abortOnCannotGrowMemory()}var TOTAL_STACK=Module["TOTAL_STACK"]||5242880;var TOTAL_MEMORY=Module["TOTAL_MEMORY"]||52428800;if(TOTAL_MEMORY0){var callback=callbacks.shift();if(typeof callback=="function"){callback();continue}var func=callback.func;if(typeof func==="number"){if(callback.arg===undefined){Module["dynCall_v"](func)}else{Module["dynCall_vi"](func,callback.arg)}}else{func(callback.arg===undefined?null:callback.arg)}}}var __ATPRERUN__=[];var __ATINIT__=[];var __ATMAIN__=[];var __ATEXIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;var runtimeExited=false;function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(__ATPRERUN__)}function ensureInitRuntime(){if(runtimeInitialized)return;runtimeInitialized=true;callRuntimeCallbacks(__ATINIT__)}function preMain(){callRuntimeCallbacks(__ATMAIN__)}function exitRuntime(){callRuntimeCallbacks(__ATEXIT__);runtimeExited=true}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var Math_abs=Math.abs;var Math_cos=Math.cos;var Math_sin=Math.sin;var Math_tan=Math.tan;var Math_acos=Math.acos;var Math_asin=Math.asin;var Math_atan=Math.atan;var Math_atan2=Math.atan2;var Math_exp=Math.exp;var Math_log=Math.log;var Math_sqrt=Math.sqrt;var Math_ceil=Math.ceil;var Math_floor=Math.floor;var Math_pow=Math.pow;var Math_imul=Math.imul;var Math_fround=Math.fround;var Math_round=Math.round;var Math_min=Math.min;var Math_max=Math.max;var Math_clz32=Math.clz32;var Math_trunc=Math.trunc;var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function addRunDependency(id){runDependencies++;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}}function removeRunDependency(id){runDependencies--;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}Module["preloadedImages"]={};Module["preloadedAudios"]={};var dataURIPrefix="data:application/octet-stream;base64,";function isDataURI(filename){return String.prototype.startsWith?filename.startsWith(dataURIPrefix):filename.indexOf(dataURIPrefix)===0}function integrateWasmJS(){var wasmTextFile="avc.wast";var wasmBinaryFile="avc.wasm";var asmjsCodeFile="avc.temp.asm.js";if(typeof Module["locateFile"]==="function"){if(!isDataURI(wasmTextFile)){wasmTextFile=Module["locateFile"](wasmTextFile)}if(!isDataURI(wasmBinaryFile)){wasmBinaryFile=Module["locateFile"](wasmBinaryFile)}if(!isDataURI(asmjsCodeFile)){asmjsCodeFile=Module["locateFile"](asmjsCodeFile)}}var wasmPageSize=64*1024;var info={"global":null,"env":null,"asm2wasm":asm2wasmImports,"parent":Module};var exports=null;function mergeMemory(newBuffer){var oldBuffer=Module["buffer"];if(newBuffer.byteLength>2];return ret}),getStr:(function(){var ret=Pointer_stringify(SYSCALLS.get());return ret}),get64:(function(){var low=SYSCALLS.get(),high=SYSCALLS.get();if(low>=0)assert(high===0);else assert(high===-1);return low}),getZero:(function(){assert(SYSCALLS.get()===0)})};function ___syscall140(which,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(),offset_high=SYSCALLS.get(),offset_low=SYSCALLS.get(),result=SYSCALLS.get(),whence=SYSCALLS.get();var offset=offset_low;FS.llseek(stream,offset,whence);HEAP32[result>>2]=stream.position;if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS==="undefined"||!(e instanceof FS.ErrnoError))abort(e);return-e.errno}}function ___syscall146(which,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.get(),iov=SYSCALLS.get(),iovcnt=SYSCALLS.get();var ret=0;if(!___syscall146.buffers){___syscall146.buffers=[null,[],[]];___syscall146.printChar=(function(stream,curr){var buffer=___syscall146.buffers[stream];assert(buffer);if(curr===0||curr===10){(stream===1?Module["print"]:Module["printErr"])(UTF8ArrayToString(buffer,0));buffer.length=0}else{buffer.push(curr)}})}for(var i=0;i>2];var len=HEAP32[iov+(i*8+4)>>2];for(var j=0;j>2]=value;return value}DYNAMICTOP_PTR=staticAlloc(4);STACK_BASE=STACKTOP=alignMemory(STATICTOP);STACK_MAX=STACK_BASE+TOTAL_STACK;DYNAMIC_BASE=alignMemory(STACK_MAX);HEAP32[DYNAMICTOP_PTR>>2]=DYNAMIC_BASE;staticSealed=true;Module["wasmTableSize"]=10;Module["wasmMaxTableSize"]=10;Module.asmGlobalArg={};Module.asmLibraryArg={"abort":abort,"enlargeMemory":enlargeMemory,"getTotalMemory":getTotalMemory,"abortOnCannotGrowMemory":abortOnCannotGrowMemory,"___setErrNo":___setErrNo,"___syscall140":___syscall140,"___syscall146":___syscall146,"___syscall54":___syscall54,"___syscall6":___syscall6,"_broadwayOnHeadersDecoded":_broadwayOnHeadersDecoded,"_broadwayOnPictureDecoded":_broadwayOnPictureDecoded,"_emscripten_memcpy_big":_emscripten_memcpy_big,"DYNAMICTOP_PTR":DYNAMICTOP_PTR,"STACKTOP":STACKTOP};var asm=Module["asm"](Module.asmGlobalArg,Module.asmLibraryArg,buffer);Module["asm"]=asm;var _broadwayCreateStream=Module["_broadwayCreateStream"]=(function(){return Module["asm"]["_broadwayCreateStream"].apply(null,arguments)});var _broadwayExit=Module["_broadwayExit"]=(function(){return Module["asm"]["_broadwayExit"].apply(null,arguments)});var _broadwayGetMajorVersion=Module["_broadwayGetMajorVersion"]=(function(){return Module["asm"]["_broadwayGetMajorVersion"].apply(null,arguments)});var _broadwayGetMinorVersion=Module["_broadwayGetMinorVersion"]=(function(){return Module["asm"]["_broadwayGetMinorVersion"].apply(null,arguments)});var _broadwayInit=Module["_broadwayInit"]=(function(){return Module["asm"]["_broadwayInit"].apply(null,arguments)});var _broadwayPlayStream=Module["_broadwayPlayStream"]=(function(){return Module["asm"]["_broadwayPlayStream"].apply(null,arguments)});Module["asm"]=asm;function ExitStatus(status){this.name="ExitStatus";this.message="Program terminated with exit("+status+")";this.status=status}ExitStatus.prototype=new Error;ExitStatus.prototype.constructor=ExitStatus;var initialStackTop;dependenciesFulfilled=function runCaller(){if(!Module["calledRun"])run();if(!Module["calledRun"])dependenciesFulfilled=runCaller};function run(args){args=args||Module["arguments"];if(runDependencies>0){return}preRun();if(runDependencies>0)return;if(Module["calledRun"])return;function doRun(){if(Module["calledRun"])return;Module["calledRun"]=true;if(ABORT)return;ensureInitRuntime();preMain();if(Module["onRuntimeInitialized"])Module["onRuntimeInitialized"]();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout((function(){setTimeout((function(){Module["setStatus"]("")}),1);doRun()}),1)}else{doRun()}}Module["run"]=run;function exit(status,implicit){if(implicit&&Module["noExitRuntime"]&&status===0){return}if(Module["noExitRuntime"]){}else{ABORT=true;EXITSTATUS=status;STACKTOP=initialStackTop;exitRuntime();if(Module["onExit"])Module["onExit"](status)}if(ENVIRONMENT_IS_NODE){process["exit"](status)}Module["quit"](status,new ExitStatus(status))}Module["exit"]=exit;function abort(what){if(Module["onAbort"]){Module["onAbort"](what)}if(what!==undefined){Module.print(what);Module.printErr(what);what=JSON.stringify(what)}else{what=""}ABORT=true;EXITSTATUS=1;throw"abort("+what+"). Build with -s ASSERTIONS=1 for more info."}Module["abort"]=abort;if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}Module["noExitRuntime"]=true;run() 67 | 68 | 69 | 70 | // return Module; 71 | //})(); 72 | 73 | var resultModule; 74 | if (typeof global !== "undefined"){ 75 | if (global.Module){ 76 | resultModule = global.Module; 77 | }; 78 | }; 79 | if (typeof Module != "undefined"){ 80 | resultModule = Module; 81 | }; 82 | 83 | resultModule._broadwayOnHeadersDecoded = par_broadwayOnHeadersDecoded; 84 | resultModule._broadwayOnPictureDecoded = par_broadwayOnPictureDecoded; 85 | 86 | var moduleIsReady = false; 87 | var cbFun; 88 | var moduleReady = function(){ 89 | moduleIsReady = true; 90 | if (cbFun){ 91 | cbFun(resultModule); 92 | } 93 | }; 94 | 95 | resultModule.onRuntimeInitialized = function(){ 96 | moduleReady(resultModule); 97 | }; 98 | return function(callback){ 99 | if (moduleIsReady){ 100 | callback(resultModule); 101 | }else{ 102 | cbFun = callback; 103 | }; 104 | }; 105 | }; 106 | 107 | return (function(){ 108 | "use strict"; 109 | 110 | 111 | var nowValue = function(){ 112 | return (new Date()).getTime(); 113 | }; 114 | 115 | if (typeof performance != "undefined"){ 116 | if (performance.now){ 117 | nowValue = function(){ 118 | return performance.now(); 119 | }; 120 | }; 121 | }; 122 | 123 | 124 | var Decoder = function(parOptions){ 125 | this.options = parOptions || {}; 126 | 127 | this.now = nowValue; 128 | 129 | var asmInstance; 130 | 131 | var fakeWindow = { 132 | }; 133 | 134 | var toU8Array; 135 | var toU32Array; 136 | 137 | var onPicFun = function ($buffer, width, height) { 138 | var buffer = this.pictureBuffers[$buffer]; 139 | if (!buffer) { 140 | buffer = this.pictureBuffers[$buffer] = toU8Array($buffer, (width * height * 3) / 2); 141 | }; 142 | 143 | var infos; 144 | var doInfo = false; 145 | if (this.infoAr.length){ 146 | doInfo = true; 147 | infos = this.infoAr; 148 | }; 149 | this.infoAr = []; 150 | 151 | if (this.options.rgb){ 152 | if (!asmInstance){ 153 | asmInstance = getAsm(width, height); 154 | }; 155 | asmInstance.inp.set(buffer); 156 | asmInstance.doit(); 157 | 158 | var copyU8 = new Uint8Array(asmInstance.outSize); 159 | copyU8.set( asmInstance.out ); 160 | 161 | if (doInfo){ 162 | infos[0].finishDecoding = nowValue(); 163 | }; 164 | 165 | this.onPictureDecoded(copyU8, width, height, infos); 166 | return; 167 | 168 | }; 169 | 170 | if (doInfo){ 171 | infos[0].finishDecoding = nowValue(); 172 | }; 173 | this.onPictureDecoded(buffer, width, height, infos); 174 | }.bind(this); 175 | 176 | var ignore = false; 177 | 178 | if (this.options.sliceMode){ 179 | onPicFun = function ($buffer, width, height, $sliceInfo) { 180 | if (ignore){ 181 | return; 182 | }; 183 | var buffer = this.pictureBuffers[$buffer]; 184 | if (!buffer) { 185 | buffer = this.pictureBuffers[$buffer] = toU8Array($buffer, (width * height * 3) / 2); 186 | }; 187 | var sliceInfo = this.pictureBuffers[$sliceInfo]; 188 | if (!sliceInfo) { 189 | sliceInfo = this.pictureBuffers[$sliceInfo] = toU32Array($sliceInfo, 18); 190 | }; 191 | 192 | var infos; 193 | var doInfo = false; 194 | if (this.infoAr.length){ 195 | doInfo = true; 196 | infos = this.infoAr; 197 | }; 198 | this.infoAr = []; 199 | 200 | /*if (this.options.rgb){ 201 | 202 | no rgb in slice mode 203 | 204 | };*/ 205 | 206 | infos[0].finishDecoding = nowValue(); 207 | var sliceInfoAr = []; 208 | for (var i = 0; i < 20; ++i){ 209 | sliceInfoAr.push(sliceInfo[i]); 210 | }; 211 | infos[0].sliceInfoAr = sliceInfoAr; 212 | 213 | this.onPictureDecoded(buffer, width, height, infos); 214 | }.bind(this); 215 | }; 216 | 217 | var ModuleCallback = getModule.apply(fakeWindow, [function () { 218 | }, onPicFun]); 219 | 220 | 221 | var MAX_STREAM_BUFFER_LENGTH = 1024 * 1024; 222 | 223 | var instance = this; 224 | this.onPictureDecoded = function (buffer, width, height, infos) { 225 | 226 | }; 227 | 228 | this.onDecoderReady = function(){}; 229 | 230 | var bufferedCalls = []; 231 | this.decode = function decode(typedAr, parInfo, copyDoneFun) { 232 | bufferedCalls.push([typedAr, parInfo, copyDoneFun]); 233 | }; 234 | 235 | ModuleCallback(function(Module){ 236 | var HEAP8 = Module.HEAP8; 237 | var HEAPU8 = Module.HEAPU8; 238 | var HEAP16 = Module.HEAP16; 239 | var HEAP32 = Module.HEAP32; 240 | // from old constructor 241 | Module._broadwayInit(); 242 | 243 | /** 244 | * Creates a typed array from a HEAP8 pointer. 245 | */ 246 | toU8Array = function(ptr, length) { 247 | return HEAPU8.subarray(ptr, ptr + length); 248 | }; 249 | toU32Array = function(ptr, length) { 250 | //var tmp = HEAPU8.subarray(ptr, ptr + (length * 4)); 251 | return new Uint32Array(HEAPU8.buffer, ptr, length); 252 | }; 253 | instance.streamBuffer = toU8Array(Module._broadwayCreateStream(MAX_STREAM_BUFFER_LENGTH), MAX_STREAM_BUFFER_LENGTH); 254 | instance.pictureBuffers = {}; 255 | // collect extra infos that are provided with the nal units 256 | instance.infoAr = []; 257 | 258 | /** 259 | * Decodes a stream buffer. This may be one single (unframed) NAL unit without the 260 | * start code, or a sequence of NAL units with framing start code prefixes. This 261 | * function overwrites stream buffer allocated by the codec with the supplied buffer. 262 | */ 263 | 264 | var sliceNum = 0; 265 | if (instance.options.sliceMode){ 266 | sliceNum = instance.options.sliceNum; 267 | 268 | instance.decode = function decode(typedAr, parInfo, copyDoneFun) { 269 | instance.infoAr.push(parInfo); 270 | parInfo.startDecoding = nowValue(); 271 | var nals = parInfo.nals; 272 | var i; 273 | if (!nals){ 274 | nals = []; 275 | parInfo.nals = nals; 276 | var l = typedAr.length; 277 | var foundSomething = false; 278 | var lastFound = 0; 279 | var lastStart = 0; 280 | for (i = 0; i < l; ++i){ 281 | if (typedAr[i] === 1){ 282 | if ( 283 | typedAr[i - 1] === 0 && 284 | typedAr[i - 2] === 0 285 | ){ 286 | var startPos = i - 2; 287 | if (typedAr[i - 3] === 0){ 288 | startPos = i - 3; 289 | }; 290 | // its a nal; 291 | if (foundSomething){ 292 | nals.push({ 293 | offset: lastFound, 294 | end: startPos, 295 | type: typedAr[lastStart] & 31 296 | }); 297 | }; 298 | lastFound = startPos; 299 | lastStart = startPos + 3; 300 | if (typedAr[i - 3] === 0){ 301 | lastStart = startPos + 4; 302 | }; 303 | foundSomething = true; 304 | }; 305 | }; 306 | }; 307 | if (foundSomething){ 308 | nals.push({ 309 | offset: lastFound, 310 | end: i, 311 | type: typedAr[lastStart] & 31 312 | }); 313 | }; 314 | }; 315 | 316 | var currentSlice = 0; 317 | var playAr; 318 | var offset = 0; 319 | for (i = 0; i < nals.length; ++i){ 320 | if (nals[i].type === 1 || nals[i].type === 5){ 321 | if (currentSlice === sliceNum){ 322 | playAr = typedAr.subarray(nals[i].offset, nals[i].end); 323 | instance.streamBuffer[offset] = 0; 324 | offset += 1; 325 | instance.streamBuffer.set(playAr, offset); 326 | offset += playAr.length; 327 | }; 328 | currentSlice += 1; 329 | }else{ 330 | playAr = typedAr.subarray(nals[i].offset, nals[i].end); 331 | instance.streamBuffer[offset] = 0; 332 | offset += 1; 333 | instance.streamBuffer.set(playAr, offset); 334 | offset += playAr.length; 335 | Module._broadwayPlayStream(offset); 336 | offset = 0; 337 | }; 338 | }; 339 | copyDoneFun(); 340 | Module._broadwayPlayStream(offset); 341 | }; 342 | 343 | }else{ 344 | instance.decode = function decode(typedAr, parInfo) { 345 | // console.info("Decoding: " + buffer.length); 346 | // collect infos 347 | if (parInfo){ 348 | instance.infoAr.push(parInfo); 349 | parInfo.startDecoding = nowValue(); 350 | }; 351 | 352 | instance.streamBuffer.set(typedAr); 353 | Module._broadwayPlayStream(typedAr.length); 354 | }; 355 | }; 356 | 357 | if (bufferedCalls.length){ 358 | var bi = 0; 359 | for (bi = 0; bi < bufferedCalls.length; ++bi){ 360 | instance.decode(bufferedCalls[bi][0], bufferedCalls[bi][1], bufferedCalls[bi][2]); 361 | }; 362 | bufferedCalls = []; 363 | }; 364 | 365 | instance.onDecoderReady(instance); 366 | 367 | }); 368 | 369 | 370 | }; 371 | 372 | 373 | Decoder.prototype = { 374 | 375 | }; 376 | 377 | 378 | 379 | 380 | /* 381 | 382 | asm.js implementation of a yuv to rgb convertor 383 | provided by @soliton4 384 | 385 | based on 386 | http://www.wordsaretoys.com/2013/10/18/making-yuv-conversion-a-little-faster/ 387 | 388 | */ 389 | 390 | 391 | // factory to create asm.js yuv -> rgb convertor for a given resolution 392 | var asmInstances = {}; 393 | var getAsm = function(parWidth, parHeight){ 394 | var idStr = "" + parWidth + "x" + parHeight; 395 | if (asmInstances[idStr]){ 396 | return asmInstances[idStr]; 397 | }; 398 | 399 | var lumaSize = parWidth * parHeight; 400 | var chromaSize = (lumaSize|0) >> 2; 401 | 402 | var inpSize = lumaSize + chromaSize + chromaSize; 403 | var outSize = parWidth * parHeight * 4; 404 | var cacheSize = Math.pow(2, 24) * 4; 405 | var size = inpSize + outSize + cacheSize; 406 | 407 | var chunkSize = Math.pow(2, 24); 408 | var heapSize = chunkSize; 409 | while (heapSize < size){ 410 | heapSize += chunkSize; 411 | }; 412 | var heap = new ArrayBuffer(heapSize); 413 | 414 | var res = asmFactory(global, {}, heap); 415 | res.init(parWidth, parHeight); 416 | asmInstances[idStr] = res; 417 | 418 | res.heap = heap; 419 | res.out = new Uint8Array(heap, 0, outSize); 420 | res.inp = new Uint8Array(heap, outSize, inpSize); 421 | res.outSize = outSize; 422 | 423 | return res; 424 | }; 425 | 426 | 427 | function asmFactory(stdlib, foreign, heap) { 428 | "use asm"; 429 | 430 | var imul = stdlib.Math.imul; 431 | var min = stdlib.Math.min; 432 | var max = stdlib.Math.max; 433 | var pow = stdlib.Math.pow; 434 | var out = new stdlib.Uint8Array(heap); 435 | var out32 = new stdlib.Uint32Array(heap); 436 | var inp = new stdlib.Uint8Array(heap); 437 | var mem = new stdlib.Uint8Array(heap); 438 | var mem32 = new stdlib.Uint32Array(heap); 439 | 440 | // for double algo 441 | /*var vt = 1.370705; 442 | var gt = 0.698001; 443 | var gt2 = 0.337633; 444 | var bt = 1.732446;*/ 445 | 446 | var width = 0; 447 | var height = 0; 448 | var lumaSize = 0; 449 | var chromaSize = 0; 450 | var inpSize = 0; 451 | var outSize = 0; 452 | 453 | var inpStart = 0; 454 | var outStart = 0; 455 | 456 | var widthFour = 0; 457 | 458 | var cacheStart = 0; 459 | 460 | 461 | function init(parWidth, parHeight){ 462 | parWidth = parWidth|0; 463 | parHeight = parHeight|0; 464 | 465 | var i = 0; 466 | var s = 0; 467 | 468 | width = parWidth; 469 | widthFour = imul(parWidth, 4)|0; 470 | height = parHeight; 471 | lumaSize = imul(width|0, height|0)|0; 472 | chromaSize = (lumaSize|0) >> 2; 473 | outSize = imul(imul(width, height)|0, 4)|0; 474 | inpSize = ((lumaSize + chromaSize)|0 + chromaSize)|0; 475 | 476 | outStart = 0; 477 | inpStart = (outStart + outSize)|0; 478 | cacheStart = (inpStart + inpSize)|0; 479 | 480 | // initializing memory (to be on the safe side) 481 | s = ~~(+pow(+2, +24)); 482 | s = imul(s, 4)|0; 483 | 484 | for (i = 0|0; ((i|0) < (s|0))|0; i = (i + 4)|0){ 485 | mem32[((cacheStart + i)|0) >> 2] = 0; 486 | }; 487 | }; 488 | 489 | function doit(){ 490 | var ystart = 0; 491 | var ustart = 0; 492 | var vstart = 0; 493 | 494 | var y = 0; 495 | var yn = 0; 496 | var u = 0; 497 | var v = 0; 498 | 499 | var o = 0; 500 | 501 | var line = 0; 502 | var col = 0; 503 | 504 | var usave = 0; 505 | var vsave = 0; 506 | 507 | var ostart = 0; 508 | var cacheAdr = 0; 509 | 510 | ostart = outStart|0; 511 | 512 | ystart = inpStart|0; 513 | ustart = (ystart + lumaSize|0)|0; 514 | vstart = (ustart + chromaSize)|0; 515 | 516 | for (line = 0; (line|0) < (height|0); line = (line + 2)|0){ 517 | usave = ustart; 518 | vsave = vstart; 519 | for (col = 0; (col|0) < (width|0); col = (col + 2)|0){ 520 | y = inp[ystart >> 0]|0; 521 | yn = inp[((ystart + width)|0) >> 0]|0; 522 | 523 | u = inp[ustart >> 0]|0; 524 | v = inp[vstart >> 0]|0; 525 | 526 | cacheAdr = (((((y << 16)|0) + ((u << 8)|0))|0) + v)|0; 527 | o = mem32[((cacheStart + cacheAdr)|0) >> 2]|0; 528 | if (o){}else{ 529 | o = yuv2rgbcalc(y,u,v)|0; 530 | mem32[((cacheStart + cacheAdr)|0) >> 2] = o|0; 531 | }; 532 | mem32[ostart >> 2] = o; 533 | 534 | cacheAdr = (((((yn << 16)|0) + ((u << 8)|0))|0) + v)|0; 535 | o = mem32[((cacheStart + cacheAdr)|0) >> 2]|0; 536 | if (o){}else{ 537 | o = yuv2rgbcalc(yn,u,v)|0; 538 | mem32[((cacheStart + cacheAdr)|0) >> 2] = o|0; 539 | }; 540 | mem32[((ostart + widthFour)|0) >> 2] = o; 541 | 542 | //yuv2rgb5(y, u, v, ostart); 543 | //yuv2rgb5(yn, u, v, (ostart + widthFour)|0); 544 | ostart = (ostart + 4)|0; 545 | 546 | // next step only for y. u and v stay the same 547 | ystart = (ystart + 1)|0; 548 | y = inp[ystart >> 0]|0; 549 | yn = inp[((ystart + width)|0) >> 0]|0; 550 | 551 | //yuv2rgb5(y, u, v, ostart); 552 | cacheAdr = (((((y << 16)|0) + ((u << 8)|0))|0) + v)|0; 553 | o = mem32[((cacheStart + cacheAdr)|0) >> 2]|0; 554 | if (o){}else{ 555 | o = yuv2rgbcalc(y,u,v)|0; 556 | mem32[((cacheStart + cacheAdr)|0) >> 2] = o|0; 557 | }; 558 | mem32[ostart >> 2] = o; 559 | 560 | //yuv2rgb5(yn, u, v, (ostart + widthFour)|0); 561 | cacheAdr = (((((yn << 16)|0) + ((u << 8)|0))|0) + v)|0; 562 | o = mem32[((cacheStart + cacheAdr)|0) >> 2]|0; 563 | if (o){}else{ 564 | o = yuv2rgbcalc(yn,u,v)|0; 565 | mem32[((cacheStart + cacheAdr)|0) >> 2] = o|0; 566 | }; 567 | mem32[((ostart + widthFour)|0) >> 2] = o; 568 | ostart = (ostart + 4)|0; 569 | 570 | //all positions inc 1 571 | 572 | ystart = (ystart + 1)|0; 573 | ustart = (ustart + 1)|0; 574 | vstart = (vstart + 1)|0; 575 | }; 576 | ostart = (ostart + widthFour)|0; 577 | ystart = (ystart + width)|0; 578 | 579 | }; 580 | 581 | }; 582 | 583 | function yuv2rgbcalc(y, u, v){ 584 | y = y|0; 585 | u = u|0; 586 | v = v|0; 587 | 588 | var r = 0; 589 | var g = 0; 590 | var b = 0; 591 | 592 | var o = 0; 593 | 594 | var a0 = 0; 595 | var a1 = 0; 596 | var a2 = 0; 597 | var a3 = 0; 598 | var a4 = 0; 599 | 600 | a0 = imul(1192, (y - 16)|0)|0; 601 | a1 = imul(1634, (v - 128)|0)|0; 602 | a2 = imul(832, (v - 128)|0)|0; 603 | a3 = imul(400, (u - 128)|0)|0; 604 | a4 = imul(2066, (u - 128)|0)|0; 605 | 606 | r = (((a0 + a1)|0) >> 10)|0; 607 | g = (((((a0 - a2)|0) - a3)|0) >> 10)|0; 608 | b = (((a0 + a4)|0) >> 10)|0; 609 | 610 | if ((((r & 255)|0) != (r|0))|0){ 611 | r = min(255, max(0, r|0)|0)|0; 612 | }; 613 | if ((((g & 255)|0) != (g|0))|0){ 614 | g = min(255, max(0, g|0)|0)|0; 615 | }; 616 | if ((((b & 255)|0) != (b|0))|0){ 617 | b = min(255, max(0, b|0)|0)|0; 618 | }; 619 | 620 | o = 255; 621 | o = (o << 8)|0; 622 | o = (o + b)|0; 623 | o = (o << 8)|0; 624 | o = (o + g)|0; 625 | o = (o << 8)|0; 626 | o = (o + r)|0; 627 | 628 | return o|0; 629 | 630 | }; 631 | 632 | 633 | 634 | return { 635 | init: init, 636 | doit: doit 637 | }; 638 | }; 639 | 640 | 641 | /* 642 | potential worker initialization 643 | 644 | */ 645 | 646 | 647 | if (typeof self != "undefined"){ 648 | var isWorker = false; 649 | var decoder; 650 | var reuseMemory = false; 651 | var sliceMode = false; 652 | var sliceNum = 0; 653 | var sliceCnt = 0; 654 | var lastSliceNum = 0; 655 | var sliceInfoAr; 656 | var lastBuf; 657 | var awaiting = 0; 658 | var pile = []; 659 | var startDecoding; 660 | var finishDecoding; 661 | var timeDecoding; 662 | 663 | var memAr = []; 664 | var getMem = function(length){ 665 | if (memAr.length){ 666 | var u = memAr.shift(); 667 | while (u && u.byteLength !== length){ 668 | u = memAr.shift(); 669 | }; 670 | if (u){ 671 | return u; 672 | }; 673 | }; 674 | return new ArrayBuffer(length); 675 | }; 676 | 677 | var copySlice = function(source, target, infoAr, width, height){ 678 | 679 | var length = width * height; 680 | var length4 = length / 4 681 | var plane2 = length; 682 | var plane3 = length + length4; 683 | 684 | var copy16 = function(parBegin, parEnd){ 685 | var i = 0; 686 | for (i = 0; i < 16; ++i){ 687 | var begin = parBegin + (width * i); 688 | var end = parEnd + (width * i) 689 | target.set(source.subarray(begin, end), begin); 690 | }; 691 | }; 692 | var copy8 = function(parBegin, parEnd){ 693 | var i = 0; 694 | for (i = 0; i < 8; ++i){ 695 | var begin = parBegin + ((width / 2) * i); 696 | var end = parEnd + ((width / 2) * i) 697 | target.set(source.subarray(begin, end), begin); 698 | }; 699 | }; 700 | var copyChunk = function(begin, end){ 701 | target.set(source.subarray(begin, end), begin); 702 | }; 703 | 704 | var begin = infoAr[0]; 705 | var end = infoAr[1]; 706 | if (end > 0){ 707 | copy16(begin, end); 708 | copy8(infoAr[2], infoAr[3]); 709 | copy8(infoAr[4], infoAr[5]); 710 | }; 711 | begin = infoAr[6]; 712 | end = infoAr[7]; 713 | if (end > 0){ 714 | copy16(begin, end); 715 | copy8(infoAr[8], infoAr[9]); 716 | copy8(infoAr[10], infoAr[11]); 717 | }; 718 | 719 | begin = infoAr[12]; 720 | end = infoAr[15]; 721 | if (end > 0){ 722 | copyChunk(begin, end); 723 | copyChunk(infoAr[13], infoAr[16]); 724 | copyChunk(infoAr[14], infoAr[17]); 725 | }; 726 | 727 | }; 728 | 729 | var sliceMsgFun = function(){}; 730 | 731 | var setSliceCnt = function(parSliceCnt){ 732 | sliceCnt = parSliceCnt; 733 | lastSliceNum = sliceCnt - 1; 734 | }; 735 | 736 | 737 | self.addEventListener('message', function(e) { 738 | 739 | if (isWorker){ 740 | if (reuseMemory){ 741 | if (e.data.reuse){ 742 | memAr.push(e.data.reuse); 743 | }; 744 | }; 745 | if (e.data.buf){ 746 | if (sliceMode && awaiting !== 0){ 747 | pile.push(e.data); 748 | }else{ 749 | decoder.decode( 750 | new Uint8Array(e.data.buf, e.data.offset || 0, e.data.length), 751 | e.data.info, 752 | function(){ 753 | if (sliceMode && sliceNum !== lastSliceNum){ 754 | postMessage(e.data, [e.data.buf]); 755 | }; 756 | } 757 | ); 758 | }; 759 | return; 760 | }; 761 | 762 | if (e.data.slice){ 763 | // update ref pic 764 | var copyStart = nowValue(); 765 | copySlice(new Uint8Array(e.data.slice), lastBuf, e.data.infos[0].sliceInfoAr, e.data.width, e.data.height); 766 | // is it the one? then we need to update it 767 | if (e.data.theOne){ 768 | copySlice(lastBuf, new Uint8Array(e.data.slice), sliceInfoAr, e.data.width, e.data.height); 769 | if (timeDecoding > e.data.infos[0].timeDecoding){ 770 | e.data.infos[0].timeDecoding = timeDecoding; 771 | }; 772 | e.data.infos[0].timeCopy += (nowValue() - copyStart); 773 | }; 774 | // move on 775 | postMessage(e.data, [e.data.slice]); 776 | 777 | // next frame in the pipe? 778 | awaiting -= 1; 779 | if (awaiting === 0 && pile.length){ 780 | var data = pile.shift(); 781 | decoder.decode( 782 | new Uint8Array(data.buf, data.offset || 0, data.length), 783 | data.info, 784 | function(){ 785 | if (sliceMode && sliceNum !== lastSliceNum){ 786 | postMessage(data, [data.buf]); 787 | }; 788 | } 789 | ); 790 | }; 791 | return; 792 | }; 793 | 794 | if (e.data.setSliceCnt){ 795 | setSliceCnt(e.data.sliceCnt); 796 | return; 797 | }; 798 | 799 | }else{ 800 | if (e.data && e.data.type === "Broadway.js - Worker init"){ 801 | isWorker = true; 802 | decoder = new Decoder(e.data.options); 803 | 804 | if (e.data.options.sliceMode){ 805 | reuseMemory = true; 806 | sliceMode = true; 807 | sliceNum = e.data.options.sliceNum; 808 | setSliceCnt(e.data.options.sliceCnt); 809 | 810 | decoder.onPictureDecoded = function (buffer, width, height, infos) { 811 | 812 | // buffer needs to be copied because we give up ownership 813 | var copyU8 = new Uint8Array(getMem(buffer.length)); 814 | copySlice(buffer, copyU8, infos[0].sliceInfoAr, width, height); 815 | 816 | startDecoding = infos[0].startDecoding; 817 | finishDecoding = infos[0].finishDecoding; 818 | timeDecoding = finishDecoding - startDecoding; 819 | infos[0].timeDecoding = timeDecoding; 820 | infos[0].timeCopy = 0; 821 | 822 | postMessage({ 823 | slice: copyU8.buffer, 824 | sliceNum: sliceNum, 825 | width: width, 826 | height: height, 827 | infos: infos 828 | }, [copyU8.buffer]); // 2nd parameter is used to indicate transfer of ownership 829 | 830 | awaiting = sliceCnt - 1; 831 | 832 | lastBuf = buffer; 833 | sliceInfoAr = infos[0].sliceInfoAr; 834 | 835 | }; 836 | 837 | }else if (e.data.options.reuseMemory){ 838 | reuseMemory = true; 839 | decoder.onPictureDecoded = function (buffer, width, height, infos) { 840 | 841 | // buffer needs to be copied because we give up ownership 842 | var copyU8 = new Uint8Array(getMem(buffer.length)); 843 | copyU8.set( buffer, 0, buffer.length ); 844 | 845 | postMessage({ 846 | buf: copyU8.buffer, 847 | length: buffer.length, 848 | width: width, 849 | height: height, 850 | infos: infos 851 | }, [copyU8.buffer]); // 2nd parameter is used to indicate transfer of ownership 852 | 853 | }; 854 | 855 | }else{ 856 | decoder.onPictureDecoded = function (buffer, width, height, infos) { 857 | if (buffer) { 858 | buffer = new Uint8Array(buffer); 859 | }; 860 | 861 | // buffer needs to be copied because we give up ownership 862 | var copyU8 = new Uint8Array(buffer.length); 863 | copyU8.set( buffer, 0, buffer.length ); 864 | 865 | postMessage({ 866 | buf: copyU8.buffer, 867 | length: buffer.length, 868 | width: width, 869 | height: height, 870 | infos: infos 871 | }, [copyU8.buffer]); // 2nd parameter is used to indicate transfer of ownership 872 | 873 | }; 874 | }; 875 | postMessage({ consoleLog: "broadway worker initialized" }); 876 | }; 877 | }; 878 | 879 | 880 | }, false); 881 | }; 882 | 883 | Decoder.nowValue = nowValue; 884 | 885 | return Decoder; 886 | 887 | })(); 888 | 889 | 890 | })); 891 | 892 | --------------------------------------------------------------------------------