├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── caspar_stack.png ├── lib ├── AMCP │ ├── basic.d.ts │ ├── basic.js │ ├── cmdResponses.d.ts │ ├── cmdResponses.js │ ├── commands.d.ts │ ├── commands.js │ ├── server.d.ts │ ├── server.js │ ├── testResponses.d.ts │ └── testResponses.js ├── chanLayer.d.ts ├── chanLayer.js ├── channel.d.ts ├── channel.js ├── consumer │ ├── consumer.d.ts │ ├── consumer.js │ ├── macadamConsumer.d.ts │ └── macadamConsumer.js ├── index.d.ts ├── index.js ├── process │ ├── bgra8.d.ts │ ├── bgra8.js │ ├── colourMaths.d.ts │ ├── colourMaths.js │ ├── combine.d.ts │ ├── combine.js │ ├── imageProcess.d.ts │ ├── imageProcess.js │ ├── io.d.ts │ ├── io.js │ ├── loadSave.d.ts │ ├── loadSave.js │ ├── mix.d.ts │ ├── mix.js │ ├── packer.d.ts │ ├── packer.js │ ├── resize.d.ts │ ├── resize.js │ ├── rgba8.d.ts │ ├── rgba8.js │ ├── switch.d.ts │ ├── switch.js │ ├── transform.d.ts │ ├── transform.js │ ├── v210.d.ts │ ├── v210.js │ ├── wipe.d.ts │ ├── wipe.js │ ├── yuv422p10.d.ts │ ├── yuv422p10.js │ ├── yuv422p8.d.ts │ └── yuv422p8.js ├── producer │ ├── ffmpegProducer.d.ts │ ├── ffmpegProducer.js │ ├── producer.d.ts │ └── producer.js ├── testing.d.ts └── testing.js ├── package.json ├── scratch ├── elecular_clunker3.js ├── high_beam.js ├── oscServer.js ├── promise_delay.js ├── rgba8-bgra8Test.js ├── v210ImageTest.js ├── v210Test.js ├── yuv422p10-bgra8Test.js ├── yuv422p10-v210Test.js └── yuv422p10Test.js ├── src ├── AMCP │ ├── basic.ts │ ├── cmdResponses.ts │ ├── commands.ts │ ├── server.ts │ └── testResponses.ts ├── chanLayer.ts ├── channel.ts ├── consumer │ ├── consumer.ts │ └── macadamConsumer.ts ├── index.ts ├── process │ ├── bgra8.ts │ ├── colourMaths.ts │ ├── combine.ts │ ├── imageProcess.ts │ ├── io.ts │ ├── loadSave.ts │ ├── mix.ts │ ├── packer.ts │ ├── resize.ts │ ├── rgba8.ts │ ├── switch.ts │ ├── transform.ts │ ├── v210.ts │ ├── wipe.ts │ ├── yuv422p10.ts │ └── yuv422p8.ts ├── producer │ ├── ffmpegProducer.ts │ └── producer.ts └── testing.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = false 9 | end_of_line = lf -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | lib -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "plugin:prettier/recommended"], 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "prettier/prettier": "error" 6 | }, 7 | "env": { 8 | "es2020": true, 9 | "node": true, 10 | "browser": true 11 | }, 12 | "parserOptions": { "sourceType": "module" }, 13 | "overrides": [ 14 | { 15 | "files": ["*.ts"], 16 | "parser": "@typescript-eslint/parser", 17 | "parserOptions": { "project": "./tsconfig.json" }, 18 | "plugins": ["@typescript-eslint"], 19 | "extends": [ 20 | "eslint:recommended", 21 | "plugin:@typescript-eslint/eslint-recommended", 22 | "plugin:@typescript-eslint/recommended", 23 | "prettier/@typescript-eslint" 24 | ] 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.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 | # Typescript dist folder 64 | dist 65 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "trailingComma": "none", 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "useTabs": true, 8 | "endOfLine": "lf", 9 | "semi": false 10 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CasparCL 2 | 3 | From the perspective of 2020 tech rather than 2000 tech, this is an experimentat to see what happens if the popular open-source graphics engine CasparCG is re-cast using different tools and architecture. This aim is to answer the following questions: 4 | 5 | * Can you implement 80%+ of the features using GPGPU techniques, decoupling the work done from limitations of OpenGL texture formats? 6 | * Can you make something like CasparCG that is both very low-latency (aiming for less than 2 frames end-to-end on current hardware) and HDR ready (10-bit plus, floating point, BT709 and BT2020)? 7 | * Can you open up the data model to be more flexible and less coupled to SDI? 8 | 9 | Using Node.js as the glue platform, the plan is mash up some existing technical components and see what happens: 10 | 11 | * Beamcoder native bindings to FFmpeg for demuxing, decoding, encoding and muxing. Possibly for access to some devices as well. 12 | * NodenCL - an experiment in using OpenCL from Node for mixing and graphics rendering with floating point values. 13 | * Highland.js to provide reactive streams over the previous two Promise-based libraries. 14 | * CasparCG code and components where appropriate, e.g. CEF for HTML rendering, following how the shaders are used, flash rendering. 15 | * SuperFly.tv CasparCG Connection library - a controlled way of getting from Node to Caspar. 16 | 17 | Experiments don't necessarily pan out. Resource is limited. Don't expect miracles. Watch this space! 18 | 19 | ## Updates 20 | 21 | ### 3rd November 2019 22 | 23 | Progress has been made since we set out on this project. See the video of a vision mixer built on this stack from the [EBU Open Source event 2019](https://tech.ebu.ch/home/publications/main/section-publication-main/section-publication-main/publicationList/2019/09/24/streampunk-beamcoder.html). 24 | 25 | * NodenCL working well on Nvidia and Intel GPUs ... typically significatly less than 25ms per frame on the GPU. Work is ongoing to optimise for AMD. 26 | * 10-bit YUV planar support added to NodenCL for integration with Beamcoder. 27 | * sRGB colourspace support added in and out with support for raw 8-bit RGBA and BGRA. 28 | * [Elecular](https://github.com/Streampunk/elecular) project uses Electron as a headless graphics renderer for [Singular.live](https://www.singular.live/) graphics and Chrome browser as a previewer with HTTP interfaces. 29 | * Basic compositing of graphics demonstrated ... compositing preserves 10-bit pictures end-to-end, even though the graphics are 8-bit. 30 | * Highland not working well with Promises - failing to overlap async processing. A different approach is required ... 31 | 32 | Current work includes: 33 | 34 | * Writing a library for Node like Highland that works well with promises over media streams. This is a reworking of the [Redioactive](https://github.com/Streampunk/node-red-contrib-dynamorse-core/blob/master/util/Redioactive.js) component inside [dynamorse](https://github.com/Streampunk/node-red-contrib-dynamorse-core). 35 | * Multi-layer compositing. 36 | * AMD GPU optimisations. 37 | 38 | Next steps: 39 | 40 | * Quick and dirty _factor of 2_ scaling up and down. 41 | * Mixer operations similar to those from CasparCG - including arbitrary scaling 42 | * Bolting AMCP on the front of the stack. 43 | * HTTP/S optimisations based around [arachnid](https://github.com/Streampunk/arachnid). 44 | 45 | ### 3rd February 2020 46 | 47 | In the last 3 months: 48 | 49 | * NodenCL has been improved using pipelining. By defining a local processing graph, each of copy to the GPU, copy from the GPU and processing can be overlapped to run in parallel. Working well on AMD and nVidia GPUs. 50 | * Typescript definitions now available for NodenCL and Beamcoder 51 | * Realtime quarter-size previews in a browser web canvas. Uncompressed with correct sRGB colors. 52 | * Prototype mixing and compositing functions, including arbitrary scaling, flipping, positioning. 10-bit video colour on input is preserved through the composite to the output. 53 | * Checking everything is working on Node 12. 54 | * Redioactive is working. Some further development required to add all higher-order features including splitting and joining streams. 55 | 56 | Still to come: 57 | 58 | * Bolting AMCP-_lite_ on the front of the stack. 59 | * Productising CasparCL - a MVP towards a release. 60 | 61 | ![CasparCL stack](/caspar_stack.png) 62 | 63 | ### 18th May 2020 64 | 65 | CasparCL will be renamed _Phaneron_ - based on the Greek for _visible, showable_ - a tool for the contruction of audio/visiual phenomenon. This will be released as both a video server with AMCP-lite controls and a kits of parts than can be used to assemble clustered video server / mixer / graphics technology. Please bear with us while we restructure this project into [something phenomenal](https://github.com/Streampunk/phaneron). 66 | 67 | We are still aiming for UHD/HDR support, open-source, low-latency, Node-based etc. etc.! 68 | 69 | In the meantime, technical developments have included: 70 | 71 | * Typescript definitions for Macadam and BlackMagic consumers and producers. 72 | * Further developments to the uncompressed RGBA HTML producer for low-latency display of streams in browsers. 73 | * AMCP controlling video playback through Redioactive. 74 | 75 | Next up: 76 | 77 | * More AMCP support, including mixer functions. 78 | * A simple pipeline for audio. 79 | * Investigation of how to best support `PLAY [HTML]` 80 | * Investigation of de-interlacing filters running on the GPU through OpenCL. 81 | * Clustering capability support in [Redioactive](https://github.com/Streampunk/redioactive). 82 | 83 | This will be the last update here. Look out for announcements 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /caspar_stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Streampunk/casparcl/95d6ab07add1ffef2fb5da891fe67f19ddf941f9/caspar_stack.png -------------------------------------------------------------------------------- /lib/AMCP/basic.d.ts: -------------------------------------------------------------------------------- 1 | import { clContext as nodenCLContext } from 'nodencl'; 2 | import { Commands } from './commands'; 3 | import { ChanLayer } from '../chanLayer'; 4 | export declare class Basic { 5 | private readonly channels; 6 | constructor(clContext: nodenCLContext); 7 | /** Add the supported basic transport commands */ 8 | addCmds(commands: Commands): void; 9 | /** 10 | * Loads a producer in the background and prepares it for playout. If no layer is specified the default layer index will be used. 11 | * 12 | * _clip_ will be parsed by available registered producer factories. If a successfully match is found, the producer will be loaded into the background. 13 | * If a file with the same name (extension excluded) but with the additional postfix _a is found this file will be used as key for the main clip. 14 | * 15 | * _loop_ will cause the clip to loop. 16 | * When playing and looping the clip will start at _frame_. 17 | * When playing and loop the clip will end after _frames_ number of frames. 18 | * 19 | * _auto_ will cause the clip to automatically start when foreground clip has ended (without play). 20 | * The clip is considered "started" after the optional transition has ended. 21 | * 22 | * Note: only one clip can be queued to play automatically per layer. 23 | */ 24 | loadbg(chanLay: ChanLayer, params: string[]): Promise; 25 | /** 26 | * Loads a clip to the foreground and plays the first frame before pausing. 27 | * If any clip is playing on the target foreground then this clip will be replaced. 28 | */ 29 | load(chanLay: ChanLayer, params: string[]): Promise; 30 | /** 31 | * Moves clip from background to foreground and starts playing it. 32 | * If a transition (see LOADBG) is prepared, it will be executed. 33 | * If additional parameters (see LOADBG) are provided then the provided clip will first be loaded to the background. 34 | */ 35 | play(chanLay: ChanLayer, params: string[]): Promise; 36 | /** Pauses playback of the foreground clip on the specified layer. The RESUME command can be used to resume playback again. */ 37 | pause(chanLay: ChanLayer, params: string[]): Promise; 38 | /** Resumes playback of a foreground clip previously paused with the PAUSE command. */ 39 | resume(chanLay: ChanLayer, params: string[]): Promise; 40 | /** Removes the foreground clip of the specified layer */ 41 | stop(chanLay: ChanLayer, params: string[]): Promise; 42 | /** 43 | * Removes all clips (both foreground and background) of the specified layer. 44 | * If no layer is specified then all layers in the specified video_channel are cleared. 45 | */ 46 | clear(chanLay: ChanLayer, params: string[]): Promise; 47 | } 48 | -------------------------------------------------------------------------------- /lib/AMCP/basic.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* Copyright 2020 Streampunk Media Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | Object.defineProperty(exports, "__esModule", { value: true }); 17 | const channel_1 = require("../channel"); 18 | class Basic { 19 | constructor(clContext) { 20 | this.channels = Array.from([1, 2, 3, 4], (c) => new channel_1.Channel(clContext, c)); 21 | } 22 | /** Add the supported basic transport commands */ 23 | addCmds(commands) { 24 | commands.add({ cmd: 'LOADBG', fn: this.loadbg.bind(this) }); 25 | commands.add({ cmd: 'LOAD', fn: this.load.bind(this) }); 26 | commands.add({ cmd: 'PLAY', fn: this.play.bind(this) }); 27 | commands.add({ cmd: 'PAUSE', fn: this.pause.bind(this) }); 28 | commands.add({ cmd: 'RESUME', fn: this.resume.bind(this) }); 29 | commands.add({ cmd: 'STOP', fn: this.stop.bind(this) }); 30 | commands.add({ cmd: 'CLEAR', fn: this.clear.bind(this) }); 31 | } 32 | /** 33 | * Loads a producer in the background and prepares it for playout. If no layer is specified the default layer index will be used. 34 | * 35 | * _clip_ will be parsed by available registered producer factories. If a successfully match is found, the producer will be loaded into the background. 36 | * If a file with the same name (extension excluded) but with the additional postfix _a is found this file will be used as key for the main clip. 37 | * 38 | * _loop_ will cause the clip to loop. 39 | * When playing and looping the clip will start at _frame_. 40 | * When playing and loop the clip will end after _frames_ number of frames. 41 | * 42 | * _auto_ will cause the clip to automatically start when foreground clip has ended (without play). 43 | * The clip is considered "started" after the optional transition has ended. 44 | * 45 | * Note: only one clip can be queued to play automatically per layer. 46 | */ 47 | async loadbg(chanLay, params) { 48 | if (!chanLay.valid) 49 | return Promise.resolve(false); 50 | let curParam = 0; 51 | const clip = params[curParam++]; 52 | const loop = params.find((param) => param === 'LOOP') !== undefined; 53 | const autoPlay = params.find((param) => param === 'AUTO') !== undefined; 54 | console.log(`loadbg: clip '${clip}', loop ${loop}, auto play ${autoPlay}`); 55 | const bgOK = this.channels[chanLay.channel - 1].createSource(chanLay, params); 56 | return bgOK; 57 | } 58 | /** 59 | * Loads a clip to the foreground and plays the first frame before pausing. 60 | * If any clip is playing on the target foreground then this clip will be replaced. 61 | */ 62 | async load(chanLay, params) { 63 | if (!chanLay.valid) 64 | return Promise.resolve(false); 65 | const bgOK = this.channels[chanLay.channel - 1].createSource(chanLay, params); 66 | return bgOK; 67 | } 68 | /** 69 | * Moves clip from background to foreground and starts playing it. 70 | * If a transition (see LOADBG) is prepared, it will be executed. 71 | * If additional parameters (see LOADBG) are provided then the provided clip will first be loaded to the background. 72 | */ 73 | async play(chanLay, params) { 74 | if (!chanLay.valid) 75 | return Promise.resolve(false); 76 | if (params.length !== 0) 77 | await this.loadbg(chanLay, params); 78 | const fgOK = this.channels[chanLay.channel - 1].play(); 79 | return fgOK; 80 | } 81 | /** Pauses playback of the foreground clip on the specified layer. The RESUME command can be used to resume playback again. */ 82 | async pause(chanLay, params) { 83 | console.log('pause', params); 84 | return chanLay.valid; 85 | } 86 | /** Resumes playback of a foreground clip previously paused with the PAUSE command. */ 87 | async resume(chanLay, params) { 88 | console.log('resume', params); 89 | return chanLay.valid; 90 | } 91 | /** Removes the foreground clip of the specified layer */ 92 | async stop(chanLay, params) { 93 | console.log('stop', params); 94 | return chanLay.valid; 95 | } 96 | /** 97 | * Removes all clips (both foreground and background) of the specified layer. 98 | * If no layer is specified then all layers in the specified video_channel are cleared. 99 | */ 100 | async clear(chanLay, params) { 101 | console.log('clear', params); 102 | return chanLay.valid; 103 | } 104 | } 105 | exports.Basic = Basic; 106 | -------------------------------------------------------------------------------- /lib/AMCP/cmdResponses.d.ts: -------------------------------------------------------------------------------- 1 | export interface Responses { 2 | [command: string]: ((req: string[] | null) => string) | Responses; 3 | } 4 | export declare const responses218: Responses; 5 | export declare const responses207: Responses; 6 | export declare const responses220: Responses; 7 | -------------------------------------------------------------------------------- /lib/AMCP/commands.d.ts: -------------------------------------------------------------------------------- 1 | import { ChanLayer } from '../chanLayer'; 2 | interface CmdEntry { 3 | cmd: string; 4 | fn: (chanLayer: ChanLayer, params: string[]) => Promise; 5 | } 6 | export declare class Commands { 7 | private readonly map; 8 | constructor(); 9 | add(entry: CmdEntry): void; 10 | process(command: string[]): Promise; 11 | } 12 | export {}; 13 | -------------------------------------------------------------------------------- /lib/AMCP/commands.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* Copyright 2020 Streampunk Media Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | Object.defineProperty(exports, "__esModule", { value: true }); 17 | function chanLayerFromString(chanLayStr) { 18 | let valid = false; 19 | let channel = 0; 20 | let layer = 0; 21 | const match = chanLayStr === null || chanLayStr === void 0 ? void 0 : chanLayStr.match('(?\\d+)-?(?\\d*)'); 22 | if (match === null || match === void 0 ? void 0 : match.groups) { 23 | valid = true; 24 | const chanLay = match.groups; 25 | channel = parseInt(chanLay.channel); 26 | if (chanLay.layer !== '') { 27 | layer = parseInt(chanLay.layer); 28 | } 29 | } 30 | return { valid: valid, channel: channel, layer: layer }; 31 | } 32 | class Commands { 33 | constructor() { 34 | this.map = []; 35 | } 36 | add(entry) { 37 | this.map.push(entry); 38 | } 39 | async process(command) { 40 | let result = false; 41 | const entry = this.map.find(({ cmd }) => cmd === command[0]); 42 | if (entry) { 43 | const chanLayer = chanLayerFromString(command[1]); 44 | result = await entry.fn(chanLayer, command.slice(chanLayer ? 2 : 1)); 45 | } 46 | return result; 47 | } 48 | } 49 | exports.Commands = Commands; 50 | -------------------------------------------------------------------------------- /lib/AMCP/server.d.ts: -------------------------------------------------------------------------------- 1 | import { Commands } from './commands'; 2 | export declare function processCommand(command: string[] | null, token?: string): string; 3 | export declare function start(commands?: Commands): Promise; 4 | export declare function stop(): Promise; 5 | export declare function version(version: string): void; 6 | -------------------------------------------------------------------------------- /lib/AMCP/server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* Copyright 2020 Streampunk Media Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | var __importStar = (this && this.__importStar) || function (mod) { 17 | if (mod && mod.__esModule) return mod; 18 | var result = {}; 19 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; 20 | result["default"] = mod; 21 | return result; 22 | }; 23 | Object.defineProperty(exports, "__esModule", { value: true }); 24 | const net = __importStar(require("net")); 25 | const cmdResponses_1 = require("./cmdResponses"); 26 | let cmds; 27 | let ccgResponses = cmdResponses_1.responses218; 28 | function processCommand(command, token = '') { 29 | if (!command) { 30 | return '400 ERROR'; 31 | } 32 | if (command[0] === 'REQ') { 33 | if (command[2] !== 'PING') { 34 | return processCommand(command.slice(2), command[1]); 35 | } 36 | else { 37 | token = command[1]; 38 | } 39 | } 40 | if (command[0] === 'SWITCH') { 41 | if (command[1] === '207') { 42 | ccgResponses = cmdResponses_1.responses207; 43 | return '202 SWITCH 207 OK'; 44 | } 45 | if (command[1] === '218') { 46 | ccgResponses = cmdResponses_1.responses218; 47 | return '202 SWITCH 218 OK'; 48 | } 49 | if (command[1] === '220') { 50 | ccgResponses = cmdResponses_1.responses220; 51 | return '202 SWITCH 220 OK'; 52 | } 53 | return '400 SWITCH ERROR'; 54 | } 55 | if (command[0] === 'BYE') { 56 | return '***BYE***'; 57 | } 58 | if (ccgResponses[command[0]]) { 59 | if (!(cmds === null || cmds === void 0 ? void 0 : cmds.process(command))) { 60 | return `400 ERROR\r\n${command.join(' ')} NOT IMPLEMENTED`; 61 | } 62 | const responseFn = ccgResponses[command[0]]; 63 | let response = null; 64 | if (typeof responseFn === 'function') { 65 | response = responseFn(command); 66 | } 67 | else { 68 | if (responseFn.none && command.length === 1) { 69 | response = responseFn.none(command); 70 | } 71 | else if (responseFn.number && command.length >= 2) { 72 | response = responseFn.number(command); 73 | } 74 | else if (responseFn.layer && command.length >= 3) { 75 | response = responseFn.layer[command[2]](command); 76 | } 77 | else if (command.length >= 2 && responseFn[command[1]]) { 78 | response = responseFn[command[1]](command); 79 | } 80 | if (response === null && responseFn.string && command.length >= 2) { 81 | response = responseFn.string(command); 82 | } 83 | } 84 | if (response) 85 | return token ? `RES ${token} ${response}` : response; 86 | } 87 | return token 88 | ? `RES ${token} 400 ERROR\r\n${command.join(' ')}` 89 | : `400 ERROR\r\n${command.join(' ')}`; 90 | } 91 | exports.processCommand = processCommand; 92 | const server = net.createServer((c) => { 93 | console.log('client connected'); 94 | c.on('end', () => { 95 | console.log('client disconnected'); 96 | }); 97 | }); 98 | server.on('error', (err) => { 99 | throw err; 100 | }); 101 | async function start(commands) { 102 | if (commands) 103 | cmds = commands; 104 | return new Promise((resolve, reject) => { 105 | let resolved = false; 106 | server.once('error', (e) => { 107 | if (!resolved) 108 | reject(e); 109 | }); 110 | server.listen(5250, () => { 111 | resolved = true; 112 | resolve('CasparCL server AMCP protocol running on port 5250'); 113 | }); 114 | }); 115 | } 116 | exports.start = start; 117 | async function stop() { 118 | return new Promise((resolve, reject) => { 119 | let resolved = false; 120 | server.once('error', (err) => { 121 | if (!resolved) 122 | reject(err); 123 | }); 124 | server.close((e) => { 125 | if (e) 126 | return reject(e); 127 | resolved = true; 128 | resolve('CasparCL server closed'); 129 | }); 130 | }); 131 | } 132 | exports.stop = stop; 133 | server.on('listening', () => { 134 | // console.log('CasparCL server AMCP protocol running on port 5250') 135 | }); 136 | server.on('connection', (sock) => { 137 | let chunk = ''; 138 | sock.on('data', (input) => { 139 | chunk += input.toString(); 140 | let eol = chunk.indexOf('\r\n'); 141 | while (eol > -1) { 142 | const command = chunk.substring(0, eol); 143 | console.log(command); 144 | const result = processCommand(command.toUpperCase().match(/"[^"]+"|""|\S+/g)); 145 | if (result === '***BYE***') { 146 | sock.destroy(); 147 | break; 148 | } 149 | sock.write(result.toString() + '\r\n'); 150 | console.log(result); 151 | if (result === '202 KILL OK') { 152 | sock.destroy(); 153 | stop().catch(console.error); 154 | break; 155 | } 156 | chunk = chunk.substring(eol + 2); 157 | eol = chunk.indexOf('\r\n'); 158 | } 159 | }); 160 | sock.on('error', console.error); 161 | sock.on('close', () => { 162 | console.log('client disconnect'); 163 | }); 164 | }); 165 | function version(version) { 166 | if (version === '207') { 167 | ccgResponses = cmdResponses_1.responses207; 168 | } 169 | if (version === '218') { 170 | ccgResponses = cmdResponses_1.responses218; 171 | } 172 | if (version === '220') { 173 | ccgResponses = cmdResponses_1.responses220; 174 | } 175 | } 176 | exports.version = version; 177 | if (!module.parent) { 178 | start().then(console.log, console.error); 179 | } 180 | -------------------------------------------------------------------------------- /lib/AMCP/testResponses.d.ts: -------------------------------------------------------------------------------- 1 | export declare const clsResponse218: string; 2 | export declare const clsResponse207: string; 3 | export declare const clsResponse220: string; 4 | export declare const flsResponse218: string; 5 | export declare const flsResponse220: string; 6 | export declare const tlsResponse207: string; 7 | export declare const tlsResponse218: string; 8 | export declare const tlsResponse220: string; 9 | -------------------------------------------------------------------------------- /lib/AMCP/testResponses.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const fixEndings = (s) => s.replace(/\n/g, '\r\n'); 4 | exports.clsResponse218 = fixEndings(`200 CLS OK 5 | "20190903T183308" STILL 1673299 20190903173309 0 0/1 6 | "AMB" MOVIE 6445960 20190715145445 268 1/25 7 | "CG1080I50" MOVIE 6159792 20190715145445 264 1/25 8 | "CG1080I50_A" MOVIE 10298115 20190715145445 260 1/25 9 | "DRAG" MOVIE 65837628 20121213111257 263 1000000/59940059 10 | "ESSENCE" MOVIE 355402300 20190823112900 574 1/25 11 | "GO1080P25" MOVIE 16694084 20190715145445 445 1/25 12 | "LADYRISES" MOVIE 119878204 20121213095529 202 1/25 13 | "LADYRISES10MBS" MOVIE 20251275 20121213095859 297 1/25 14 | "SCENE/NAMESIGN/CROWN-PLATE" STILL 8172 20190715145446 0 0/1 15 | "SCENE/NAMESIGN/WHITE-PLATE" STILL 21341 20190715145446 0 0/1 16 | "SCENE/ROPE/ROPE_END" STILL 1173 20190715145446 0 0/1 17 | "SCENE/ROPE/ROPE_NODE" STILL 965 20190715145446 0 0/1 18 | "SCENE/TEMPLATEPACK1/ADVANCEDTEMPLATE1_PLATE" STILL 6640 20190715145446 0 0/1 19 | `); 20 | exports.clsResponse207 = fixEndings(`200 CLS OK 21 | "AMB" MOVIE 6445960 20190715160634 268 1/25 22 | "CG1080I50" MOVIE 6159792 20190715160634 264 1/25 23 | "CG1080I50_A" MOVIE 10298115 20190715160634 260 1/25 24 | "GO1080P25" MOVIE 16694084 20190715160634 445 1/25 25 | "SPLIT" STILL 6220854 20190715160634 0 0/1 26 | "TESTPATTERNS\\1080I5000_TEST" MOVIE 1053771 20190715160634 50 1/25 27 | "TESTPATTERNS\\1080I5000_TEST_A" MOVIE 1340796 20190715160634 50 1/25 28 | "TESTPATTERNS\\1080I5994_TEST" MOVIE 1250970 20190715160634 60 1/30 29 | "TESTPATTERNS\\1080I5994_TEST_A" MOVIE 1592759 20190715160634 60 1/30 30 | "TESTPATTERNS\\1080I6000_TEST" MOVIE 1268605 20190715160634 59 125/3747 31 | `); 32 | exports.clsResponse220 = fixEndings(`200 CLS OK 33 | "AMB" MOVIE 6445960 20190718163105 268 1/25 34 | "CG1080I50" MOVIE 6159792 20190718163106 264 1/25 35 | "CG1080I50_A" MOVIE 10298115 20190718163109 260 1/25 36 | "GO1080P25" MOVIE 16694084 20190718163108 445 1/25 37 | "SCENE/NAMESIGN/CROWN-PLATE" STILL 8172 20190718163025 NaN 0/0 38 | "SCENE/NAMESIGN/WHITE-PLATE" STILL 21341 20190816191059 NaN 0/0 39 | "SCENE/ROPE/ROPE_END" STILL 1173 20190718163025 NaN 0/0 40 | "SCENE/ROPE/ROPE_NODE" STILL 965 20190816191059 NaN 0/0 41 | "SCENE/TEMPLATEPACK1/ADVANCEDTEMPLATE1_PLATE" STILL 6640 20190718163026 NaN 0/0 42 | "SPLIT" STILL 6220854 20190718163110 NaN 0/0 43 | `); 44 | exports.flsResponse218 = fixEndings(`200 FLS OK 45 | "LiberationSans" "LiberationSans-Regular.ttf" 46 | "Roboto-Light" "Roboto-Light.ttf" 47 | "Roboto-Regular" "Roboto-Regular.ttf" 48 | `); 49 | exports.flsResponse220 = fixEndings(`200 FLS OK 50 | LIBERATIONSANS-REGULAR 51 | ROBOTO-LIGHT 52 | ROBOTO-REGULAR 53 | `); 54 | exports.tlsResponse207 = fixEndings(`200 TLS OK 55 | "CasparCG_Flash_Templates_Example_Pack_1/ADVANCEDTEMPLATE1" 30327 20190715160636 56 | "CasparCG_Flash_Templates_Example_Pack_1/ADVANCEDTEMPLATE2" 49578 20190715160636 57 | "CasparCG_Flash_Templates_Example_Pack_1/SIMPLETEMPLATE1" 18606 20190715160636 58 | "CasparCG_Flash_Templates_Example_Pack_1/SIMPLETEMPLATE2" 1751565 20190715160636 59 | "CASPAR_TEXT" 19920 20190715160636 60 | "FRAME" 244156 20190715160636 61 | "NTSC-TEST-30" 37275 20190715160636 62 | "NTSC-TEST-60" 37274 20190715160636 63 | "PHONE" 1442360 20190715160636 64 | `); 65 | exports.tlsResponse218 = fixEndings(`200 TLS OK 66 | "CASPAR_TEXT" 19920 20190715145448 flash 67 | "fetch-weather-example/INDEX" 6496 20190718114232 html 68 | "fetch-weather-example/js/monkeecreate-jquery.simpleWeather-0d95e82/INDEX" 120079 20190718114232 html 69 | "FRAME" 244156 20190715145448 flash 70 | "hello-world/INDEX" 622 20190718114232 html 71 | "html_template/template_js/TEMPLATE" 5428 20190820170049 html 72 | "NTSC-TEST-30" 37275 20190715145448 flash 73 | "NTSC-TEST-60" 37274 20190715145448 flash 74 | "PHONE" 1442360 20190715145448 flash 75 | "scene/crawler/CRAWLER" 3266 20190715145448 scene 76 | `); 77 | exports.tlsResponse220 = fixEndings(`200 TLS OK 78 | HELLO-WORLD/INDEX 79 | FETCH-WEATHER-EXAMPLE/INDEX 80 | FETCH-WEATHER-EXAMPLE/JS/MONKEECREATE-JQUERY.SIMPLEWEATHER-0D95E82/INDEX 81 | `); 82 | -------------------------------------------------------------------------------- /lib/chanLayer.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { OpenCLBuffer } from 'nodencl'; 3 | export interface ChanLayer { 4 | valid: boolean; 5 | channel: number; 6 | layer: number; 7 | } 8 | export interface SourceFrame { 9 | video: OpenCLBuffer; 10 | audio: Buffer; 11 | timestamp: number; 12 | } 13 | -------------------------------------------------------------------------------- /lib/chanLayer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* Copyright 2020 Streampunk Media Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | Object.defineProperty(exports, "__esModule", { value: true }); 17 | -------------------------------------------------------------------------------- /lib/channel.d.ts: -------------------------------------------------------------------------------- 1 | import { clContext as nodenCLContext } from 'nodencl'; 2 | import { ChanLayer } from './chanLayer'; 3 | export declare class Channel { 4 | private readonly channel; 5 | private readonly producerRegistry; 6 | private readonly consumerRegistry; 7 | private foreground; 8 | private background; 9 | private spout; 10 | constructor(clContext: nodenCLContext, channel: number); 11 | createSource(chanLay: ChanLayer, params: string[]): Promise; 12 | play(): Promise; 13 | } 14 | -------------------------------------------------------------------------------- /lib/channel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* Copyright 2020 Streampunk Media Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | Object.defineProperty(exports, "__esModule", { value: true }); 17 | const producer_1 = require("./producer/producer"); 18 | const consumer_1 = require("./consumer/consumer"); 19 | class Channel { 20 | constructor(clContext, channel) { 21 | this.channel = channel; 22 | this.producerRegistry = new producer_1.ProducerRegistry(clContext); 23 | this.consumerRegistry = new consumer_1.ConsumerRegistry(clContext); 24 | this.foreground = null; 25 | this.background = null; 26 | this.spout = null; 27 | } 28 | async createSource(chanLay, params) { 29 | this.background = await this.producerRegistry.createSource(chanLay, params); 30 | return this.background != null; 31 | } 32 | async play() { 33 | if (this.background !== null) { 34 | this.foreground = this.background; 35 | this.background = null; 36 | } 37 | if (this.foreground != null) 38 | this.spout = await this.consumerRegistry.createSpout(this.channel, this.foreground); 39 | return Promise.resolve(this.spout != null); 40 | } 41 | } 42 | exports.Channel = Channel; 43 | -------------------------------------------------------------------------------- /lib/consumer/consumer.d.ts: -------------------------------------------------------------------------------- 1 | import { clContext as nodenCLContext, OpenCLBuffer } from 'nodencl'; 2 | import { SourceFrame } from '../chanLayer'; 3 | import { RedioPipe, RedioStream } from 'redioactive'; 4 | export interface Consumer { 5 | initialise(pipe: RedioPipe): Promise | null>; 6 | } 7 | export interface ConsumerFactory { 8 | createConsumer(channel: number): T; 9 | } 10 | export declare class InvalidConsumerError extends Error { 11 | constructor(message?: string); 12 | } 13 | export declare class ConsumerRegistry { 14 | private readonly consumerFactories; 15 | constructor(clContext: nodenCLContext); 16 | createSpout(channel: number, pipe: RedioPipe): Promise | null>; 17 | } 18 | -------------------------------------------------------------------------------- /lib/consumer/consumer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* Copyright 2020 Streampunk Media Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | Object.defineProperty(exports, "__esModule", { value: true }); 17 | const macadamConsumer_1 = require("./macadamConsumer"); 18 | class InvalidConsumerError extends Error { 19 | constructor(message) { 20 | super(message); 21 | // see: typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html 22 | Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain 23 | this.name = InvalidConsumerError.name; // stack traces display correctly now 24 | } 25 | } 26 | exports.InvalidConsumerError = InvalidConsumerError; 27 | class ConsumerRegistry { 28 | constructor(clContext) { 29 | this.consumerFactories = []; 30 | this.consumerFactories.push(new macadamConsumer_1.MacadamConsumerFactory(clContext)); 31 | } 32 | async createSpout(channel, pipe) { 33 | let p = null; 34 | for (const f of this.consumerFactories) { 35 | try { 36 | const consumer = f.createConsumer(channel); 37 | if ((p = await consumer.initialise(pipe)) !== null) 38 | break; 39 | } 40 | catch (err) { 41 | if (!(err instanceof InvalidConsumerError)) { 42 | throw err; 43 | } 44 | } 45 | } 46 | if (p === null) { 47 | console.log(`Failed to find consumer for channel: '${channel}'`); 48 | } 49 | return p; 50 | } 51 | } 52 | exports.ConsumerRegistry = ConsumerRegistry; 53 | -------------------------------------------------------------------------------- /lib/consumer/macadamConsumer.d.ts: -------------------------------------------------------------------------------- 1 | import { SourceFrame } from '../chanLayer'; 2 | import { clContext as nodenCLContext, OpenCLBuffer } from 'nodencl'; 3 | import { ConsumerFactory, Consumer } from './consumer'; 4 | import { RedioPipe, RedioStream } from 'redioactive'; 5 | export declare class MacadamConsumer implements Consumer { 6 | private readonly channel; 7 | private clContext; 8 | private playback; 9 | private fromRGBA; 10 | private vidProcess; 11 | private vidSaver; 12 | private spout; 13 | private clDests; 14 | private field; 15 | private frameNumber; 16 | private readonly latency; 17 | constructor(channel: number, context: nodenCLContext); 18 | initialise(pipe: RedioPipe): Promise | null>; 19 | } 20 | export declare class MacadamConsumerFactory implements ConsumerFactory { 21 | private clContext; 22 | constructor(clContext: nodenCLContext); 23 | createConsumer(channel: number): MacadamConsumer; 24 | } 25 | -------------------------------------------------------------------------------- /lib/consumer/macadamConsumer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* Copyright 2020 Streampunk Media Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | var __importStar = (this && this.__importStar) || function (mod) { 17 | if (mod && mod.__esModule) return mod; 18 | var result = {}; 19 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; 20 | result["default"] = mod; 21 | return result; 22 | }; 23 | Object.defineProperty(exports, "__esModule", { value: true }); 24 | const redioactive_1 = require("redioactive"); 25 | const Macadam = __importStar(require("macadam")); 26 | const io_1 = require("../process/io"); 27 | const v210_1 = require("../process/v210"); 28 | class MacadamConsumer { 29 | constructor(channel, context) { 30 | this.playback = null; 31 | this.channel = channel; 32 | this.clContext = context; 33 | this.field = 0; 34 | this.frameNumber = 0; 35 | this.latency = 3; 36 | } 37 | async initialise(pipe) { 38 | this.playback = await Macadam.playback({ 39 | deviceIndex: this.channel - 1, 40 | displayMode: Macadam.bmdModeHD1080i50, 41 | pixelFormat: Macadam.bmdFormat10BitYUV 42 | }); 43 | this.fromRGBA = new io_1.FromRGBA(this.clContext, '709', new v210_1.Writer(this.playback.width, this.playback.height, this.playback.fieldDominance != 'progressiveFrame')); 44 | await this.fromRGBA.init(); 45 | this.vidProcess = pipe.valve(async (frame) => { 46 | if (!redioactive_1.isEnd(frame) && !redioactive_1.isNil(frame)) { 47 | const fromRGBA = this.fromRGBA; 48 | if (this.field === 0) 49 | this.clDests = await fromRGBA.createDests(); 50 | const clDests = this.clDests; 51 | const srcFrame = frame; 52 | const queue = this.clContext.queue.process; 53 | const interlace = 0x1 | (this.field << 1); 54 | await fromRGBA.processFrame(srcFrame.video, clDests, queue, interlace); 55 | await this.clContext.waitFinish(queue); 56 | srcFrame.video.release(); 57 | this.field = 1 - this.field; 58 | return this.field === 1 ? redioactive_1.nil : clDests[0]; 59 | } 60 | else { 61 | return frame; 62 | } 63 | }, { bufferSizeMax: 3, oneToMany: false }); 64 | this.vidSaver = this.vidProcess.valve(async (frame) => { 65 | if (!redioactive_1.isEnd(frame) && !redioactive_1.isNil(frame)) { 66 | const v210Frame = frame; 67 | const fromRGBA = this.fromRGBA; 68 | await fromRGBA.saveFrame(v210Frame, this.clContext.queue.unload); 69 | await this.clContext.waitFinish(this.clContext.queue.unload); 70 | return v210Frame; 71 | } 72 | else { 73 | return frame; 74 | } 75 | }, { bufferSizeMax: 3, oneToMany: false }); 76 | this.spout = this.vidSaver.spout(async (frame) => { 77 | var _a, _b, _c; 78 | if (!redioactive_1.isEnd(frame) && !redioactive_1.isNil(frame)) { 79 | const v210Frame = frame; 80 | (_a = this.playback) === null || _a === void 0 ? void 0 : _a.schedule({ video: v210Frame, time: 1000 * this.frameNumber }); 81 | if (this.frameNumber === this.latency) 82 | (_b = this.playback) === null || _b === void 0 ? void 0 : _b.start({ startTime: 0 }); 83 | if (this.frameNumber >= this.latency) 84 | await ((_c = this.playback) === null || _c === void 0 ? void 0 : _c.played((this.frameNumber - this.latency) * 1000)); 85 | this.frameNumber++; 86 | v210Frame.release(); 87 | return Promise.resolve(); 88 | } 89 | else { 90 | return Promise.resolve(); 91 | } 92 | }, { bufferSizeMax: 3, oneToMany: false }); 93 | console.log(`Created Macadam consumer for Blackmagic id: ${this.channel - 1}`); 94 | return this.spout; 95 | } 96 | } 97 | exports.MacadamConsumer = MacadamConsumer; 98 | class MacadamConsumerFactory { 99 | constructor(clContext) { 100 | this.clContext = clContext; 101 | } 102 | createConsumer(channel) { 103 | return new MacadamConsumer(channel, this.clContext); 104 | } 105 | } 106 | exports.MacadamConsumerFactory = MacadamConsumerFactory; 107 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* Copyright 2020 Streampunk Media Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | var __importDefault = (this && this.__importDefault) || function (mod) { 17 | return (mod && mod.__esModule) ? mod : { "default": mod }; 18 | }; 19 | Object.defineProperty(exports, "__esModule", { value: true }); 20 | const nodencl_1 = require("nodencl"); 21 | const server_1 = require("./AMCP/server"); 22 | const commands_1 = require("./AMCP/commands"); 23 | const basic_1 = require("./AMCP/basic"); 24 | const koa_1 = __importDefault(require("koa")); 25 | const cors_1 = __importDefault(require("@koa/cors")); 26 | const readline_1 = __importDefault(require("readline")); 27 | const initialiseOpenCL = async () => { 28 | const platformIndex = 0; 29 | const deviceIndex = 0; 30 | const clContext = new nodencl_1.clContext({ 31 | platformIndex: platformIndex, 32 | deviceIndex: deviceIndex, 33 | overlapping: true 34 | }); 35 | await clContext.initialise(); 36 | const platformInfo = clContext.getPlatformInfo(); 37 | console.log(`OpenCL accelerator running on device from vendor '${platformInfo.vendor}', type '${platformInfo.devices[deviceIndex].type}'`); 38 | return clContext; 39 | }; 40 | const rl = readline_1.default.createInterface({ 41 | input: process.stdin, 42 | output: process.stdout, 43 | prompt: 'AMCP> ' 44 | }); 45 | rl.on('line', async (input) => { 46 | if (input === 'q') { 47 | process.kill(process.pid, 'SIGTERM'); 48 | } 49 | if (input !== '') { 50 | console.log(`AMCP received: ${input}`); 51 | await server_1.processCommand(input.toUpperCase().match(/"[^"]+"|""|\S+/g)); 52 | } 53 | rl.prompt(); 54 | }); 55 | rl.on('SIGINT', () => { 56 | process.kill(process.pid, 'SIGTERM'); 57 | }); 58 | // 960 * 540 RGBA 8-bit 59 | const lastWeb = Buffer.alloc(1920 * 1080); 60 | const kapp = new koa_1.default(); 61 | kapp.use(cors_1.default()); 62 | kapp.use((ctx) => { 63 | ctx.body = lastWeb; 64 | }); 65 | const server = kapp.listen(3001); 66 | process.on('SIGHUP', () => server.close); 67 | const commands = new commands_1.Commands(); 68 | initialiseOpenCL().then((context) => { 69 | const basic = new basic_1.Basic(context); 70 | basic.addCmds(commands); 71 | }); 72 | server_1.start(commands).then((fulfilled) => console.log('Command:', fulfilled), console.error); 73 | -------------------------------------------------------------------------------- /lib/process/bgra8.d.ts: -------------------------------------------------------------------------------- 1 | import { PackImpl } from './packer'; 2 | import { KernelParams, OpenCLBuffer } from 'nodencl'; 3 | export declare function getPitchBytes(width: number): number; 4 | export declare function fillBuf(buf: OpenCLBuffer, width: number, height: number): void; 5 | export declare function dumpBuf(buf: OpenCLBuffer, width: number, numLines: number): void; 6 | export declare class Reader extends PackImpl { 7 | constructor(width: number, height: number); 8 | getKernelParams(params: KernelParams): KernelParams; 9 | } 10 | export declare class Writer extends PackImpl { 11 | constructor(width: number, height: number, interlaced: boolean); 12 | getKernelParams(params: KernelParams): KernelParams; 13 | } 14 | -------------------------------------------------------------------------------- /lib/process/colourMaths.d.ts: -------------------------------------------------------------------------------- 1 | export declare function gamma2linearLUT(colSpec: string): Float32Array; 2 | export declare function linear2gammaLUT(colSpec: string): Float32Array; 3 | export declare function matrixMultiply(a: Float32Array[], b: Float32Array[]): Float32Array[]; 4 | export declare function ycbcr2rgbMatrix(colSpec: string, numBits: number, lumaBlack: number, lumaWhite: number, chrRange: number): Float32Array[]; 5 | export declare function rgb2ycbcrMatrix(colSpec: string, numBits: number, lumaBlack: number, lumaWhite: number, chrRange: number): Float32Array[]; 6 | export declare function rgb2rgbMatrix(srcColSpec: string, dstColSpec: string): Float32Array[]; 7 | export declare function matrixFlatten(a: Float32Array[]): Float32Array; 8 | -------------------------------------------------------------------------------- /lib/process/combine.d.ts: -------------------------------------------------------------------------------- 1 | import { ProcessImpl } from './imageProcess'; 2 | import { KernelParams } from 'nodencl'; 3 | export default class Combine extends ProcessImpl { 4 | private readonly numOverlays; 5 | constructor(width: number, height: number, numOverlays: number); 6 | init(): Promise; 7 | getKernelParams(params: KernelParams): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /lib/process/combine.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* Copyright 2019 Streampunk Media Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | Object.defineProperty(exports, "__esModule", { value: true }); 17 | const imageProcess_1 = require("./imageProcess"); 18 | const combineKernel = ` 19 | __constant sampler_t sampler1 = 20 | CLK_NORMALIZED_COORDS_FALSE 21 | | CLK_ADDRESS_CLAMP_TO_EDGE 22 | | CLK_FILTER_NEAREST; 23 | 24 | __kernel void 25 | twoInputs(__read_only image2d_t bgIn, 26 | __read_only image2d_t ovIn, 27 | __write_only image2d_t output) { 28 | 29 | int x = get_global_id(0); 30 | int y = get_global_id(1); 31 | float4 bg = read_imagef(bgIn, sampler1, (int2)(x,y)); 32 | float4 ov = read_imagef(ovIn, sampler1, (int2)(x,y)); 33 | float k = 1.0f - ov.s3; 34 | float4 k4 = (float4)(k, k, k, 0.0f); 35 | float4 out = fma(bg, k4, ov); 36 | write_imagef(output, (int2)(x, y), out); 37 | }; 38 | 39 | __kernel void 40 | threeInputs(__read_only image2d_t bgIn, 41 | __read_only image2d_t ov0In, 42 | __read_only image2d_t ov1In, 43 | __write_only image2d_t output) { 44 | 45 | int x = get_global_id(0); 46 | int y = get_global_id(1); 47 | float4 bg = read_imagef(bgIn, sampler1, (int2)(x,y)); 48 | 49 | float4 ov0 = read_imagef(ov0In, sampler1, (int2)(x,y)); 50 | float k = 1.0f - ov0.s3; 51 | float4 k4 = (float4)(k, k, k, 0.0f); 52 | float4 out0 = fma(bg, k4, ov0); 53 | 54 | float4 ov1 = read_imagef(ov1In, sampler1, (int2)(x,y)); 55 | k = 1.0f - ov1.s3; 56 | k4 = (float4)(k, k, k, 0.0f); 57 | float4 out1 = fma(out0, k4, ov1); 58 | write_imagef(output, (int2)(x, y), out1); 59 | }; 60 | `; 61 | class Combine extends imageProcess_1.ProcessImpl { 62 | constructor(width, height, numOverlays) { 63 | super(numOverlays === 1 ? 'combine-1' : 'combine-2', width, height, combineKernel, numOverlays === 1 ? 'twoInputs' : 'threeInputs'); 64 | this.numOverlays = numOverlays; 65 | if (!(this.numOverlays > 0 && this.numOverlays < 3)) 66 | throw new Error(`Combine supports one or two overlays, ${this.numOverlays} requested`); 67 | } 68 | async init() { 69 | return Promise.resolve(); 70 | } 71 | async getKernelParams(params) { 72 | const kernelParams = { 73 | bgIn: params.bgIn, 74 | output: params.output 75 | }; 76 | const ovArray = params.ovIn; 77 | if (ovArray.length !== 1 && ovArray.length !== 2) 78 | throw new Error("Combine requires 'ovIn' array parameter with 1 or 2 OpenCL buffers"); 79 | switch (this.numOverlays) { 80 | case 1: 81 | kernelParams.ovIn = ovArray[0]; 82 | break; 83 | case 2: 84 | kernelParams.ov0In = ovArray[0]; 85 | kernelParams.ov1In = ovArray[1]; 86 | break; 87 | } 88 | return Promise.resolve(kernelParams); 89 | } 90 | } 91 | exports.default = Combine; 92 | -------------------------------------------------------------------------------- /lib/process/imageProcess.d.ts: -------------------------------------------------------------------------------- 1 | import { clContext as nodenCLContext, KernelParams, RunTimings } from 'nodencl'; 2 | export declare abstract class ProcessImpl { 3 | protected readonly name: string; 4 | protected readonly width: number; 5 | protected readonly height: number; 6 | readonly kernel: string; 7 | readonly programName: string; 8 | readonly globalWorkItems = 0; 9 | constructor(name: string, width: number, height: number, kernel: string, programName: string); 10 | abstract init(): Promise; 11 | getNumBytesRGBA(): number; 12 | getGlobalWorkItems(): Uint32Array; 13 | abstract getKernelParams(params: KernelParams, clQueue: number): Promise; 14 | } 15 | export default class ImageProcess { 16 | private readonly clContext; 17 | private readonly processImpl; 18 | private program; 19 | constructor(clContext: nodenCLContext, processImpl: ProcessImpl); 20 | init(): Promise; 21 | run(params: KernelParams, clQueue: number): Promise; 22 | } 23 | -------------------------------------------------------------------------------- /lib/process/imageProcess.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* Copyright 2019 Streampunk Media Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | Object.defineProperty(exports, "__esModule", { value: true }); 17 | class ProcessImpl { 18 | constructor(name, width, height, kernel, programName) { 19 | this.globalWorkItems = 0; 20 | this.name = name; 21 | this.width = width; 22 | this.height = height; 23 | this.kernel = kernel; 24 | this.programName = programName; 25 | } 26 | getNumBytesRGBA() { 27 | return this.width * this.height * 4 * 4; 28 | } 29 | getGlobalWorkItems() { 30 | return Uint32Array.from([this.width, this.height]); 31 | } 32 | } 33 | exports.ProcessImpl = ProcessImpl; 34 | class ImageProcess { 35 | constructor(clContext, processImpl) { 36 | this.program = null; 37 | this.clContext = clContext; 38 | this.processImpl = processImpl; 39 | } 40 | async init() { 41 | this.program = await this.clContext.createProgram(this.processImpl.kernel, { 42 | name: this.processImpl.programName, 43 | globalWorkItems: this.processImpl.getGlobalWorkItems() 44 | }); 45 | return this.processImpl.init(); 46 | } 47 | async run(params, clQueue) { 48 | if (this.program == null) 49 | throw new Error('Loader.run failed with no program available'); 50 | const kernelParams = await this.processImpl.getKernelParams(params, clQueue); 51 | return this.clContext.runProgram(this.program, kernelParams, clQueue); 52 | } 53 | } 54 | exports.default = ImageProcess; 55 | -------------------------------------------------------------------------------- /lib/process/io.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { clContext as nodenCLContext, OpenCLBuffer, ImageDims, RunTimings } from 'nodencl'; 3 | import { PackImpl, Interlace } from './packer'; 4 | export declare class ToRGBA { 5 | private readonly clContext; 6 | private readonly loader; 7 | private readonly numBytes; 8 | private readonly numBytesRGBA; 9 | private readonly totalBytes; 10 | constructor(clContext: nodenCLContext, colSpecRead: string, colSpecWrite: string, readImpl: PackImpl); 11 | init(): Promise; 12 | getNumBytes(): Array; 13 | getNumBytesRGBA(): number; 14 | getTotalBytes(): number; 15 | createSources(): Promise>; 16 | createDest(imageDims: ImageDims): Promise; 17 | loadFrame(input: Buffer | Array, sources: Array, clQueue?: number | undefined): Promise>; 18 | processFrame(sources: Array, dest: OpenCLBuffer, clQueue?: number): Promise; 19 | } 20 | export declare class FromRGBA { 21 | private readonly clContext; 22 | private readonly width; 23 | private readonly height; 24 | private readonly saver; 25 | private readonly numBytes; 26 | private readonly numBytesRGBA; 27 | private readonly totalBytes; 28 | private readonly srcWidth; 29 | private readonly srcHeight; 30 | private resizer; 31 | private rgbaSz; 32 | constructor(clContext: nodenCLContext, colSpecRead: string, writeImpl: PackImpl, srcWidth?: number, srcHeight?: number); 33 | init(): Promise; 34 | getNumBytes(): Array; 35 | getNumBytesRGBA(): number; 36 | getTotalBytes(): number; 37 | createDests(): Promise>; 38 | processFrame(source: OpenCLBuffer, dests: Array, clQueue?: number, interlace?: Interlace): Promise; 39 | saveFrame(output: OpenCLBuffer | Array, clQueue?: number | undefined): Promise>; 40 | } 41 | -------------------------------------------------------------------------------- /lib/process/io.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* Copyright 2019 Streampunk Media Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | var __importDefault = (this && this.__importDefault) || function (mod) { 17 | return (mod && mod.__esModule) ? mod : { "default": mod }; 18 | }; 19 | Object.defineProperty(exports, "__esModule", { value: true }); 20 | const loadSave_1 = require("./loadSave"); 21 | const imageProcess_1 = __importDefault(require("./imageProcess")); 22 | const resize_1 = __importDefault(require("./resize")); 23 | class ToRGBA { 24 | constructor(clContext, colSpecRead, colSpecWrite, readImpl) { 25 | this.clContext = clContext; 26 | this.loader = new loadSave_1.Loader(this.clContext, colSpecRead, colSpecWrite, readImpl); 27 | this.numBytes = readImpl.getNumBytes(); 28 | this.numBytesRGBA = readImpl.getNumBytesRGBA(); 29 | this.totalBytes = readImpl.getTotalBytes(); 30 | } 31 | async init() { 32 | await this.loader.init(); 33 | } 34 | getNumBytes() { 35 | return this.numBytes; 36 | } 37 | getNumBytesRGBA() { 38 | return this.numBytesRGBA; 39 | } 40 | getTotalBytes() { 41 | return this.totalBytes; 42 | } 43 | async createSources() { 44 | return Promise.all(this.numBytes.map((bytes) => this.clContext.createBuffer(bytes, 'readonly', 'coarse', undefined, 'ToRGBA'))); 45 | } 46 | async createDest(imageDims) { 47 | return this.clContext.createBuffer(this.numBytesRGBA, 'readonly', 'coarse', imageDims, 'ToRGBA'); 48 | } 49 | async loadFrame(input, sources, clQueue) { 50 | const inputs = Array.isArray(input) ? input : [input]; 51 | return Promise.all(sources.map(async (src, i) => { 52 | await src.hostAccess('writeonly', clQueue ? clQueue : 0, inputs[i].slice(0, this.numBytes[i])); 53 | return src.hostAccess('none', clQueue ? clQueue : 0); 54 | })); 55 | } 56 | async processFrame(sources, dest, clQueue) { 57 | return this.loader.run({ sources: sources, dest: dest }, clQueue ? clQueue : 0); 58 | } 59 | } 60 | exports.ToRGBA = ToRGBA; 61 | class FromRGBA { 62 | constructor(clContext, colSpecRead, writeImpl, srcWidth, srcHeight) { 63 | this.resizer = null; 64 | this.rgbaSz = null; 65 | this.clContext = clContext; 66 | this.width = writeImpl.getWidth(); 67 | this.height = writeImpl.getHeight(); 68 | this.saver = new loadSave_1.Saver(this.clContext, colSpecRead, writeImpl); 69 | this.numBytes = writeImpl.getNumBytes(); 70 | this.numBytesRGBA = writeImpl.getNumBytesRGBA(); 71 | this.totalBytes = writeImpl.getTotalBytes(); 72 | this.srcWidth = srcWidth ? srcWidth : this.width; 73 | this.srcHeight = srcHeight ? srcHeight : this.height; 74 | } 75 | async init() { 76 | await this.saver.init(); 77 | if (!(this.srcWidth === this.width && this.srcHeight === this.height)) { 78 | this.resizer = new imageProcess_1.default(this.clContext, new resize_1.default(this.clContext, this.width, this.height)); 79 | await this.resizer.init(); 80 | this.rgbaSz = await this.clContext.createBuffer(this.numBytesRGBA, 'readwrite', 'coarse', { width: this.width, height: this.height }, 'rgbaSz'); 81 | } 82 | } 83 | getNumBytes() { 84 | return this.numBytes; 85 | } 86 | getNumBytesRGBA() { 87 | return this.numBytesRGBA; 88 | } 89 | getTotalBytes() { 90 | return this.totalBytes; 91 | } 92 | async createDests() { 93 | return Promise.all(this.numBytes.map((bytes) => this.clContext.createBuffer(bytes, 'readonly', 'coarse', undefined, 'ToRGBA'))); 94 | } 95 | async processFrame(source, dests, clQueue, interlace) { 96 | let saveSource = source; 97 | if (this.resizer && this.rgbaSz) { 98 | await this.resizer.run({ input: source, output: this.rgbaSz }, clQueue ? clQueue : 0); 99 | saveSource = this.rgbaSz; 100 | } 101 | return this.saver.run({ source: saveSource, dests: dests, interlace: interlace }, clQueue ? clQueue : 0); 102 | } 103 | async saveFrame(output, clQueue) { 104 | const outputs = Array.isArray(output) ? output : [output]; 105 | return Promise.all(outputs.map((op) => op.hostAccess('readonly', clQueue ? clQueue : 0))); 106 | } 107 | } 108 | exports.FromRGBA = FromRGBA; 109 | -------------------------------------------------------------------------------- /lib/process/loadSave.d.ts: -------------------------------------------------------------------------------- 1 | import Packer, { PackImpl } from './packer'; 2 | import { clContext as nodenCLContext, KernelParams, RunTimings } from 'nodencl'; 3 | export declare class Loader extends Packer { 4 | private readonly gammaArray; 5 | private readonly colMatrixArray; 6 | private readonly gamutMatrixArray; 7 | private gammaLut; 8 | private colMatrix; 9 | private gamutMatrix; 10 | constructor(clContext: nodenCLContext, colSpec: string, outColSpec: string, packImpl: PackImpl); 11 | init(): Promise; 12 | run(params: KernelParams, queueNum: number): Promise; 13 | } 14 | export declare class Saver extends Packer { 15 | private readonly gammaArray; 16 | private readonly colMatrixArray; 17 | private gammaLut; 18 | private colMatrix; 19 | constructor(clContext: nodenCLContext, colSpec: string, packImpl: PackImpl); 20 | init(): Promise; 21 | run(params: KernelParams, queueNum: number): Promise; 22 | } 23 | -------------------------------------------------------------------------------- /lib/process/loadSave.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* Copyright 2019 Streampunk Media Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | var __importDefault = (this && this.__importDefault) || function (mod) { 17 | return (mod && mod.__esModule) ? mod : { "default": mod }; 18 | }; 19 | Object.defineProperty(exports, "__esModule", { value: true }); 20 | const packer_1 = __importDefault(require("./packer")); 21 | const colourMaths_1 = require("./colourMaths"); 22 | class Loader extends packer_1.default { 23 | constructor(clContext, colSpec, outColSpec, packImpl) { 24 | super(clContext, packImpl); 25 | this.colMatrixArray = null; 26 | this.gammaLut = null; 27 | this.colMatrix = null; 28 | this.gamutMatrix = null; 29 | this.gammaArray = colourMaths_1.gamma2linearLUT(colSpec); 30 | if (!this.packImpl.getIsRGB()) { 31 | const colMatrix2d = colourMaths_1.ycbcr2rgbMatrix(colSpec, this.packImpl.numBits, this.packImpl.lumaBlack, this.packImpl.lumaWhite, this.packImpl.chromaRange); 32 | this.colMatrixArray = colourMaths_1.matrixFlatten(colMatrix2d); 33 | } 34 | const gamutMatrix2d = colourMaths_1.rgb2rgbMatrix(colSpec, outColSpec); 35 | this.gamutMatrixArray = colourMaths_1.matrixFlatten(gamutMatrix2d); 36 | } 37 | async init() { 38 | await super.init(); 39 | this.gammaLut = await this.clContext.createBuffer(this.gammaArray.byteLength, 'readonly', 'coarse'); 40 | await this.gammaLut.hostAccess('writeonly'); 41 | Buffer.from(this.gammaArray.buffer).copy(this.gammaLut); 42 | if (this.colMatrixArray) { 43 | this.colMatrix = await this.clContext.createBuffer(this.colMatrixArray.byteLength, 'readonly', 'none'); 44 | await this.colMatrix.hostAccess('writeonly'); 45 | Buffer.from(this.colMatrixArray.buffer).copy(this.colMatrix); 46 | } 47 | this.gamutMatrix = await this.clContext.createBuffer(this.gamutMatrixArray.byteLength, 'readonly', 'none'); 48 | await this.gamutMatrix.hostAccess('writeonly'); 49 | Buffer.from(this.gamutMatrixArray.buffer).copy(this.gamutMatrix); 50 | } 51 | async run(params, queueNum) { 52 | if (this.program === null) 53 | throw new Error('Loader.run failed with no program available'); 54 | const kernelParams = this.packImpl.getKernelParams(params); 55 | kernelParams.gammaLut = this.gammaLut; 56 | kernelParams.gamutMatrix = this.gamutMatrix; 57 | if (this.colMatrix) 58 | kernelParams.colMatrix = this.colMatrix; 59 | return this.clContext.runProgram(this.program, kernelParams, queueNum); 60 | } 61 | } 62 | exports.Loader = Loader; 63 | class Saver extends packer_1.default { 64 | constructor(clContext, colSpec, packImpl) { 65 | super(clContext, packImpl); 66 | this.colMatrixArray = null; 67 | this.gammaLut = null; 68 | this.colMatrix = null; 69 | this.gammaArray = colourMaths_1.linear2gammaLUT(colSpec); 70 | if (!this.packImpl.getIsRGB()) { 71 | const colMatrix2d = colourMaths_1.rgb2ycbcrMatrix(colSpec, this.packImpl.numBits, this.packImpl.lumaBlack, this.packImpl.lumaWhite, this.packImpl.chromaRange); 72 | this.colMatrixArray = colourMaths_1.matrixFlatten(colMatrix2d); 73 | } 74 | } 75 | async init() { 76 | await super.init(); 77 | this.gammaLut = await this.clContext.createBuffer(this.gammaArray.byteLength, 'readonly', 'coarse'); 78 | await this.gammaLut.hostAccess('writeonly'); 79 | Buffer.from(this.gammaArray.buffer).copy(this.gammaLut); 80 | if (this.colMatrixArray) { 81 | this.colMatrix = await this.clContext.createBuffer(this.colMatrixArray.byteLength, 'readonly', 'none'); 82 | await this.colMatrix.hostAccess('writeonly'); 83 | Buffer.from(this.colMatrixArray.buffer).copy(this.colMatrix); 84 | } 85 | } 86 | async run(params, queueNum) { 87 | if (this.program === null) 88 | throw new Error('Saver.run failed with no program available'); 89 | const kernelParams = this.packImpl.getKernelParams(params); 90 | kernelParams.gammaLut = this.gammaLut; 91 | if (this.colMatrix) 92 | kernelParams.colMatrix = this.colMatrix; 93 | return this.clContext.runProgram(this.program, kernelParams, queueNum); 94 | } 95 | } 96 | exports.Saver = Saver; 97 | -------------------------------------------------------------------------------- /lib/process/mix.d.ts: -------------------------------------------------------------------------------- 1 | import { ProcessImpl } from './imageProcess'; 2 | import { KernelParams } from 'nodencl'; 3 | export default class Mix extends ProcessImpl { 4 | constructor(width: number, height: number); 5 | init(): Promise; 6 | getKernelParams(params: KernelParams): Promise; 7 | } 8 | -------------------------------------------------------------------------------- /lib/process/mix.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* Copyright 2019 Streampunk Media Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | Object.defineProperty(exports, "__esModule", { value: true }); 17 | const imageProcess_1 = require("./imageProcess"); 18 | const mixKernel = ` 19 | __constant sampler_t sampler1 = 20 | CLK_NORMALIZED_COORDS_FALSE 21 | | CLK_ADDRESS_CLAMP_TO_EDGE 22 | | CLK_FILTER_NEAREST; 23 | 24 | __kernel void mixer( 25 | __read_only image2d_t input0, 26 | __read_only image2d_t input1, 27 | __private float mix, 28 | __write_only image2d_t output) { 29 | 30 | int x = get_global_id(0); 31 | int y = get_global_id(1); 32 | float4 in0 = read_imagef(input0, sampler1, (int2)(x,y)); 33 | float4 in1 = read_imagef(input1, sampler1, (int2)(x,y)); 34 | 35 | float rmix = 1.0f - mix; 36 | float4 out = fma(in0, mix, in1 * rmix); 37 | 38 | write_imagef(output, (int2)(x, y), out); 39 | }; 40 | `; 41 | class Mix extends imageProcess_1.ProcessImpl { 42 | constructor(width, height) { 43 | super('mixer', width, height, mixKernel, 'mixer'); 44 | } 45 | async init() { 46 | return Promise.resolve(); 47 | } 48 | async getKernelParams(params) { 49 | return Promise.resolve({ 50 | input0: params.input0, 51 | input1: params.input1, 52 | mix: params.mix, 53 | output: params.output 54 | }); 55 | } 56 | } 57 | exports.default = Mix; 58 | -------------------------------------------------------------------------------- /lib/process/packer.d.ts: -------------------------------------------------------------------------------- 1 | import { clContext as nodenCLContext, OpenCLProgram, KernelParams, RunTimings } from 'nodencl'; 2 | export declare enum Interlace { 3 | Progressive = 0, 4 | TopField = 1, 5 | BottomField = 3 6 | } 7 | export declare abstract class PackImpl { 8 | protected readonly name: string; 9 | protected readonly width: number; 10 | protected readonly height: number; 11 | protected interlaced: boolean; 12 | readonly kernel: string; 13 | readonly programName: string; 14 | numBits: number; 15 | lumaBlack: number; 16 | lumaWhite: number; 17 | chromaRange: number; 18 | protected isRGB: boolean; 19 | protected numBytes: Array; 20 | protected globalWorkItems: number; 21 | protected workItemsPerGroup: number; 22 | constructor(name: string, width: number, height: number, kernel: string, programName: string); 23 | getWidth(): number; 24 | getHeight(): number; 25 | getNumBytes(): Array; 26 | getNumBytesRGBA(): number; 27 | getIsRGB(): boolean; 28 | getTotalBytes(): number; 29 | getGlobalWorkItems(): number; 30 | getWorkItemsPerGroup(): number; 31 | abstract getKernelParams(params: KernelParams): KernelParams; 32 | } 33 | export default abstract class Packer { 34 | protected readonly clContext: nodenCLContext; 35 | protected readonly packImpl: PackImpl; 36 | protected program: OpenCLProgram | null; 37 | constructor(clContext: nodenCLContext, packImpl: PackImpl); 38 | init(): Promise; 39 | abstract run(kernelParams: KernelParams, queueNum: number): Promise; 40 | } 41 | -------------------------------------------------------------------------------- /lib/process/packer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* Copyright 2019 Streampunk Media Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | Object.defineProperty(exports, "__esModule", { value: true }); 17 | var Interlace; 18 | (function (Interlace) { 19 | Interlace[Interlace["Progressive"] = 0] = "Progressive"; 20 | Interlace[Interlace["TopField"] = 1] = "TopField"; 21 | Interlace[Interlace["BottomField"] = 3] = "BottomField"; 22 | })(Interlace = exports.Interlace || (exports.Interlace = {})); 23 | class PackImpl { 24 | constructor(name, width, height, kernel, programName) { 25 | this.interlaced = false; 26 | this.numBits = 10; 27 | this.lumaBlack = 64; 28 | this.lumaWhite = 940; 29 | this.chromaRange = 896; 30 | this.isRGB = true; 31 | this.numBytes = [0]; 32 | this.globalWorkItems = 0; 33 | this.workItemsPerGroup = 0; 34 | this.name = name; 35 | this.width = width; 36 | this.height = height; 37 | this.kernel = kernel; 38 | this.programName = programName; 39 | } 40 | getWidth() { 41 | return this.width; 42 | } 43 | getHeight() { 44 | return this.height; 45 | } 46 | getNumBytes() { 47 | return this.numBytes; 48 | } 49 | getNumBytesRGBA() { 50 | return this.width * this.height * 4 * 4; 51 | } 52 | getIsRGB() { 53 | return this.isRGB; 54 | } 55 | getTotalBytes() { 56 | return this.numBytes.reduce((acc, n) => acc + n, 0); 57 | } 58 | getGlobalWorkItems() { 59 | return this.globalWorkItems; 60 | } 61 | getWorkItemsPerGroup() { 62 | return this.workItemsPerGroup; 63 | } 64 | } 65 | exports.PackImpl = PackImpl; 66 | class Packer { 67 | constructor(clContext, packImpl) { 68 | this.program = null; 69 | this.clContext = clContext; 70 | this.packImpl = packImpl; 71 | } 72 | async init() { 73 | this.program = await this.clContext.createProgram(this.packImpl.kernel, { 74 | name: this.packImpl.programName, 75 | globalWorkItems: this.packImpl.getGlobalWorkItems(), 76 | workItemsPerGroup: this.packImpl.getWorkItemsPerGroup() 77 | }); 78 | } 79 | } 80 | exports.default = Packer; 81 | -------------------------------------------------------------------------------- /lib/process/resize.d.ts: -------------------------------------------------------------------------------- 1 | import { ProcessImpl } from './imageProcess'; 2 | import { clContext as nodenCLContext, KernelParams } from 'nodencl'; 3 | export default class Resize extends ProcessImpl { 4 | private readonly clContext; 5 | private flipH; 6 | private flipV; 7 | private flipArr; 8 | private readonly flipArrBytes; 9 | private flipVals; 10 | constructor(clContext: nodenCLContext, width: number, height: number); 11 | private updateFlip; 12 | init(): Promise; 13 | getKernelParams(params: KernelParams, clQueue: number): Promise; 14 | } 15 | -------------------------------------------------------------------------------- /lib/process/resize.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* Copyright 2019 Streampunk Media Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | Object.defineProperty(exports, "__esModule", { value: true }); 17 | const imageProcess_1 = require("./imageProcess"); 18 | const resizeKernel = ` 19 | __constant sampler_t samplerIn = 20 | CLK_NORMALIZED_COORDS_TRUE | 21 | CLK_ADDRESS_CLAMP | 22 | CLK_FILTER_LINEAR; 23 | 24 | __constant sampler_t samplerOut = 25 | CLK_NORMALIZED_COORDS_FALSE | 26 | CLK_ADDRESS_CLAMP | 27 | CLK_FILTER_NEAREST; 28 | 29 | __kernel void resize( 30 | __read_only image2d_t input, 31 | __private float scale, 32 | __private float offsetX, 33 | __private float offsetY, 34 | __global float* restrict flip, 35 | __write_only image2d_t output) { 36 | 37 | int w = get_image_width(output); 38 | int h = get_image_height(output); 39 | 40 | int outX = get_global_id(0); 41 | int outY = get_global_id(1); 42 | int2 posOut = {outX, outY}; 43 | 44 | float2 inPos = (float2)(outX / (float) w, outY / (float) h); 45 | float centreOffX = (-0.5f - offsetX) / scale + 0.5f; 46 | float centreOffY = (-0.5f - offsetY) / scale + 0.5f; 47 | float2 off = (float2)(fma(centreOffX, flip[1], flip[0]), fma(centreOffY, flip[3], flip[2])); 48 | float2 mul = (float2)(flip[1] / scale, flip[3] / scale); 49 | float2 posIn = fma(inPos, mul, off); 50 | 51 | float4 in = read_imagef(input, samplerIn, posIn); 52 | write_imagef(output, posOut, in); 53 | } 54 | `; 55 | class Resize extends imageProcess_1.ProcessImpl { 56 | constructor(clContext, width, height) { 57 | super('resize', width, height, resizeKernel, 'resize'); 58 | this.flipVals = null; 59 | this.clContext = clContext; 60 | this.flipH = false; 61 | this.flipV = false; 62 | this.flipArr = Float32Array.from([0.0, 1.0, 0.0, 1.0]); 63 | this.flipArrBytes = this.flipArr.length * this.flipArr.BYTES_PER_ELEMENT; 64 | } 65 | async updateFlip(flipH, flipV, clQueue) { 66 | if (this.flipVals === null) 67 | throw new Error('Resize.updateFlip failed with no program available'); 68 | this.flipH = flipH; 69 | this.flipV = flipV; 70 | this.flipArr = Float32Array.from([ 71 | this.flipH ? 1.0 : 0.0, 72 | this.flipH ? -1.0 : 1.0, 73 | this.flipV ? 1.0 : 0.0, 74 | this.flipV ? -1.0 : 1.0 75 | ]); 76 | await this.flipVals.hostAccess('writeonly', clQueue, Buffer.from(this.flipArr.buffer)); 77 | return this.flipVals.hostAccess('none', clQueue); 78 | } 79 | async init() { 80 | this.flipVals = await this.clContext.createBuffer(this.flipArrBytes, 'readonly', 'coarse'); 81 | return this.updateFlip(false, false, this.clContext.queue.load); 82 | } 83 | async getKernelParams(params, clQueue) { 84 | const flipH = params.flipH; 85 | const flipV = params.flipV; 86 | const scale = params.scale; 87 | const offsetX = params.offsetX; 88 | const offsetY = params.offsetY; 89 | if (!(this.flipH === flipH && this.flipV === flipV)) 90 | await this.updateFlip(flipH, flipV, clQueue); 91 | if (scale && !(scale > 0.0)) 92 | throw 'resize scale factor must be greater than zero'; 93 | if (offsetX && !(offsetX >= -1.0 && offsetX <= 1.0)) 94 | throw 'resize offsetX must be between -1.0 and +1.0'; 95 | if (offsetY && !(offsetY >= -1.0 && offsetY <= 1.0)) 96 | throw 'resize offsetX must be between -1.0 and +1.0'; 97 | return Promise.resolve({ 98 | input: params.input, 99 | scale: params.scale || 1.0, 100 | offsetX: params.offsetX || 0.0, 101 | offsetY: params.offsetY || 0.0, 102 | flip: this.flipVals, 103 | output: params.output 104 | }); 105 | } 106 | } 107 | exports.default = Resize; 108 | -------------------------------------------------------------------------------- /lib/process/rgba8.d.ts: -------------------------------------------------------------------------------- 1 | import { PackImpl } from './packer'; 2 | import { KernelParams, OpenCLBuffer } from 'nodencl'; 3 | export declare function getPitchBytes(width: number): number; 4 | export declare function fillBuf(buf: OpenCLBuffer, width: number, height: number): void; 5 | export declare function dumpBuf(buf: OpenCLBuffer, width: number, numLines: number): void; 6 | export declare class Reader extends PackImpl { 7 | constructor(width: number, height: number); 8 | getKernelParams(params: KernelParams): KernelParams; 9 | } 10 | export declare class Writer extends PackImpl { 11 | constructor(width: number, height: number, interlaced: boolean); 12 | getKernelParams(params: KernelParams): KernelParams; 13 | } 14 | -------------------------------------------------------------------------------- /lib/process/switch.d.ts: -------------------------------------------------------------------------------- 1 | import { clContext as nodenCLContext, OpenCLBuffer, KernelParams, RunTimings } from 'nodencl'; 2 | export default class Switch { 3 | private readonly clContext; 4 | private readonly width; 5 | private readonly height; 6 | private readonly numInputs; 7 | private readonly numOverlays; 8 | private xform0; 9 | private xform1; 10 | private rgbaXf0; 11 | private rgbaXf1; 12 | private rgbaMx; 13 | private mixer; 14 | private wiper; 15 | private combiner; 16 | constructor(clContext: nodenCLContext, width: number, height: number, numInputs: number, numOverlays: number); 17 | init(): Promise; 18 | processFrame(inParams: Array, mixParams: KernelParams, overlays: Array, output: OpenCLBuffer, clQueue: number): Promise; 19 | } 20 | -------------------------------------------------------------------------------- /lib/process/switch.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* Copyright 2019 Streampunk Media Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | var __importDefault = (this && this.__importDefault) || function (mod) { 17 | return (mod && mod.__esModule) ? mod : { "default": mod }; 18 | }; 19 | Object.defineProperty(exports, "__esModule", { value: true }); 20 | const imageProcess_1 = __importDefault(require("./imageProcess")); 21 | const transform_1 = __importDefault(require("./transform")); 22 | const mix_1 = __importDefault(require("./mix")); 23 | const wipe_1 = __importDefault(require("./wipe")); 24 | const combine_1 = __importDefault(require("./combine")); 25 | class Switch { 26 | constructor(clContext, width, height, numInputs, numOverlays) { 27 | this.xform0 = null; 28 | this.xform1 = null; 29 | this.rgbaXf0 = null; 30 | this.rgbaXf1 = null; 31 | this.rgbaMx = null; 32 | this.mixer = null; 33 | this.wiper = null; 34 | this.combiner = null; 35 | this.clContext = clContext; 36 | this.width = width; 37 | this.height = height; 38 | this.numInputs = numInputs; 39 | this.numOverlays = numOverlays; 40 | } 41 | async init() { 42 | const numBytesRGBA = this.width * this.height * 4 * 4; 43 | this.xform0 = new imageProcess_1.default(this.clContext, new transform_1.default(this.clContext, this.width, this.height)); 44 | await this.xform0.init(); 45 | this.rgbaXf0 = await this.clContext.createBuffer(numBytesRGBA, 'readwrite', 'coarse', { 46 | width: this.width, 47 | height: this.height 48 | }, 'switch'); 49 | if (this.numInputs > 1) { 50 | this.xform1 = new imageProcess_1.default(this.clContext, new transform_1.default(this.clContext, this.width, this.height)); 51 | await this.xform1.init(); 52 | this.rgbaXf1 = await this.clContext.createBuffer(numBytesRGBA, 'readwrite', 'coarse', { 53 | width: this.width, 54 | height: this.height 55 | }, 'switch'); 56 | this.mixer = new imageProcess_1.default(this.clContext, new mix_1.default(this.width, this.height)); 57 | await this.mixer.init(); 58 | this.wiper = new imageProcess_1.default(this.clContext, new wipe_1.default(this.width, this.height)); 59 | await this.wiper.init(); 60 | } 61 | this.combiner = new imageProcess_1.default(this.clContext, new combine_1.default(this.width, this.height, this.numOverlays)); 62 | await this.combiner.init(); 63 | this.rgbaMx = await this.clContext.createBuffer(numBytesRGBA, 'readwrite', 'coarse', { 64 | width: this.width, 65 | height: this.height 66 | }, 'switch'); 67 | } 68 | async processFrame(inParams, mixParams, overlays, output, clQueue) { 69 | if (!(this.xform0 && this.xform1 && this.mixer && this.wiper && this.combiner)) 70 | throw new Error('Switch needs to be initialised'); 71 | inParams[0].output = this.rgbaXf0; 72 | await this.xform0.run(inParams[0], clQueue); 73 | if (this.numInputs > 1) { 74 | inParams[1].output = this.rgbaXf1; 75 | await this.xform1.run(inParams[1], clQueue); 76 | if (mixParams.wipe) { 77 | /*mixParams.frac*/ 78 | await this.wiper.run({ input0: this.rgbaXf0, input1: this.rgbaXf1, wipe: mixParams.frac, output: this.rgbaMx }, clQueue); 79 | } 80 | else { 81 | await this.mixer.run({ input0: this.rgbaXf0, input1: this.rgbaXf1, mix: mixParams.frac, output: this.rgbaMx }, clQueue); 82 | } 83 | } 84 | return await this.combiner.run({ bgIn: this.rgbaMx, ovIn: overlays, output: output }, clQueue); 85 | } 86 | } 87 | exports.default = Switch; 88 | -------------------------------------------------------------------------------- /lib/process/transform.d.ts: -------------------------------------------------------------------------------- 1 | import { ProcessImpl } from './imageProcess'; 2 | import { clContext as nodenCLContext, OpenCLBuffer, KernelParams } from 'nodencl'; 3 | export default class Transform extends ProcessImpl { 4 | clContext: nodenCLContext; 5 | transformMatrix: Array; 6 | transformArray: Float32Array; 7 | matrixBuffer: OpenCLBuffer | null; 8 | constructor(clContext: nodenCLContext, width: number, height: number); 9 | private updateMatrix; 10 | init(): Promise; 11 | getKernelParams(params: KernelParams, clQueue: number): Promise; 12 | } 13 | -------------------------------------------------------------------------------- /lib/process/transform.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* Copyright 2019 Streampunk Media Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | Object.defineProperty(exports, "__esModule", { value: true }); 17 | const imageProcess_1 = require("./imageProcess"); 18 | const colourMaths_1 = require("./colourMaths"); 19 | const transformKernel = ` 20 | __constant sampler_t samplerIn = 21 | CLK_NORMALIZED_COORDS_TRUE | 22 | CLK_ADDRESS_CLAMP | 23 | CLK_FILTER_LINEAR; 24 | 25 | __constant sampler_t samplerOut = 26 | CLK_NORMALIZED_COORDS_FALSE | 27 | CLK_ADDRESS_CLAMP | 28 | CLK_FILTER_NEAREST; 29 | 30 | __kernel void transform( 31 | __read_only image2d_t input, 32 | __global float4* restrict transformMatrix, 33 | __write_only image2d_t output) { 34 | 35 | int w = get_image_width(output); 36 | int h = get_image_height(output); 37 | 38 | // Load two rows of the 3x3 transform matrix via two float4s 39 | float4 tmpMat0 = transformMatrix[0]; 40 | float4 tmpMat1 = transformMatrix[1]; 41 | float3 mat0 = (float3)(tmpMat0.s0, tmpMat0.s1, tmpMat0.s2); 42 | float3 mat1 = (float3)(tmpMat0.s3, tmpMat1.s0, tmpMat1.s1); 43 | 44 | int outX = get_global_id(0); 45 | int outY = get_global_id(1); 46 | int2 posOut = {outX, outY}; 47 | 48 | float3 inPos = (float3)(outX / (float) w - 0.5f, outY / (float) h - 0.5f, 1.0f); 49 | float2 posIn = (float2)(dot(mat0, inPos) + 0.5f, dot(mat1, inPos) + 0.5f); 50 | 51 | float4 in = read_imagef(input, samplerIn, posIn); 52 | write_imagef(output, posOut, in); 53 | } 54 | `; 55 | class Transform extends imageProcess_1.ProcessImpl { 56 | constructor(clContext, width, height) { 57 | super('transform', width, height, transformKernel, 'transform'); 58 | this.matrixBuffer = null; 59 | this.clContext = clContext; 60 | this.transformMatrix = [...new Array(3)].map(() => new Float32Array(3)); 61 | this.transformMatrix[0] = Float32Array.from([1.0, 0.0, 0.0]); 62 | this.transformMatrix[1] = Float32Array.from([0.0, 1.0, 0.0]); 63 | this.transformMatrix[2] = Float32Array.from([0.0, 0.0, 1.0]); 64 | this.transformArray = colourMaths_1.matrixFlatten(this.transformMatrix); 65 | } 66 | async updateMatrix(clQueue) { 67 | if (!this.matrixBuffer) 68 | throw new Error('Transform needs to be initialised'); 69 | this.transformArray = colourMaths_1.matrixFlatten(this.transformMatrix); 70 | await this.matrixBuffer.hostAccess('writeonly', clQueue, Buffer.from(this.transformArray.buffer)); 71 | return this.matrixBuffer.hostAccess('none', clQueue); 72 | } 73 | async init() { 74 | this.matrixBuffer = await this.clContext.createBuffer(this.transformArray.byteLength, 'readonly', 'coarse'); 75 | return this.updateMatrix(this.clContext.queue.load); 76 | } 77 | async getKernelParams(params, clQueue) { 78 | const aspect = this.width / this.height; 79 | const flipX = params.flipH || false ? -1.0 : 1.0; 80 | const flipY = params.flipV || false ? -1.0 : 1.0; 81 | const scaleX = (params.scale || 1.0) * flipX * aspect; 82 | const scaleY = (params.scale || 1.0) * flipY; 83 | const offsetX = params.offsetX || 0.0; 84 | const offsetY = params.offsetY || 0.0; 85 | const rotate = params.rotate || 0.0; 86 | const scaleMatrix = [...new Array(3)].map(() => new Float32Array(3)); 87 | scaleMatrix[0] = Float32Array.from([1.0 / scaleX, 0.0, 0.0]); 88 | scaleMatrix[1] = Float32Array.from([0.0, 1.0 / scaleY, 0.0]); 89 | scaleMatrix[2] = Float32Array.from([0.0, 0.0, 1.0]); 90 | const translateMatrix = [...new Array(3)].map(() => new Float32Array(3)); 91 | translateMatrix[0] = Float32Array.from([1.0, 0.0, offsetX]); 92 | translateMatrix[1] = Float32Array.from([0.0, 1.0, offsetY]); 93 | translateMatrix[2] = Float32Array.from([0.0, 0.0, 1.0]); 94 | const rotateMatrix = [...new Array(3)].map(() => new Float32Array(3)); 95 | rotateMatrix[0] = Float32Array.from([Math.cos(rotate), -Math.sin(rotate), 0.0]); 96 | rotateMatrix[1] = Float32Array.from([Math.sin(rotate), Math.cos(rotate), 0.0]); 97 | rotateMatrix[2] = Float32Array.from([0.0, 0.0, 1.0]); 98 | const projectMatrix = [...new Array(3)].map(() => new Float32Array(3)); 99 | projectMatrix[0] = Float32Array.from([aspect, 0.0, 0.0]); 100 | projectMatrix[1] = Float32Array.from([0.0, 1.0, 0.0]); 101 | projectMatrix[2] = Float32Array.from([0.0, 0.0, 1.0]); 102 | this.transformMatrix = colourMaths_1.matrixMultiply(colourMaths_1.matrixMultiply(colourMaths_1.matrixMultiply(scaleMatrix, translateMatrix), rotateMatrix), projectMatrix); 103 | await this.updateMatrix(clQueue); 104 | return Promise.resolve({ 105 | input: params.input, 106 | transformMatrix: this.matrixBuffer, 107 | output: params.output 108 | }); 109 | } 110 | } 111 | exports.default = Transform; 112 | -------------------------------------------------------------------------------- /lib/process/v210.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { PackImpl } from './packer'; 3 | import { KernelParams, OpenCLBuffer } from 'nodencl'; 4 | export declare function fillBuf(buf: Buffer, width: number, height: number): void; 5 | export declare function dumpBufUnpack(buf: OpenCLBuffer, width: number, numPixels: number, numLines: number): void; 6 | export declare function dumpBuf(buf: Buffer, width: number, numLines: number): void; 7 | export declare class Reader extends PackImpl { 8 | constructor(width: number, height: number); 9 | getKernelParams(params: KernelParams): KernelParams; 10 | } 11 | export declare class Writer extends PackImpl { 12 | constructor(width: number, height: number, interlaced: boolean); 13 | getKernelParams(params: KernelParams): KernelParams; 14 | } 15 | -------------------------------------------------------------------------------- /lib/process/wipe.d.ts: -------------------------------------------------------------------------------- 1 | import { ProcessImpl } from './imageProcess'; 2 | import { KernelParams } from 'nodencl'; 3 | export default class Wipe extends ProcessImpl { 4 | constructor(width: number, height: number); 5 | init(): Promise; 6 | getKernelParams(params: KernelParams): Promise; 7 | } 8 | -------------------------------------------------------------------------------- /lib/process/wipe.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* Copyright 2019 Streampunk Media Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | Object.defineProperty(exports, "__esModule", { value: true }); 17 | const imageProcess_1 = require("./imageProcess"); 18 | const wipeKernel = ` 19 | __constant sampler_t sampler1 = 20 | CLK_NORMALIZED_COORDS_FALSE 21 | | CLK_ADDRESS_CLAMP_TO_EDGE 22 | | CLK_FILTER_NEAREST; 23 | 24 | __kernel void wipe( 25 | __read_only image2d_t input0, 26 | __read_only image2d_t input1, 27 | __private float wipe, 28 | __write_only image2d_t output) { 29 | 30 | int w = get_image_width(output); 31 | int h = get_image_height(output); 32 | 33 | int x = get_global_id(0); 34 | int y = get_global_id(1); 35 | float4 in0 = read_imagef(input0, sampler1, (int2)(x,y)); 36 | float4 in1 = read_imagef(input1, sampler1, (int2)(x,y)); 37 | 38 | float4 out = x > w * wipe ? in1 : in0; 39 | 40 | write_imagef(output, (int2)(x, y), out); 41 | }; 42 | `; 43 | class Wipe extends imageProcess_1.ProcessImpl { 44 | constructor(width, height) { 45 | super('wipe', width, height, wipeKernel, 'wipe'); 46 | } 47 | async init() { 48 | return Promise.resolve(); 49 | } 50 | async getKernelParams(params) { 51 | return Promise.resolve({ 52 | input0: params.input0, 53 | input1: params.input1, 54 | wipe: params.wipe, 55 | output: params.output 56 | }); 57 | } 58 | } 59 | exports.default = Wipe; 60 | -------------------------------------------------------------------------------- /lib/process/yuv422p10.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { PackImpl } from './packer'; 3 | import { KernelParams } from 'nodencl'; 4 | export declare function fillBuf(buf: Buffer, width: number, height: number): void; 5 | export declare function dumpBuf(buf: Buffer, width: number, height: number, numLines: number): void; 6 | export declare class Reader extends PackImpl { 7 | constructor(width: number, height: number); 8 | getKernelParams(params: KernelParams): KernelParams; 9 | } 10 | export declare class Writer extends PackImpl { 11 | constructor(width: number, height: number, interlaced: boolean); 12 | getKernelParams(params: KernelParams): KernelParams; 13 | } 14 | -------------------------------------------------------------------------------- /lib/process/yuv422p8.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { PackImpl } from './packer'; 3 | import { KernelParams } from 'nodencl'; 4 | export declare function fillBuf(buf: Buffer, width: number, height: number): void; 5 | export declare function dumpBuf(buf: Buffer, width: number, height: number, numLines: number): void; 6 | export declare class Reader extends PackImpl { 7 | constructor(width: number, height: number); 8 | getKernelParams(params: KernelParams): KernelParams; 9 | } 10 | export declare class Writer extends PackImpl { 11 | constructor(width: number, height: number, interlaced: boolean); 12 | getKernelParams(params: KernelParams): KernelParams; 13 | } 14 | -------------------------------------------------------------------------------- /lib/producer/ffmpegProducer.d.ts: -------------------------------------------------------------------------------- 1 | import { SourceFrame } from '../chanLayer'; 2 | import { ProducerFactory, Producer } from './producer'; 3 | import { clContext as nodenCLContext } from 'nodencl'; 4 | import { RedioPipe } from 'redioactive'; 5 | export declare class FFmpegProducer implements Producer { 6 | private readonly id; 7 | private params; 8 | private clContext; 9 | private demuxer; 10 | private readonly decoders; 11 | private readonly filterers; 12 | private vidSource; 13 | private vidDecode; 14 | private vidFilter; 15 | private vidLoader; 16 | private vidProcess; 17 | private toRGBA; 18 | constructor(id: string, params: string[], context: nodenCLContext); 19 | initialise(): Promise | null>; 20 | } 21 | export declare class FFmpegProducerFactory implements ProducerFactory { 22 | private clContext; 23 | constructor(clContext: nodenCLContext); 24 | createProducer(id: string, params: string[]): FFmpegProducer; 25 | } 26 | -------------------------------------------------------------------------------- /lib/producer/producer.d.ts: -------------------------------------------------------------------------------- 1 | import { clContext as nodenCLContext } from 'nodencl'; 2 | import { ChanLayer, SourceFrame } from '../chanLayer'; 3 | import { RedioPipe } from 'redioactive'; 4 | export interface Producer { 5 | initialise(): Promise | null>; 6 | } 7 | export interface ProducerFactory { 8 | createProducer(id: string, params: string[]): T; 9 | } 10 | export declare class InvalidProducerError extends Error { 11 | constructor(message?: string); 12 | } 13 | export declare class ProducerRegistry { 14 | private readonly producerFactories; 15 | constructor(clContext: nodenCLContext); 16 | createSource(chanLay: ChanLayer, params: string[]): Promise | null>; 17 | } 18 | -------------------------------------------------------------------------------- /lib/producer/producer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* Copyright 2020 Streampunk Media Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | Object.defineProperty(exports, "__esModule", { value: true }); 17 | const ffmpegProducer_1 = require("./ffmpegProducer"); 18 | class InvalidProducerError extends Error { 19 | constructor(message) { 20 | super(message); 21 | // see: typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html 22 | Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain 23 | this.name = InvalidProducerError.name; // stack traces display correctly now 24 | } 25 | } 26 | exports.InvalidProducerError = InvalidProducerError; 27 | class ProducerRegistry { 28 | constructor(clContext) { 29 | this.producerFactories = []; 30 | this.producerFactories.push(new ffmpegProducer_1.FFmpegProducerFactory(clContext)); 31 | } 32 | async createSource(chanLay, params) { 33 | const id = `${chanLay.channel}-${chanLay.layer}`; 34 | let p = null; 35 | for (const f of this.producerFactories) { 36 | try { 37 | const producer = f.createProducer(id, params); 38 | if ((p = await producer.initialise()) !== null) 39 | break; 40 | } 41 | catch (err) { 42 | if (!(err instanceof InvalidProducerError)) { 43 | throw err; 44 | } 45 | } 46 | } 47 | if (p === null) { 48 | console.log(`Failed to find producer for params: '${params}'`); 49 | } 50 | return p; 51 | } 52 | } 53 | exports.ProducerRegistry = ProducerRegistry; 54 | -------------------------------------------------------------------------------- /lib/testing.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "casparcl", 3 | "version": "0.1.1", 4 | "description": "Implementing the features of CasparCG with Node.JS and OpenCL.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "lint": "eslint . --ext .js,.ts", 9 | "lint:fix": "yarn lint -- --fix" 10 | }, 11 | "repository": "https://github.com/Streampunk/casparcl.git", 12 | "author": "Richard Cartwright ", 13 | "license": "GPL-3.0-or-later", 14 | "dependencies": { 15 | "@koa/cors": "2", 16 | "beamcoder": "^0.5.2", 17 | "highland": "^2.13.5", 18 | "koa": "^2.11.0", 19 | "macadam": "^2.0.11", 20 | "nodencl": "^1.3.0", 21 | "osc": "^2.4.1", 22 | "redioactive": "^0.0.3", 23 | "request": "^2.88.0", 24 | "request-promise-native": "^1.0.8" 25 | }, 26 | "devDependencies": { 27 | "@types/highland": "^2.12.9", 28 | "@types/koa": "^2.11.3", 29 | "@types/koa__cors": "^3.0.1", 30 | "@types/node": "^12.12.35", 31 | "@typescript-eslint/eslint-plugin": "2.29.0", 32 | "@typescript-eslint/parser": "^2.29.0", 33 | "eslint": "^6.8.0", 34 | "eslint-config-prettier": "^6.11.0", 35 | "eslint-plugin-prettier": "^3.1.3", 36 | "prettier": "^2.0.5", 37 | "ts-node": "^8.9.0", 38 | "typescript": "^3.8.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /scratch/high_beam.js: -------------------------------------------------------------------------------- 1 | const H = require('highland') 2 | const beamy = require('beamcoder') 3 | const macadam = require('macadam') 4 | 5 | async function run() { 6 | let dm = await beamy.demuxer('file:../media/dpp/AS11_DPP_HD_EXAMPLE_1.mxf') 7 | console.log(dm) 8 | let dec = await beamy.decoder({ demuxer: dm, stream_index: 0 }) 9 | let enc = beamy.encoder({ 10 | name: 'v210', 11 | codec_id: 127, 12 | width: 1920, 13 | height: 1080, 14 | pix_fmt: 'yuv422p10le', 15 | bits_per_raw_sample: 20, 16 | time_base: [1, 25] 17 | }) 18 | console.log(enc, enc._codecPar) 19 | 20 | let playback = await macadam.playback({ 21 | deviceIndex: 0, // Index relative to the 'macadam.getDeviceInfo()' array 22 | displayMode: macadam.bmdModeHD1080i50, 23 | pixelFormat: macadam.bmdFormat10BitYUV 24 | }) 25 | 26 | let stamp = process.hrtime() 27 | 28 | H((push, next) => { 29 | dm.read().then((p) => { 30 | push(null, p) 31 | next() 32 | }) 33 | }) 34 | .filter((p) => p.stream_index === 0) 35 | .drop(2000) 36 | .flatMap((p) => H(dec.decode(p))) 37 | //.tap(x => console.log(x.frames, x.frames[0].data.map(x => x.length))) 38 | .flatMap((p) => { 39 | return H(enc.encode(p.frames)) 40 | }) 41 | .flatMap((p) => H(playback.displayFrame(p.packets[0].data))) 42 | .consume((err, x, push, next) => { 43 | let wait = 40 - process.hrtime(stamp)[1] / 1000000 44 | if (err) { 45 | push(err) 46 | next() 47 | } else if (x === H.nil) { 48 | push(null, x) 49 | } else { 50 | // console.log('wait', wait) 51 | setTimeout( 52 | () => { 53 | push(null, x) 54 | next() 55 | }, 56 | wait > 0 ? wait : 0 57 | ) 58 | } 59 | }) 60 | .each(() => { 61 | console.log(process.hrtime(stamp)) 62 | stamp = process.hrtime() 63 | }) 64 | .done(() => { 65 | dec.flush() 66 | }) 67 | } 68 | 69 | run() 70 | -------------------------------------------------------------------------------- /scratch/oscServer.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2019 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | const osc = require('osc') 17 | 18 | function oscServer(params) { 19 | this.port = params.port || 9876 20 | this.map = [] 21 | 22 | this.oscPort = new osc.UDPPort({ 23 | localAddress: '0.0.0.0', 24 | localPort: this.port, 25 | remotePort: this.port + 1, 26 | remoteAddress: params.remoteAddr 27 | }) 28 | 29 | this.oscPort.on('ready', () => console.log(`OSC listening on port ${this.port}`)) 30 | 31 | this.oscPort.on('message', (oscMessage /*, timeTag, info*/) => { 32 | const control = oscMessage.address 33 | const values = oscMessage.args 34 | console.log(`OSC message: '${control}' values: [ ${values} ]`) 35 | 36 | this.map.forEach((entry) => { 37 | const update = entry[control] 38 | if (update) update(values) 39 | }) 40 | }) 41 | 42 | this.oscPort.on('error', (err) => { 43 | console.log('OSC port error: ', err) 44 | }) 45 | 46 | this.oscPort.open() 47 | } 48 | 49 | oscServer.prototype.sendMsg = function (control, msg) { 50 | this.oscPort.send({ 51 | address: control, 52 | args: msg 53 | }) 54 | } 55 | 56 | oscServer.prototype.addControl = function (control, upd, set) { 57 | this.map.push({ [control]: upd }) 58 | this.sendMsg(control, set()) 59 | } 60 | 61 | oscServer.prototype.removeControl = function (control) { 62 | this.map.forEach((entry, index) => { 63 | if (entry[control]) this.map.splice(index, 1) 64 | }) 65 | } 66 | 67 | oscServer.prototype.close = function () { 68 | console.log('Closing OSC') 69 | this.oscPort.close() 70 | } 71 | 72 | module.exports = oscServer 73 | -------------------------------------------------------------------------------- /scratch/promise_delay.js: -------------------------------------------------------------------------------- 1 | const H = require('highland') 2 | 3 | const wait = (d) => (v) => 4 | new Promise((resolve) => { 5 | setTimeout(() => resolve(v), d) 6 | }) 7 | 8 | let genc = 0 9 | let sc = 0 10 | let stamp = process.hrtime() 11 | 12 | let eagerPromer = (p) => { 13 | let ep = (err, x, push, next) => { 14 | console.log('>>> EAGER <<<', sc) 15 | if (err) { 16 | push(err) 17 | next() 18 | } else if (x === H.nil) { 19 | push(null, x) 20 | } else { 21 | next() 22 | p(x).then((m) => { 23 | push(null, m) 24 | }) 25 | } 26 | } 27 | return H.consume(ep) 28 | } 29 | 30 | H((push, next) => { 31 | console.log('*** GENERATOR ***', genc++) 32 | wait(1000)('Waited').then((m) => { 33 | // console.log('*** GENDONE ***', (genc - 1)) 34 | next() 35 | push(null, m) 36 | }) 37 | }) 38 | .through(eagerPromer(wait(1500))) // decode(x) 39 | .each((m) => { 40 | console.log(m, process.hrtime(stamp)) 41 | stamp = process.hrtime() 42 | }) 43 | -------------------------------------------------------------------------------- /scratch/rgba8-bgra8Test.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | const addon = require('nodencl') 17 | const io = require('../lib/process/io.js') 18 | const rgba8_io = require('../lib/process/rgba8.js') 19 | const bgra8_io = require('../lib/process/bgra8.js') 20 | 21 | function dumpFloatBuf(buf, width, height, numPixels, numLines) { 22 | const r = (b, o) => b.readFloatLE(o).toFixed(4) 23 | for (let y = 0; y < numLines; ++y) { 24 | const off = y * width * 4 * 4 25 | let s = `Line ${y}: ${r(buf, off)}` 26 | for (let i = 1; i < numPixels * 4; ++i) s += `, ${r(buf, off + i * 4)}` 27 | console.log(s) 28 | } 29 | } 30 | 31 | async function noden() { 32 | const platformIndex = 1 33 | const deviceIndex = 0 34 | const context = new addon.clContext({ 35 | platformIndex: platformIndex, 36 | deviceIndex: deviceIndex 37 | }) 38 | await context.initialise() 39 | const platformInfo = context.getPlatformInfo() 40 | // console.log(JSON.stringify(platformInfo, null, 2)); 41 | console.log(platformInfo.vendor, platformInfo.devices[deviceIndex].type) 42 | 43 | const colSpecRead = 'sRGB' 44 | const colSpecWrite = '709' 45 | const width = 1920 46 | const height = 1080 47 | 48 | const rgba8Loader = new io.ToRGBA( 49 | context, 50 | colSpecRead, 51 | colSpecWrite, 52 | new rgba8_io.Reader(width, height) 53 | ) 54 | await rgba8Loader.init() 55 | 56 | const bgra8Saver = new io.FromRGBA( 57 | context, 58 | colSpecWrite, 59 | new bgra8_io.Writer(width, height, false) 60 | ) 61 | await bgra8Saver.init() 62 | 63 | const rgba8Srcs = await rgba8Loader.createSources() 64 | const rgba8Src = rgba8Srcs[0] 65 | await rgba8Src.hostAccess('writeonly') 66 | rgba8_io.fillBuf(rgba8Src, width, height) 67 | rgba8_io.dumpBuf(rgba8Src, width, 4) 68 | 69 | const rgbaDst = await rgba8Loader.createDest({ width: width, height: height }) 70 | const bgra8Dsts = await bgra8Saver.createDests() 71 | 72 | let timings = await rgba8Loader.processFrame(rgba8Srcs, rgbaDst) 73 | console.log( 74 | `${timings.dataToKernel}, ${timings.kernelExec}, ${timings.dataFromKernel}, ${timings.totalTime}` 75 | ) 76 | 77 | await rgbaDst.hostAccess('readonly') 78 | dumpFloatBuf(rgbaDst, width, height, 2, 8) 79 | 80 | timings = await bgra8Saver.processFrame(rgbaDst, bgra8Dsts) 81 | console.log( 82 | `${timings.dataToKernel}, ${timings.kernelExec}, ${timings.dataFromKernel}, ${timings.totalTime}` 83 | ) 84 | 85 | const bgra8Dst = bgra8Dsts[0] 86 | await bgra8Dst.hostAccess('readonly') 87 | bgra8_io.dumpBuf(bgra8Dst, width, 8) 88 | 89 | return [rgba8Src, bgra8Dst] 90 | } 91 | noden() 92 | .then(([i, o]) => [i.creationTime, o.creationTime]) 93 | .then(([ict, oct]) => { 94 | if (global.gc) global.gc() 95 | console.log(ict, oct) 96 | }) 97 | .catch(console.error) 98 | -------------------------------------------------------------------------------- /scratch/v210ImageTest.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | const addon = require('nodencl') 17 | const io = require('../lib/process/io.js') 18 | const v210_io = require('../lib/process/v210.js') 19 | 20 | const testImage = ` 21 | __constant sampler_t sampler = 22 | CLK_NORMALIZED_COORDS_FALSE 23 | | CLK_ADDRESS_CLAMP_TO_EDGE 24 | | CLK_FILTER_NEAREST; 25 | 26 | __kernel void 27 | testImage(__read_only image2d_t input, 28 | __write_only image2d_t output) { 29 | 30 | int x = get_global_id(0); 31 | int y = get_global_id(1); 32 | float4 in = read_imagef(input, sampler, (int2)(x,y)); 33 | write_imagef(output, (int2)(x,y), in); 34 | } 35 | ` 36 | 37 | function dumpFloatBuf(buf, width, numPixels, numLines) { 38 | let lineOff = 0 39 | const r = (o) => buf.readFloatLE(lineOff + o).toFixed(4) 40 | for (let y = 0; y < numLines; ++y) { 41 | lineOff = y * width * 4 * 4 42 | let s = `Line ${y}: ${r(0)}` 43 | for (let i = 1; i < numPixels * 4; ++i) s += `, ${r(i * 4)}` 44 | s += ` ... ${r(128)}` 45 | for (let i = 1; i < numPixels * 4; ++i) s += `, ${r(128 + i * 4)}` 46 | console.log(s) 47 | } 48 | } 49 | 50 | async function noden() { 51 | const platformIndex = 1 52 | const deviceIndex = 0 53 | const context = new addon.clContext({ 54 | platformIndex: platformIndex, 55 | deviceIndex: deviceIndex 56 | }) 57 | await context.initialise() 58 | const platformInfo = context.getPlatformInfo() 59 | // console.log(JSON.stringify(platformInfo, null, 2)); 60 | console.log(platformInfo.vendor, platformInfo.devices[deviceIndex].type) 61 | 62 | const colSpecRead = '709' 63 | const colSpecWrite = '2020' 64 | const width = 1920 65 | const height = 1080 66 | 67 | const v210Loader = new io.ToRGBA( 68 | context, 69 | colSpecRead, 70 | colSpecWrite, 71 | new v210_io.Reader(width, height) 72 | ) 73 | await v210Loader.init() 74 | 75 | const v210Saver = new io.FromRGBA(context, colSpecWrite, new v210_io.Writer(width, height, false)) 76 | await v210Saver.init() 77 | 78 | // const globalWorkItems = Uint32Array.from([ width, height ]); 79 | const testImageProgram = await context.createProgram(testImage, { 80 | globalWorkItems: Uint32Array.from([width, height]) 81 | }) 82 | 83 | const v210Srcs = await v210Loader.createSources() 84 | const rgbaDst = await v210Loader.createDest({ width: width, height: height }) 85 | 86 | const imageDst = await context.createBuffer(v210Loader.getNumBytesRGBA(), 'readwrite', 'coarse', { 87 | width: width, 88 | height: height 89 | }) 90 | 91 | const v210Dsts = await v210Saver.createDests() 92 | 93 | const v210Src = v210Srcs[0] 94 | await v210Src.hostAccess('writeonly') 95 | v210_io.fillBuf(v210Src, width, height) 96 | v210_io.dumpBuf(v210Src, width, 4) 97 | 98 | let timings = await v210Loader.processFrame(v210Srcs, rgbaDst) 99 | console.log( 100 | `${timings.dataToKernel}, ${timings.kernelExec}, ${timings.dataFromKernel}, ${timings.totalTime}` 101 | ) 102 | 103 | await rgbaDst.hostAccess('readonly') 104 | dumpFloatBuf(rgbaDst, width, 2, 4) 105 | 106 | timings = await testImageProgram.run({ input: rgbaDst, output: imageDst }) 107 | console.log( 108 | `${timings.dataToKernel}, ${timings.kernelExec}, ${timings.dataFromKernel}, ${timings.totalTime}` 109 | ) 110 | 111 | await imageDst.hostAccess('readonly') 112 | dumpFloatBuf(imageDst, width, 2, 4) 113 | 114 | timings = await v210Saver.processFrame(imageDst, v210Dsts) 115 | console.log( 116 | `${timings.dataToKernel}, ${timings.kernelExec}, ${timings.dataFromKernel}, ${timings.totalTime}` 117 | ) 118 | 119 | const v210Dst = v210Dsts[0] 120 | await v210Dst.hostAccess('readonly') 121 | v210_io.dumpBuf(v210Dst, width, 4) 122 | 123 | await v210Src.hostAccess('readonly') 124 | console.log('Compare returned', v210Src.compare(v210Dst)) 125 | 126 | return [v210Src, v210Dst] 127 | } 128 | noden() 129 | .then(([i, o]) => [i.creationTime, o.creationTime]) 130 | .then(([ict, oct]) => { 131 | if (global.gc) global.gc() 132 | console.log(ict, oct) 133 | }) 134 | .catch(console.error) 135 | -------------------------------------------------------------------------------- /scratch/v210Test.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2019 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | const addon = require('nodencl') 17 | const io = require('../lib/process/io.js') 18 | const v210_io = require('../lib/process/v210.js') 19 | 20 | function dumpFloatBuf(buf, width, numPixels, numLines) { 21 | const r = (b, o) => b.readFloatLE(o).toFixed(4) 22 | for (let y = 0; y < numLines; ++y) { 23 | const off = y * width * 4 * 4 24 | let s = `Line ${y}: ${r(buf, off)}` 25 | for (let i = 1; i < numPixels * 4; ++i) s += `, ${r(buf, off + i * 4)}` 26 | console.log(s) 27 | } 28 | } 29 | 30 | async function noden() { 31 | const platformIndex = 1 32 | const deviceIndex = 0 33 | const context = new addon.clContext({ 34 | platformIndex: platformIndex, 35 | deviceIndex: deviceIndex 36 | }) 37 | await context.initialise() 38 | const platformInfo = context.getPlatformInfo() 39 | // console.log(JSON.stringify(platformInfo, null, 2)); 40 | console.log(platformInfo.vendor, platformInfo.devices[deviceIndex].type) 41 | 42 | const colSpecRead = '709' 43 | const colSpecWrite = '709' 44 | const width = 1920 45 | const height = 1080 46 | 47 | const v210Loader = new io.ToRGBA( 48 | context, 49 | colSpecRead, 50 | colSpecWrite, 51 | new v210_io.Reader(width, height) 52 | ) 53 | await v210Loader.init() 54 | 55 | const v210Saver = new io.FromRGBA(context, colSpecWrite, new v210_io.Writer(width, height, false)) 56 | await v210Saver.init() 57 | 58 | const v210Srcs = await v210Loader.createSources() 59 | const rgbaDst = await v210Loader.createDest({ width: width, height: height }) 60 | 61 | const v210Dsts = await v210Saver.createDests() 62 | 63 | const v210Src = v210Srcs[0] 64 | await v210Src.hostAccess('writeonly') 65 | v210_io.fillBuf(v210Src, width, height) 66 | v210_io.dumpBuf(v210Src, width, 4) 67 | 68 | let timings = await v210Loader.processFrame(v210Srcs, rgbaDst) 69 | console.log( 70 | `${timings.dataToKernel}, ${timings.kernelExec}, ${timings.dataFromKernel}, ${timings.totalTime}` 71 | ) 72 | 73 | await rgbaDst.hostAccess('readonly') 74 | dumpFloatBuf(rgbaDst, width, 2, 4) 75 | 76 | timings = await v210Saver.processFrame(rgbaDst, v210Dsts) 77 | console.log( 78 | `${timings.dataToKernel}, ${timings.kernelExec}, ${timings.dataFromKernel}, ${timings.totalTime}` 79 | ) 80 | 81 | const v210Dst = v210Dsts[0] 82 | await v210Dst.hostAccess('readonly') 83 | v210_io.dumpBuf(v210Dst, width, 4) 84 | 85 | await v210Src.hostAccess('readonly') 86 | console.log('Compare returned', v210Src.compare(v210Dst)) 87 | 88 | return [v210Src, v210Dst] 89 | } 90 | noden() 91 | .then(([i, o]) => [i.creationTime, o.creationTime]) 92 | .then(([ict, oct]) => { 93 | if (global.gc) global.gc() 94 | console.log(ict, oct) 95 | }) 96 | .catch(console.error) 97 | -------------------------------------------------------------------------------- /scratch/yuv422p10-bgra8Test.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | const addon = require('nodencl') 17 | const io = require('../lib/process/io.js') 18 | const yuv422p10_io = require('../lib/process/yuv422p10.js') 19 | const bgra8_io = require('../lib/process/bgra8.js') 20 | 21 | function dumpFloatBuf(buf, width, height, numPixels, numLines) { 22 | const r = (b, o) => b.readFloatLE(o).toFixed(4) 23 | for (let y = 0; y < numLines; ++y) { 24 | const off = y * width * 4 * 4 25 | let s = `Line ${y}: ${r(buf, off)}` 26 | for (let i = 1; i < numPixels * 4; ++i) s += `, ${r(buf, off + i * 4)}` 27 | console.log(s) 28 | } 29 | } 30 | 31 | async function noden() { 32 | const platformIndex = 1 33 | const deviceIndex = 0 34 | const context = new addon.clContext({ 35 | platformIndex: platformIndex, 36 | deviceIndex: deviceIndex 37 | }) 38 | await context.initialise() 39 | const platformInfo = context.getPlatformInfo() 40 | // console.log(JSON.stringify(platformInfo, null, 2)); 41 | console.log(platformInfo.vendor, platformInfo.devices[deviceIndex].type) 42 | 43 | const colSpecRead = '709' 44 | const colSpecWrite = 'sRGB' 45 | const width = 1920 46 | const height = 1080 47 | 48 | const yuv422p10Loader = new io.ToRGBA( 49 | context, 50 | colSpecRead, 51 | colSpecWrite, 52 | new yuv422p10_io.Reader(width, height) 53 | ) 54 | await yuv422p10Loader.init() 55 | 56 | const bgra8Saver = new io.FromRGBA( 57 | context, 58 | colSpecWrite, 59 | new bgra8_io.Writer(width, height, false) 60 | ) 61 | await bgra8Saver.init() 62 | 63 | const srcs = await yuv422p10Loader.createSources() 64 | const rgbaDst = await yuv422p10Loader.createDest({ width: width, height: height }) 65 | 66 | const bgra8Dsts = await bgra8Saver.createDests() 67 | 68 | const numBytes = yuv422p10Loader.getNumBytes() 69 | const lumaBytes = numBytes[0] 70 | const chromaBytes = numBytes[1] 71 | const numBytesyuv422p10 = yuv422p10Loader.getTotalBytes() 72 | const yuv422p10Src = Buffer.allocUnsafe(numBytesyuv422p10) 73 | yuv422p10_io.fillBuf(yuv422p10Src, width, height) 74 | yuv422p10_io.dumpBuf(yuv422p10Src, width, height, 4) 75 | 76 | await srcs[0].hostAccess('writeonly', 0, yuv422p10Src.slice(0, lumaBytes)) 77 | await srcs[1].hostAccess('writeonly', 0, yuv422p10Src.slice(lumaBytes, lumaBytes + chromaBytes)) 78 | await srcs[2].hostAccess( 79 | 'writeonly', 80 | 0, 81 | yuv422p10Src.slice(lumaBytes + chromaBytes, lumaBytes + chromaBytes * 2) 82 | ) 83 | 84 | let timings = await yuv422p10Loader.processFrame(srcs, rgbaDst) 85 | console.log( 86 | `${timings.dataToKernel}, ${timings.kernelExec}, ${timings.dataFromKernel}, ${timings.totalTime}` 87 | ) 88 | 89 | await rgbaDst.hostAccess('readonly') 90 | dumpFloatBuf(rgbaDst, width, height, 2, 8) 91 | 92 | timings = await bgra8Saver.processFrame(rgbaDst, bgra8Dsts) 93 | console.log( 94 | `${timings.dataToKernel}, ${timings.kernelExec}, ${timings.dataFromKernel}, ${timings.totalTime}` 95 | ) 96 | 97 | const bgra8Dst = bgra8Dsts[0] 98 | await bgra8Dst.hostAccess('readonly') 99 | bgra8_io.dumpBuf(bgra8Dst, width, 8) 100 | 101 | return [srcs[0], bgra8Dst] 102 | } 103 | noden() 104 | .then(([i, o]) => [i.creationTime, o.creationTime]) 105 | .then(([ict, oct]) => { 106 | if (global.gc) global.gc() 107 | console.log(ict, oct) 108 | }) 109 | .catch(console.error) 110 | -------------------------------------------------------------------------------- /scratch/yuv422p10-v210Test.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | const addon = require('nodencl') 17 | const io = require('../lib/process/io.js') 18 | const yuv422p10_io = require('../lib/process/yuv422p10.js') 19 | const v210_io = require('../lib/process/v210.js') 20 | 21 | function dumpFloatBuf(buf, width, height, numPixels, numLines) { 22 | const r = (b, o) => b.readFloatLE(o).toFixed(4) 23 | for (let y = 0; y < numLines; ++y) { 24 | const off = y * width * 4 * 4 25 | let s = `Line ${y}: ${r(buf, off)}` 26 | for (let i = 1; i < numPixels * 4; ++i) s += `, ${r(buf, off + i * 4)}` 27 | console.log(s) 28 | } 29 | } 30 | 31 | async function noden() { 32 | const platformIndex = 1 33 | const deviceIndex = 0 34 | const context = new addon.clContext({ 35 | platformIndex: platformIndex, 36 | deviceIndex: deviceIndex 37 | }) 38 | await context.initialise() 39 | const platformInfo = context.getPlatformInfo() 40 | // console.log(JSON.stringify(platformInfo, null, 2)); 41 | console.log(platformInfo.vendor, platformInfo.devices[deviceIndex].type) 42 | 43 | const colSpecRead = '709' 44 | const colSpecWrite = '2020' 45 | const width = 1920 46 | const height = 1080 47 | 48 | const yuv422p10Loader = new io.ToRGBA( 49 | context, 50 | colSpecRead, 51 | colSpecWrite, 52 | new yuv422p10_io.Reader(width, height) 53 | ) 54 | await yuv422p10Loader.init() 55 | 56 | const v210Saver = new io.FromRGBA(context, colSpecWrite, new v210_io.Writer(width, height, false)) 57 | await v210Saver.init() 58 | 59 | const srcs = await yuv422p10Loader.createSources() 60 | const rgbaDst = await yuv422p10Loader.createDest({ width: width, height: height }) 61 | 62 | const v210Dsts = await v210Saver.createDests() 63 | 64 | const numBytes = yuv422p10Loader.getNumBytes() 65 | const lumaBytes = numBytes[0] 66 | const chromaBytes = numBytes[1] 67 | const numBytesyuv422p10 = yuv422p10Loader.getTotalBytes() 68 | const yuv422p10Src = Buffer.allocUnsafe(numBytesyuv422p10) 69 | yuv422p10_io.fillBuf(yuv422p10Src, width, height) 70 | yuv422p10_io.dumpBuf(yuv422p10Src, width, height, 4) 71 | 72 | await srcs[0].hostAccess('writeonly', 0, yuv422p10Src.slice(0, lumaBytes)) 73 | await srcs[1].hostAccess('writeonly', 0, yuv422p10Src.slice(lumaBytes, lumaBytes + chromaBytes)) 74 | await srcs[2].hostAccess( 75 | 'writeonly', 76 | 0, 77 | yuv422p10Src.slice(lumaBytes + chromaBytes, lumaBytes + chromaBytes * 2) 78 | ) 79 | 80 | let timings = await yuv422p10Loader.processFrame(srcs, rgbaDst) 81 | console.log( 82 | `${timings.dataToKernel}, ${timings.kernelExec}, ${timings.dataFromKernel}, ${timings.totalTime}` 83 | ) 84 | 85 | await rgbaDst.hostAccess('readonly') 86 | dumpFloatBuf(rgbaDst, width, height, 2, 4) 87 | 88 | timings = await v210Saver.processFrame(rgbaDst, v210Dsts) 89 | console.log( 90 | `${timings.dataToKernel}, ${timings.kernelExec}, ${timings.dataFromKernel}, ${timings.totalTime}` 91 | ) 92 | 93 | const v210Dst = v210Dsts[0] 94 | await v210Dst.hostAccess('readonly') 95 | v210_io.dumpBuf(v210Dst, width, 4) 96 | 97 | return [srcs[0], v210Dst] 98 | } 99 | noden() 100 | .then(([i, o]) => [i.creationTime, o.creationTime]) 101 | .then(([ict, oct]) => { 102 | if (global.gc) global.gc() 103 | console.log(ict, oct) 104 | }) 105 | .catch(console.error) 106 | -------------------------------------------------------------------------------- /scratch/yuv422p10Test.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | const addon = require('nodencl') 17 | const io = require('../lib/process/io.js') 18 | const yuv422p10_io = require('../lib/process/yuv422p10.js') 19 | 20 | function dumpFloatBuf(buf, width, height, numPixels, numLines) { 21 | const r = (b, o) => b.readFloatLE(o).toFixed(4) 22 | for (let y = 0; y < numLines; ++y) { 23 | const off = y * width * 4 * 4 24 | let s = `Line ${y}: ${r(buf, off)}` 25 | for (let i = 1; i < numPixels * 4; ++i) s += `, ${r(buf, off + i * 4)}` 26 | console.log(s) 27 | } 28 | } 29 | 30 | async function noden() { 31 | const platformIndex = 1 32 | const deviceIndex = 0 33 | const context = new addon.clContext({ 34 | platformIndex: platformIndex, 35 | deviceIndex: deviceIndex 36 | }) 37 | await context.initialise() 38 | const platformInfo = context.getPlatformInfo() 39 | // console.log(JSON.stringify(platformInfo, null, 2)); 40 | console.log(platformInfo.vendor, platformInfo.devices[deviceIndex].type) 41 | 42 | const colSpecRead = '709' 43 | const colSpecWrite = '709' 44 | const width = 1920 45 | const height = 1080 46 | 47 | const yuv422p10Loader = new io.ToRGBA( 48 | context, 49 | colSpecRead, 50 | colSpecWrite, 51 | new yuv422p10_io.Reader(width, height) 52 | ) 53 | await yuv422p10Loader.init() 54 | 55 | const yuv422p10Saver = new io.FromRGBA( 56 | context, 57 | colSpecWrite, 58 | new yuv422p10_io.Writer(width, height, false) 59 | ) 60 | await yuv422p10Saver.init() 61 | 62 | const srcs = await yuv422p10Loader.createSources() 63 | const rgbaDst = await yuv422p10Loader.createDest({ width: width, height: height }) 64 | 65 | const dsts = await yuv422p10Saver.createDests() 66 | 67 | const numBytes = yuv422p10Loader.getNumBytes() 68 | const lumaBytes = numBytes[0] 69 | const chromaBytes = numBytes[1] 70 | const numBytesyuv422p10 = yuv422p10Loader.getTotalBytes() 71 | const yuv422p10Src = Buffer.allocUnsafe(numBytesyuv422p10) 72 | yuv422p10_io.fillBuf(yuv422p10Src, width, height) 73 | yuv422p10_io.dumpBuf(yuv422p10Src, width, height, 4) 74 | 75 | await srcs[0].hostAccess('writeonly', 0, yuv422p10Src.slice(0, lumaBytes)) 76 | await srcs[1].hostAccess('writeonly', 0, yuv422p10Src.slice(lumaBytes, lumaBytes + chromaBytes)) 77 | await srcs[2].hostAccess( 78 | 'writeonly', 79 | 0, 80 | yuv422p10Src.slice(lumaBytes + chromaBytes, lumaBytes + chromaBytes * 2) 81 | ) 82 | 83 | let timings = await yuv422p10Loader.processFrame(srcs, rgbaDst) 84 | console.log( 85 | `${timings.dataToKernel}, ${timings.kernelExec}, ${timings.dataFromKernel}, ${timings.totalTime}` 86 | ) 87 | 88 | await rgbaDst.hostAccess('readonly') 89 | dumpFloatBuf(rgbaDst, width, height, 2, 4) 90 | 91 | timings = await yuv422p10Saver.processFrame(rgbaDst, dsts) 92 | console.log( 93 | `${timings.dataToKernel}, ${timings.kernelExec}, ${timings.dataFromKernel}, ${timings.totalTime}` 94 | ) 95 | 96 | await dsts[0].hostAccess('readonly') 97 | await dsts[1].hostAccess('readonly') 98 | await dsts[2].hostAccess('readonly') 99 | const yuv422p10Dst = Buffer.concat(dsts, numBytesyuv422p10) 100 | yuv422p10_io.dumpBuf(yuv422p10Dst, width, height, 4) 101 | 102 | await srcs[0].hostAccess('readonly') 103 | await srcs[1].hostAccess('readonly') 104 | await srcs[2].hostAccess('readonly') 105 | console.log('Compare returned', yuv422p10Src.compare(yuv422p10Dst)) 106 | 107 | return [srcs[0], dsts[0]] 108 | } 109 | noden() 110 | .then(([i, o]) => [i.creationTime, o.creationTime]) 111 | .then(([ict, oct]) => { 112 | if (global.gc) global.gc() 113 | console.log(ict, oct) 114 | }) 115 | .catch(console.error) 116 | -------------------------------------------------------------------------------- /src/AMCP/basic.ts: -------------------------------------------------------------------------------- 1 | /* Copyright 2020 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | import { clContext as nodenCLContext } from 'nodencl' 17 | import { Commands } from './commands' 18 | import { ChanLayer } from '../chanLayer' 19 | import { Channel } from '../channel' 20 | 21 | export class Basic { 22 | private readonly channels: Array 23 | 24 | constructor(clContext: nodenCLContext) { 25 | this.channels = Array.from([1, 2, 3, 4], (c) => new Channel(clContext, c)) 26 | } 27 | 28 | /** Add the supported basic transport commands */ 29 | addCmds(commands: Commands): void { 30 | commands.add({ cmd: 'LOADBG', fn: this.loadbg.bind(this) }) 31 | commands.add({ cmd: 'LOAD', fn: this.load.bind(this) }) 32 | commands.add({ cmd: 'PLAY', fn: this.play.bind(this) }) 33 | commands.add({ cmd: 'PAUSE', fn: this.pause.bind(this) }) 34 | commands.add({ cmd: 'RESUME', fn: this.resume.bind(this) }) 35 | commands.add({ cmd: 'STOP', fn: this.stop.bind(this) }) 36 | commands.add({ cmd: 'CLEAR', fn: this.clear.bind(this) }) 37 | } 38 | 39 | /** 40 | * Loads a producer in the background and prepares it for playout. If no layer is specified the default layer index will be used. 41 | * 42 | * _clip_ will be parsed by available registered producer factories. If a successfully match is found, the producer will be loaded into the background. 43 | * If a file with the same name (extension excluded) but with the additional postfix _a is found this file will be used as key for the main clip. 44 | * 45 | * _loop_ will cause the clip to loop. 46 | * When playing and looping the clip will start at _frame_. 47 | * When playing and loop the clip will end after _frames_ number of frames. 48 | * 49 | * _auto_ will cause the clip to automatically start when foreground clip has ended (without play). 50 | * The clip is considered "started" after the optional transition has ended. 51 | * 52 | * Note: only one clip can be queued to play automatically per layer. 53 | */ 54 | async loadbg(chanLay: ChanLayer, params: string[]): Promise { 55 | if (!chanLay.valid) return Promise.resolve(false) 56 | 57 | let curParam = 0 58 | const clip = params[curParam++] 59 | const loop = params.find((param) => param === 'LOOP') !== undefined 60 | const autoPlay = params.find((param) => param === 'AUTO') !== undefined 61 | console.log(`loadbg: clip '${clip}', loop ${loop}, auto play ${autoPlay}`) 62 | 63 | const bgOK = this.channels[chanLay.channel - 1].createSource(chanLay, params) 64 | 65 | return bgOK 66 | } 67 | 68 | /** 69 | * Loads a clip to the foreground and plays the first frame before pausing. 70 | * If any clip is playing on the target foreground then this clip will be replaced. 71 | */ 72 | async load(chanLay: ChanLayer, params: string[]): Promise { 73 | if (!chanLay.valid) return Promise.resolve(false) 74 | 75 | const bgOK = this.channels[chanLay.channel - 1].createSource(chanLay, params) 76 | 77 | return bgOK 78 | } 79 | 80 | /** 81 | * Moves clip from background to foreground and starts playing it. 82 | * If a transition (see LOADBG) is prepared, it will be executed. 83 | * If additional parameters (see LOADBG) are provided then the provided clip will first be loaded to the background. 84 | */ 85 | async play(chanLay: ChanLayer, params: string[]): Promise { 86 | if (!chanLay.valid) return Promise.resolve(false) 87 | 88 | if (params.length !== 0) await this.loadbg(chanLay, params) 89 | 90 | const fgOK = this.channels[chanLay.channel - 1].play() 91 | 92 | return fgOK 93 | } 94 | 95 | /** Pauses playback of the foreground clip on the specified layer. The RESUME command can be used to resume playback again. */ 96 | async pause(chanLay: ChanLayer, params: string[]): Promise { 97 | console.log('pause', params) 98 | return chanLay.valid 99 | } 100 | 101 | /** Resumes playback of a foreground clip previously paused with the PAUSE command. */ 102 | async resume(chanLay: ChanLayer, params: string[]): Promise { 103 | console.log('resume', params) 104 | return chanLay.valid 105 | } 106 | 107 | /** Removes the foreground clip of the specified layer */ 108 | async stop(chanLay: ChanLayer, params: string[]): Promise { 109 | console.log('stop', params) 110 | return chanLay.valid 111 | } 112 | 113 | /** 114 | * Removes all clips (both foreground and background) of the specified layer. 115 | * If no layer is specified then all layers in the specified video_channel are cleared. 116 | */ 117 | async clear(chanLay: ChanLayer, params: string[]): Promise { 118 | console.log('clear', params) 119 | return chanLay.valid 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/AMCP/cmdResponses.ts: -------------------------------------------------------------------------------- 1 | import * as responses from './testResponses' 2 | 3 | export interface Responses { 4 | [command: string]: ((req: string[] | null) => string) | Responses 5 | } 6 | 7 | export const responses218: Responses = { 8 | LOADBG: () => '202 LOADBG OK', 9 | LOAD: () => '202 LOAD OK', 10 | PLAY: () => '202 PLAY OK', 11 | PAUSE: () => '202 PAUSE OK', 12 | RESUME: () => '202 RESUME OK', 13 | STOP: () => '202 STOP OK', 14 | CLEAR: () => '202 CLEAR OK', 15 | CALL: () => 'CALL', // TODO 16 | SWAP: () => 'SWAP', // TODO 17 | ADD: () => '202 ADD OK', 18 | REMOVE: () => '202 REMOVE OK', 19 | PRINT: () => '202 PRINT OK', 20 | LOG: { 21 | LEVEL: (c): string => 22 | c && 23 | c.length === 3 && 24 | c[2].toLowerCase() in ['trace', 'debug', 'info', 'warning', 'error', 'fatal'] 25 | ? '202 LOG OK' 26 | : '400 ERROR', 27 | CATEGORY: (c): string => (c && c.length === 4 ? '202 LOG OK' : '400 ERROR') // TODO 28 | }, 29 | SET: () => 'SET', 30 | LOCK: () => 'LOCK', 31 | DATA: { 32 | STORE: (): string => 'DATA STORE', 33 | RETRIEVE: (): string => 'DATA RETRIEVE', 34 | LIST: (): string => 'DATA LIST', 35 | REMOVE: (): string => 'DATA REMOVE' 36 | }, 37 | CG: { 38 | layer: { 39 | ADD: (): string => 'CG ADD', 40 | PLAY: (): string => 'CG PLAY', 41 | STOP: (): string => 'CG STOP', 42 | NEXT: (): string => 'CG NEXT', 43 | REMOVE: (): string => 'CG REMOVE', 44 | CLEAR: (): string => 'CG CLEAR', 45 | UPDATE: (): string => 'CG UPDATE', 46 | INVOKE: (): string => 'CG INVOKE', 47 | INFO: (): string => 'CG INFO' 48 | } 49 | }, 50 | MIXER: { 51 | layer: { 52 | KEYER: (): string => 'MIXER KEYER', 53 | CHROMA: (): string => 'MIXER CHROMA', 54 | BLEND: (): string => 'MIXER BLEND', 55 | INVERT: (): string => 'MIXER_INVERT', 56 | OPACITY: (): string => 'MIXER OPACITY', 57 | BRIGHTNESS: (): string => 'MIXER BRIGHTNESS', 58 | SATURATION: (): string => 'MIXER SATURATION', 59 | CONTRAST: (): string => 'MIXER CONTRAST', 60 | LEVELS: (): string => 'MIXER LEVELS', 61 | FILL: (): string => 'MIXER FILL', 62 | CLIP: (): string => 'MIXER CLIP', 63 | ANCHOR: (): string => 'MIXER ANCHOR', 64 | CROP: (): string => 'MIXER CROP', 65 | ROTATION: (): string => 'MIXER ROTATION', 66 | PERSPECTIVE: (): string => 'MIXER PERSPECTIVE', 67 | MIPMAP: (): string => 'MIXER MIPMAP', 68 | VOLUME: (): string => 'MIXER VOLUME', 69 | MASTERVOLUME: (): string => 'MIXER MASTERVOLUME', 70 | STRAIGHT_ALPHA_OUTPUT: (): string => 'MIXER STRAIGHT_ALPHA_OUTPUT', 71 | GRID: (): string => 'MIXER GRID', 72 | COMMIT: (): string => 'MIXER COMMIT', 73 | CLEAR: (): string => 'MIXER CLEAR' 74 | } 75 | }, 76 | CHANNEL_GRID: () => '202 CHANNEL_GRID OK', 77 | THUMBNAIL: { 78 | LIST: (): string => 'THUMBNAIL LIST', 79 | RETRIEVE: (): string => 'THUMBNAIL RETRIEVE', 80 | GENERATE: (): string => 'THUMBNAIL GENERATE', 81 | GENERATE_ALL: (): string => 'THUMBNAIL GENERATE_ALL' 82 | }, 83 | CINF: () => 'CINF', 84 | CLS: () => responses.clsResponse218, 85 | FLS: () => responses.flsResponse218, 86 | TLS: () => responses.tlsResponse218, 87 | VERSION: () => '201 VERSION OK\r\n2.1.8.12205 62ea2b24d NRK', 88 | INFO: { 89 | none: (): string => 'INFO', 90 | number: (): string => 'INFO channel', 91 | TEMPLATE: (): string => 'INFO TEMPLATE', 92 | CONFIG: (): string => 'INFO CONFIG', 93 | PATHS: (): string => 'INFO PATHS', 94 | SYSTEM: (): string => 'INFO SYSTEM', 95 | SERVER: (): string => 'INFO SERVER', 96 | THREADS: (): string => 'INFO THREADS', 97 | DELAY: (): string => 'INFO DELAY' 98 | }, 99 | DIAG: () => '202 DIAG OK', 100 | // BYE: () => 'BYE', 101 | KILL: () => '202 KILL OK', 102 | RESTART: () => '202 RESTART OK', 103 | PING: (c) => (c && c.length > 1 ? 'PONG ' + c.slice(1).join(' ') : 'PONG'), 104 | HELP: { 105 | none: (): string => 'HELP', // commands 106 | string: (): string => 'HELP command', 107 | PRODUCER: (): string => 'HELP PRODUCER', 108 | CONSUMER: (): string => 'HELP CONSUMER' 109 | }, 110 | TIME: () => 'TIME', 111 | SCHEDULE: { 112 | SET: (): string => 'SCHEDULE_SET', 113 | LIST: (): string => 'SCHEDULE_LIST', 114 | CLEAR: (): string => 'SCHEDULE_CLEAR', 115 | REMOVE: (): string => 'SCHEDULE_REMOVE', 116 | INFO: (): string => 'SCHEDULE_INFO' 117 | }, 118 | TIMECODE: { 119 | layer: { 120 | SOURCE: (): string => 'TIMECODE_SOURCE' 121 | } 122 | } 123 | } 124 | 125 | export const responses207: Responses = Object.assign({}, responses218, { 126 | VERSION: () => '201 VERSION OK\r\n2.0.7.e9fc25a Stable', 127 | ROUTE: () => 'ROUTE', 128 | GL_INFO: () => 'GL INFO', 129 | GL_GC: () => 'GL GC', 130 | CLS: () => responses.clsResponse207, 131 | TLS: () => responses.tlsResponse207 132 | }) 133 | 134 | responses207.LOG = Object.assign({}, responses218.LOG) 135 | delete (responses207.LOG as Responses).CATEGORY 136 | let mixerLayer = Object.assign({}, (responses218.MIXER as Responses).layer) 137 | delete (mixerLayer as Responses).INVERT 138 | responses207.MIXER = Object.assign({}, { layer: mixerLayer }) 139 | delete responses207.FLS 140 | delete responses207.HELP 141 | delete responses207.TIME 142 | delete responses207.PING 143 | delete responses207.SCHEDULE 144 | delete responses207.TIMECODE 145 | 146 | const info = Object.assign({}, responses218.INFO as Responses) 147 | info.QUEUES = (): string => 'INFO QUEUES' 148 | responses207.INFO = info 149 | 150 | export const responses220: Responses = Object.assign({}, responses218, { 151 | VERSION: () => '201 VERSION OK\r\n2.2.0 66a9e3e2 Stable' 152 | }) 153 | 154 | responses220.LOG = Object.assign({}, responses218.LOG, { 155 | CLS: () => responses.clsResponse220, 156 | FLS: () => responses.flsResponse220, 157 | TLS: () => responses.tlsResponse220 158 | }) 159 | delete (responses220.LOG as Responses).CATEGORY 160 | const cgLayer = Object.assign({}, (responses218.CG as Responses).layer) 161 | delete (cgLayer as Responses).INFO 162 | responses220.CG = Object.assign({}, { layer: cgLayer }) 163 | delete (responses220.CG.layer as Responses).INFO 164 | mixerLayer = Object.assign({}, (responses218.MIXER as Responses).layer) 165 | delete (mixerLayer as Responses).INVERT 166 | delete (mixerLayer as Responses).STRAIGHT_ALPHA_OUTPUT 167 | responses220.MIXER = Object.assign({}, { layer: mixerLayer }) 168 | responses220.INFO = { 169 | none: (responses218.INFO as Responses).none, 170 | number: (responses218.INFO as Responses).number 171 | } 172 | delete responses220.HELP 173 | delete responses220.TIME 174 | delete responses220.SCHEDULE 175 | delete responses220.TIMECODE 176 | -------------------------------------------------------------------------------- /src/AMCP/commands.ts: -------------------------------------------------------------------------------- 1 | /* Copyright 2020 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | import { ChanLayer } from '../chanLayer' 17 | 18 | function chanLayerFromString(chanLayStr: string): ChanLayer { 19 | let valid = false 20 | let channel = 0 21 | let layer = 0 22 | const match = chanLayStr?.match('(?\\d+)-?(?\\d*)') 23 | if (match?.groups) { 24 | valid = true 25 | const chanLay = match.groups 26 | channel = parseInt(chanLay.channel) 27 | if (chanLay.layer !== '') { 28 | layer = parseInt(chanLay.layer) 29 | } 30 | } 31 | return { valid: valid, channel: channel, layer: layer } 32 | } 33 | 34 | interface CmdEntry { 35 | cmd: string 36 | fn: (chanLayer: ChanLayer, params: string[]) => Promise 37 | } 38 | 39 | export class Commands { 40 | private readonly map: CmdEntry[] 41 | 42 | constructor() { 43 | this.map = [] 44 | } 45 | 46 | add(entry: CmdEntry): void { 47 | this.map.push(entry) 48 | } 49 | async process(command: string[]): Promise { 50 | let result = false 51 | const entry = this.map.find(({ cmd }) => cmd === command[0]) 52 | if (entry) { 53 | const chanLayer = chanLayerFromString(command[1]) 54 | result = await entry.fn(chanLayer, command.slice(chanLayer ? 2 : 1)) 55 | } 56 | 57 | return result 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/AMCP/server.ts: -------------------------------------------------------------------------------- 1 | /* Copyright 2020 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | import * as net from 'net' 17 | import { Responses, responses207, responses218, responses220 } from './cmdResponses' 18 | import { Commands } from './commands' 19 | 20 | let cmds: Commands 21 | let ccgResponses = responses218 22 | 23 | export function processCommand(command: string[] | null, token = ''): string { 24 | if (!command) { 25 | return '400 ERROR' 26 | } 27 | if (command[0] === 'REQ') { 28 | if (command[2] !== 'PING') { 29 | return processCommand(command.slice(2), command[1]) 30 | } else { 31 | token = command[1] 32 | } 33 | } 34 | if (command[0] === 'SWITCH') { 35 | if (command[1] === '207') { 36 | ccgResponses = responses207 37 | return '202 SWITCH 207 OK' 38 | } 39 | if (command[1] === '218') { 40 | ccgResponses = responses218 41 | return '202 SWITCH 218 OK' 42 | } 43 | if (command[1] === '220') { 44 | ccgResponses = responses220 45 | return '202 SWITCH 220 OK' 46 | } 47 | return '400 SWITCH ERROR' 48 | } 49 | if (command[0] === 'BYE') { 50 | return '***BYE***' 51 | } 52 | if (ccgResponses[command[0]]) { 53 | if (!cmds?.process(command)) { 54 | return `400 ERROR\r\n${command.join(' ')} NOT IMPLEMENTED` 55 | } 56 | const responseFn = ccgResponses[command[0]] 57 | let response: string | null = null 58 | if (typeof responseFn === 'function') { 59 | response = responseFn(command) 60 | } else { 61 | if (responseFn.none && command.length === 1) { 62 | response = (responseFn.none as (req: string[]) => string | null)(command) 63 | } else if (responseFn.number && command.length >= 2) { 64 | response = (responseFn.number as (req: string[]) => string | null)(command) 65 | } else if (responseFn.layer && command.length >= 3) { 66 | response = ((responseFn.layer as Responses)[command[2]] as ( 67 | req: string[] 68 | ) => string | null)(command) 69 | } else if (command.length >= 2 && responseFn[command[1]]) { 70 | response = (responseFn[command[1]] as (req: string[]) => string | null)(command) 71 | } 72 | if (response === null && responseFn.string && command.length >= 2) { 73 | response = (responseFn.string as (req: string[]) => string | null)(command) 74 | } 75 | } 76 | if (response) return token ? `RES ${token} ${response}` : response 77 | } 78 | 79 | return token 80 | ? `RES ${token} 400 ERROR\r\n${command.join(' ')}` 81 | : `400 ERROR\r\n${command.join(' ')}` 82 | } 83 | 84 | const server = net.createServer((c) => { 85 | console.log('client connected') 86 | c.on('end', () => { 87 | console.log('client disconnected') 88 | }) 89 | }) 90 | server.on('error', (err) => { 91 | throw err 92 | }) 93 | 94 | export async function start(commands?: Commands): Promise { 95 | if (commands) cmds = commands 96 | 97 | return new Promise((resolve, reject) => { 98 | let resolved = false 99 | server.once('error', (e) => { 100 | if (!resolved) reject(e) 101 | }) 102 | server.listen(5250, () => { 103 | resolved = true 104 | resolve('CasparCL server AMCP protocol running on port 5250') 105 | }) 106 | }) 107 | } 108 | 109 | export async function stop(): Promise { 110 | return new Promise((resolve, reject) => { 111 | let resolved = false 112 | server.once('error', (err) => { 113 | if (!resolved) reject(err) 114 | }) 115 | server.close((e) => { 116 | if (e) return reject(e) 117 | resolved = true 118 | resolve('CasparCL server closed') 119 | }) 120 | }) 121 | } 122 | 123 | server.on('listening', () => { 124 | // console.log('CasparCL server AMCP protocol running on port 5250') 125 | }) 126 | 127 | server.on('connection', (sock) => { 128 | let chunk = '' 129 | sock.on('data', (input) => { 130 | chunk += input.toString() 131 | let eol = chunk.indexOf('\r\n') 132 | 133 | while (eol > -1) { 134 | const command = chunk.substring(0, eol) 135 | console.log(command) 136 | const result = processCommand(command.toUpperCase().match(/"[^"]+"|""|\S+/g)) 137 | if (result === '***BYE***') { 138 | sock.destroy() 139 | break 140 | } 141 | sock.write(result.toString() + '\r\n') 142 | console.log(result) 143 | if (result === '202 KILL OK') { 144 | sock.destroy() 145 | stop().catch(console.error) 146 | break 147 | } 148 | chunk = chunk.substring(eol + 2) 149 | eol = chunk.indexOf('\r\n') 150 | } 151 | }) 152 | sock.on('error', console.error) 153 | sock.on('close', () => { 154 | console.log('client disconnect') 155 | }) 156 | }) 157 | 158 | export function version(version: string): void { 159 | if (version === '207') { 160 | ccgResponses = responses207 161 | } 162 | if (version === '218') { 163 | ccgResponses = responses218 164 | } 165 | if (version === '220') { 166 | ccgResponses = responses220 167 | } 168 | } 169 | 170 | if (!module.parent) { 171 | start().then(console.log, console.error) 172 | } 173 | -------------------------------------------------------------------------------- /src/AMCP/testResponses.ts: -------------------------------------------------------------------------------- 1 | const fixEndings = (s: string): string => s.replace(/\n/g, '\r\n') 2 | 3 | export const clsResponse218 = fixEndings(`200 CLS OK 4 | "20190903T183308" STILL 1673299 20190903173309 0 0/1 5 | "AMB" MOVIE 6445960 20190715145445 268 1/25 6 | "CG1080I50" MOVIE 6159792 20190715145445 264 1/25 7 | "CG1080I50_A" MOVIE 10298115 20190715145445 260 1/25 8 | "DRAG" MOVIE 65837628 20121213111257 263 1000000/59940059 9 | "ESSENCE" MOVIE 355402300 20190823112900 574 1/25 10 | "GO1080P25" MOVIE 16694084 20190715145445 445 1/25 11 | "LADYRISES" MOVIE 119878204 20121213095529 202 1/25 12 | "LADYRISES10MBS" MOVIE 20251275 20121213095859 297 1/25 13 | "SCENE/NAMESIGN/CROWN-PLATE" STILL 8172 20190715145446 0 0/1 14 | "SCENE/NAMESIGN/WHITE-PLATE" STILL 21341 20190715145446 0 0/1 15 | "SCENE/ROPE/ROPE_END" STILL 1173 20190715145446 0 0/1 16 | "SCENE/ROPE/ROPE_NODE" STILL 965 20190715145446 0 0/1 17 | "SCENE/TEMPLATEPACK1/ADVANCEDTEMPLATE1_PLATE" STILL 6640 20190715145446 0 0/1 18 | `) 19 | 20 | export const clsResponse207 = fixEndings(`200 CLS OK 21 | "AMB" MOVIE 6445960 20190715160634 268 1/25 22 | "CG1080I50" MOVIE 6159792 20190715160634 264 1/25 23 | "CG1080I50_A" MOVIE 10298115 20190715160634 260 1/25 24 | "GO1080P25" MOVIE 16694084 20190715160634 445 1/25 25 | "SPLIT" STILL 6220854 20190715160634 0 0/1 26 | "TESTPATTERNS\\1080I5000_TEST" MOVIE 1053771 20190715160634 50 1/25 27 | "TESTPATTERNS\\1080I5000_TEST_A" MOVIE 1340796 20190715160634 50 1/25 28 | "TESTPATTERNS\\1080I5994_TEST" MOVIE 1250970 20190715160634 60 1/30 29 | "TESTPATTERNS\\1080I5994_TEST_A" MOVIE 1592759 20190715160634 60 1/30 30 | "TESTPATTERNS\\1080I6000_TEST" MOVIE 1268605 20190715160634 59 125/3747 31 | `) 32 | 33 | export const clsResponse220 = fixEndings(`200 CLS OK 34 | "AMB" MOVIE 6445960 20190718163105 268 1/25 35 | "CG1080I50" MOVIE 6159792 20190718163106 264 1/25 36 | "CG1080I50_A" MOVIE 10298115 20190718163109 260 1/25 37 | "GO1080P25" MOVIE 16694084 20190718163108 445 1/25 38 | "SCENE/NAMESIGN/CROWN-PLATE" STILL 8172 20190718163025 NaN 0/0 39 | "SCENE/NAMESIGN/WHITE-PLATE" STILL 21341 20190816191059 NaN 0/0 40 | "SCENE/ROPE/ROPE_END" STILL 1173 20190718163025 NaN 0/0 41 | "SCENE/ROPE/ROPE_NODE" STILL 965 20190816191059 NaN 0/0 42 | "SCENE/TEMPLATEPACK1/ADVANCEDTEMPLATE1_PLATE" STILL 6640 20190718163026 NaN 0/0 43 | "SPLIT" STILL 6220854 20190718163110 NaN 0/0 44 | `) 45 | 46 | export const flsResponse218 = fixEndings(`200 FLS OK 47 | "LiberationSans" "LiberationSans-Regular.ttf" 48 | "Roboto-Light" "Roboto-Light.ttf" 49 | "Roboto-Regular" "Roboto-Regular.ttf" 50 | `) 51 | 52 | export const flsResponse220 = fixEndings(`200 FLS OK 53 | LIBERATIONSANS-REGULAR 54 | ROBOTO-LIGHT 55 | ROBOTO-REGULAR 56 | `) 57 | 58 | export const tlsResponse207 = fixEndings(`200 TLS OK 59 | "CasparCG_Flash_Templates_Example_Pack_1/ADVANCEDTEMPLATE1" 30327 20190715160636 60 | "CasparCG_Flash_Templates_Example_Pack_1/ADVANCEDTEMPLATE2" 49578 20190715160636 61 | "CasparCG_Flash_Templates_Example_Pack_1/SIMPLETEMPLATE1" 18606 20190715160636 62 | "CasparCG_Flash_Templates_Example_Pack_1/SIMPLETEMPLATE2" 1751565 20190715160636 63 | "CASPAR_TEXT" 19920 20190715160636 64 | "FRAME" 244156 20190715160636 65 | "NTSC-TEST-30" 37275 20190715160636 66 | "NTSC-TEST-60" 37274 20190715160636 67 | "PHONE" 1442360 20190715160636 68 | `) 69 | 70 | export const tlsResponse218 = fixEndings(`200 TLS OK 71 | "CASPAR_TEXT" 19920 20190715145448 flash 72 | "fetch-weather-example/INDEX" 6496 20190718114232 html 73 | "fetch-weather-example/js/monkeecreate-jquery.simpleWeather-0d95e82/INDEX" 120079 20190718114232 html 74 | "FRAME" 244156 20190715145448 flash 75 | "hello-world/INDEX" 622 20190718114232 html 76 | "html_template/template_js/TEMPLATE" 5428 20190820170049 html 77 | "NTSC-TEST-30" 37275 20190715145448 flash 78 | "NTSC-TEST-60" 37274 20190715145448 flash 79 | "PHONE" 1442360 20190715145448 flash 80 | "scene/crawler/CRAWLER" 3266 20190715145448 scene 81 | `) 82 | 83 | export const tlsResponse220 = fixEndings(`200 TLS OK 84 | HELLO-WORLD/INDEX 85 | FETCH-WEATHER-EXAMPLE/INDEX 86 | FETCH-WEATHER-EXAMPLE/JS/MONKEECREATE-JQUERY.SIMPLEWEATHER-0D95E82/INDEX 87 | `) 88 | -------------------------------------------------------------------------------- /src/chanLayer.ts: -------------------------------------------------------------------------------- 1 | /* Copyright 2020 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | import { OpenCLBuffer } from 'nodencl' 17 | 18 | export interface ChanLayer { 19 | valid: boolean 20 | channel: number 21 | layer: number 22 | } 23 | 24 | export interface SourceFrame { 25 | video: OpenCLBuffer 26 | audio: Buffer 27 | timestamp: number 28 | } 29 | -------------------------------------------------------------------------------- /src/channel.ts: -------------------------------------------------------------------------------- 1 | /* Copyright 2020 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | import { clContext as nodenCLContext, OpenCLBuffer } from 'nodencl' 17 | import { ChanLayer, SourceFrame } from './chanLayer' 18 | import { ProducerRegistry } from './producer/producer' 19 | import { ConsumerRegistry } from './consumer/consumer' 20 | import { RedioPipe, RedioStream } from 'redioactive' 21 | 22 | export class Channel { 23 | private readonly channel: number 24 | private readonly producerRegistry: ProducerRegistry 25 | private readonly consumerRegistry: ConsumerRegistry 26 | private foreground: RedioPipe | null 27 | private background: RedioPipe | null 28 | private spout: RedioStream | null 29 | 30 | constructor(clContext: nodenCLContext, channel: number) { 31 | this.channel = channel 32 | this.producerRegistry = new ProducerRegistry(clContext) 33 | this.consumerRegistry = new ConsumerRegistry(clContext) 34 | this.foreground = null 35 | this.background = null 36 | this.spout = null 37 | } 38 | 39 | async createSource(chanLay: ChanLayer, params: string[]): Promise { 40 | this.background = await this.producerRegistry.createSource(chanLay, params) 41 | return this.background != null 42 | } 43 | 44 | async play(): Promise { 45 | if (this.background !== null) { 46 | this.foreground = this.background 47 | this.background = null 48 | } 49 | 50 | if (this.foreground != null) 51 | this.spout = await this.consumerRegistry.createSpout(this.channel, this.foreground) 52 | 53 | return Promise.resolve(this.spout != null) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/consumer/consumer.ts: -------------------------------------------------------------------------------- 1 | /* Copyright 2020 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | import { clContext as nodenCLContext, OpenCLBuffer } from 'nodencl' 17 | import { SourceFrame } from '../chanLayer' 18 | import { MacadamConsumerFactory } from './macadamConsumer' 19 | import { RedioPipe, RedioStream } from 'redioactive' 20 | 21 | export interface Consumer { 22 | initialise(pipe: RedioPipe): Promise | null> 23 | } 24 | 25 | export interface ConsumerFactory { 26 | createConsumer(channel: number): T 27 | } 28 | 29 | export class InvalidConsumerError extends Error { 30 | constructor(message?: string) { 31 | super(message) 32 | // see: typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html 33 | Object.setPrototypeOf(this, new.target.prototype) // restore prototype chain 34 | this.name = InvalidConsumerError.name // stack traces display correctly now 35 | } 36 | } 37 | export class ConsumerRegistry { 38 | private readonly consumerFactories: ConsumerFactory[] 39 | 40 | constructor(clContext: nodenCLContext) { 41 | this.consumerFactories = [] 42 | this.consumerFactories.push(new MacadamConsumerFactory(clContext)) 43 | } 44 | 45 | async createSpout( 46 | channel: number, 47 | pipe: RedioPipe 48 | ): Promise | null> { 49 | let p: RedioStream | null = null 50 | for (const f of this.consumerFactories) { 51 | try { 52 | const consumer = f.createConsumer(channel) as Consumer 53 | if ((p = await consumer.initialise(pipe)) !== null) break 54 | } catch (err) { 55 | if (!(err instanceof InvalidConsumerError)) { 56 | throw err 57 | } 58 | } 59 | } 60 | 61 | if (p === null) { 62 | console.log(`Failed to find consumer for channel: '${channel}'`) 63 | } 64 | 65 | return p 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/consumer/macadamConsumer.ts: -------------------------------------------------------------------------------- 1 | /* Copyright 2020 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | import { SourceFrame } from '../chanLayer' 17 | import { clContext as nodenCLContext, OpenCLBuffer } from 'nodencl' 18 | import { ConsumerFactory, Consumer } from './consumer' 19 | import { RedioPipe, RedioStream, nil, isEnd, isNil } from 'redioactive' 20 | import * as Macadam from 'macadam' 21 | import { FromRGBA } from '../process/io' 22 | import { Writer } from '../process/v210' 23 | 24 | export class MacadamConsumer implements Consumer { 25 | private readonly channel: number 26 | private clContext: nodenCLContext 27 | private playback: Macadam.PlaybackChannel | null = null 28 | private fromRGBA: FromRGBA | undefined 29 | private vidProcess: RedioPipe | undefined 30 | private vidSaver: RedioPipe | undefined 31 | private spout: RedioStream | undefined 32 | private clDests: Array | undefined 33 | private field: number 34 | private frameNumber: number 35 | private readonly latency: number 36 | 37 | constructor(channel: number, context: nodenCLContext) { 38 | this.channel = channel 39 | this.clContext = context 40 | this.field = 0 41 | this.frameNumber = 0 42 | this.latency = 3 43 | } 44 | 45 | async initialise(pipe: RedioPipe): Promise | null> { 46 | this.playback = await Macadam.playback({ 47 | deviceIndex: this.channel - 1, 48 | displayMode: Macadam.bmdModeHD1080i50, 49 | pixelFormat: Macadam.bmdFormat10BitYUV 50 | }) 51 | 52 | this.fromRGBA = new FromRGBA( 53 | this.clContext, 54 | '709', 55 | new Writer( 56 | this.playback.width, 57 | this.playback.height, 58 | this.playback.fieldDominance != 'progressiveFrame' 59 | ) 60 | ) 61 | await this.fromRGBA.init() 62 | 63 | this.vidProcess = pipe.valve( 64 | async (frame) => { 65 | if (!isEnd(frame) && !isNil(frame)) { 66 | const fromRGBA = this.fromRGBA as FromRGBA 67 | if (this.field === 0) this.clDests = await fromRGBA.createDests() 68 | const clDests = this.clDests as Array 69 | const srcFrame = frame as SourceFrame 70 | const queue = this.clContext.queue.process 71 | const interlace = 0x1 | (this.field << 1) 72 | await fromRGBA.processFrame(srcFrame.video, clDests, queue, interlace) 73 | await this.clContext.waitFinish(queue) 74 | srcFrame.video.release() 75 | this.field = 1 - this.field 76 | return this.field === 1 ? nil : clDests[0] 77 | } else { 78 | return frame 79 | } 80 | }, 81 | { bufferSizeMax: 3, oneToMany: false } 82 | ) 83 | 84 | this.vidSaver = this.vidProcess.valve( 85 | async (frame) => { 86 | if (!isEnd(frame) && !isNil(frame)) { 87 | const v210Frame = frame as OpenCLBuffer 88 | const fromRGBA = this.fromRGBA as FromRGBA 89 | await fromRGBA.saveFrame(v210Frame, this.clContext.queue.unload) 90 | await this.clContext.waitFinish(this.clContext.queue.unload) 91 | return v210Frame 92 | } else { 93 | return frame 94 | } 95 | }, 96 | { bufferSizeMax: 3, oneToMany: false } 97 | ) 98 | 99 | this.spout = this.vidSaver.spout( 100 | async (frame) => { 101 | if (!isEnd(frame) && !isNil(frame)) { 102 | const v210Frame = frame as OpenCLBuffer 103 | this.playback?.schedule({ video: v210Frame, time: 1000 * this.frameNumber }) 104 | if (this.frameNumber === this.latency) this.playback?.start({ startTime: 0 }) 105 | if (this.frameNumber >= this.latency) 106 | await this.playback?.played((this.frameNumber - this.latency) * 1000) 107 | 108 | this.frameNumber++ 109 | v210Frame.release() 110 | return Promise.resolve() 111 | } else { 112 | return Promise.resolve() 113 | } 114 | }, 115 | { bufferSizeMax: 3, oneToMany: false } 116 | ) 117 | 118 | console.log(`Created Macadam consumer for Blackmagic id: ${this.channel - 1}`) 119 | return this.spout 120 | } 121 | } 122 | 123 | export class MacadamConsumerFactory implements ConsumerFactory { 124 | private clContext: nodenCLContext 125 | 126 | constructor(clContext: nodenCLContext) { 127 | this.clContext = clContext 128 | } 129 | 130 | createConsumer(channel: number): MacadamConsumer { 131 | return new MacadamConsumer(channel, this.clContext) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* Copyright 2020 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | import { clContext as nodenCLContext } from 'nodencl' 17 | import { start, processCommand } from './AMCP/server' 18 | import { Commands } from './AMCP/commands' 19 | import { Basic } from './AMCP/basic' 20 | import Koa from 'koa' 21 | import cors from '@koa/cors' 22 | import readline from 'readline' 23 | 24 | const initialiseOpenCL = async (): Promise => { 25 | const platformIndex = 0 26 | const deviceIndex = 0 27 | const clContext = new nodenCLContext({ 28 | platformIndex: platformIndex, 29 | deviceIndex: deviceIndex, 30 | overlapping: true 31 | }) 32 | await clContext.initialise() 33 | const platformInfo = clContext.getPlatformInfo() 34 | console.log( 35 | `OpenCL accelerator running on device from vendor '${platformInfo.vendor}', type '${platformInfo.devices[deviceIndex].type}'` 36 | ) 37 | return clContext 38 | } 39 | 40 | const rl = readline.createInterface({ 41 | input: process.stdin, 42 | output: process.stdout, 43 | prompt: 'AMCP> ' 44 | }) 45 | 46 | rl.on('line', async (input) => { 47 | if (input === 'q') { 48 | process.kill(process.pid, 'SIGTERM') 49 | } 50 | 51 | if (input !== '') { 52 | console.log(`AMCP received: ${input}`) 53 | await processCommand(input.toUpperCase().match(/"[^"]+"|""|\S+/g)) 54 | } 55 | 56 | rl.prompt() 57 | }) 58 | 59 | rl.on('SIGINT', () => { 60 | process.kill(process.pid, 'SIGTERM') 61 | }) 62 | 63 | // 960 * 540 RGBA 8-bit 64 | const lastWeb = Buffer.alloc(1920 * 1080) 65 | 66 | const kapp = new Koa() 67 | kapp.use(cors()) 68 | kapp.use((ctx) => { 69 | ctx.body = lastWeb 70 | }) 71 | const server = kapp.listen(3001) 72 | process.on('SIGHUP', () => server.close) 73 | 74 | const commands: Commands = new Commands() 75 | initialiseOpenCL().then((context) => { 76 | const basic = new Basic(context) 77 | basic.addCmds(commands) 78 | }) 79 | 80 | start(commands).then((fulfilled) => console.log('Command:', fulfilled), console.error) 81 | -------------------------------------------------------------------------------- /src/process/combine.ts: -------------------------------------------------------------------------------- 1 | /* Copyright 2019 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | import { ProcessImpl } from './imageProcess' 17 | import { OpenCLBuffer, KernelParams } from 'nodencl' 18 | 19 | const combineKernel = ` 20 | __constant sampler_t sampler1 = 21 | CLK_NORMALIZED_COORDS_FALSE 22 | | CLK_ADDRESS_CLAMP_TO_EDGE 23 | | CLK_FILTER_NEAREST; 24 | 25 | __kernel void 26 | twoInputs(__read_only image2d_t bgIn, 27 | __read_only image2d_t ovIn, 28 | __write_only image2d_t output) { 29 | 30 | int x = get_global_id(0); 31 | int y = get_global_id(1); 32 | float4 bg = read_imagef(bgIn, sampler1, (int2)(x,y)); 33 | float4 ov = read_imagef(ovIn, sampler1, (int2)(x,y)); 34 | float k = 1.0f - ov.s3; 35 | float4 k4 = (float4)(k, k, k, 0.0f); 36 | float4 out = fma(bg, k4, ov); 37 | write_imagef(output, (int2)(x, y), out); 38 | }; 39 | 40 | __kernel void 41 | threeInputs(__read_only image2d_t bgIn, 42 | __read_only image2d_t ov0In, 43 | __read_only image2d_t ov1In, 44 | __write_only image2d_t output) { 45 | 46 | int x = get_global_id(0); 47 | int y = get_global_id(1); 48 | float4 bg = read_imagef(bgIn, sampler1, (int2)(x,y)); 49 | 50 | float4 ov0 = read_imagef(ov0In, sampler1, (int2)(x,y)); 51 | float k = 1.0f - ov0.s3; 52 | float4 k4 = (float4)(k, k, k, 0.0f); 53 | float4 out0 = fma(bg, k4, ov0); 54 | 55 | float4 ov1 = read_imagef(ov1In, sampler1, (int2)(x,y)); 56 | k = 1.0f - ov1.s3; 57 | k4 = (float4)(k, k, k, 0.0f); 58 | float4 out1 = fma(out0, k4, ov1); 59 | write_imagef(output, (int2)(x, y), out1); 60 | }; 61 | ` 62 | 63 | export default class Combine extends ProcessImpl { 64 | private readonly numOverlays: number 65 | 66 | constructor(width: number, height: number, numOverlays: number) { 67 | super( 68 | numOverlays === 1 ? 'combine-1' : 'combine-2', 69 | width, 70 | height, 71 | combineKernel, 72 | numOverlays === 1 ? 'twoInputs' : 'threeInputs' 73 | ) 74 | this.numOverlays = numOverlays 75 | if (!(this.numOverlays > 0 && this.numOverlays < 3)) 76 | throw new Error(`Combine supports one or two overlays, ${this.numOverlays} requested`) 77 | } 78 | 79 | async init(): Promise { 80 | return Promise.resolve() 81 | } 82 | 83 | async getKernelParams(params: KernelParams): Promise { 84 | const kernelParams: KernelParams = { 85 | bgIn: params.bgIn, 86 | output: params.output 87 | } 88 | 89 | const ovArray = params.ovIn as Array 90 | if (ovArray.length !== 1 && ovArray.length !== 2) 91 | throw new Error("Combine requires 'ovIn' array parameter with 1 or 2 OpenCL buffers") 92 | 93 | switch (this.numOverlays) { 94 | case 1: 95 | kernelParams.ovIn = ovArray[0] 96 | break 97 | case 2: 98 | kernelParams.ov0In = ovArray[0] 99 | kernelParams.ov1In = ovArray[1] 100 | break 101 | } 102 | 103 | return Promise.resolve(kernelParams) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/process/imageProcess.ts: -------------------------------------------------------------------------------- 1 | /* Copyright 2019 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | import { clContext as nodenCLContext, OpenCLProgram, KernelParams, RunTimings } from 'nodencl' 17 | 18 | export abstract class ProcessImpl { 19 | protected readonly name: string 20 | protected readonly width: number 21 | protected readonly height: number 22 | readonly kernel: string 23 | readonly programName: string 24 | readonly globalWorkItems = 0 25 | 26 | constructor(name: string, width: number, height: number, kernel: string, programName: string) { 27 | this.name = name 28 | this.width = width 29 | this.height = height 30 | this.kernel = kernel 31 | this.programName = programName 32 | } 33 | 34 | abstract async init(): Promise 35 | 36 | getNumBytesRGBA(): number { 37 | return this.width * this.height * 4 * 4 38 | } 39 | getGlobalWorkItems(): Uint32Array { 40 | return Uint32Array.from([this.width, this.height]) 41 | } 42 | 43 | abstract async getKernelParams(params: KernelParams, clQueue: number): Promise 44 | } 45 | 46 | export default class ImageProcess { 47 | private readonly clContext: nodenCLContext 48 | private readonly processImpl: ProcessImpl 49 | private program: OpenCLProgram | null = null 50 | constructor(clContext: nodenCLContext, processImpl: ProcessImpl) { 51 | this.clContext = clContext 52 | this.processImpl = processImpl 53 | } 54 | 55 | async init(): Promise { 56 | this.program = await this.clContext.createProgram(this.processImpl.kernel, { 57 | name: this.processImpl.programName, 58 | globalWorkItems: this.processImpl.getGlobalWorkItems() 59 | }) 60 | return this.processImpl.init() 61 | } 62 | 63 | async run(params: KernelParams, clQueue: number): Promise { 64 | if (this.program == null) throw new Error('Loader.run failed with no program available') 65 | const kernelParams = await this.processImpl.getKernelParams(params, clQueue) 66 | return this.clContext.runProgram(this.program, kernelParams, clQueue) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/process/io.ts: -------------------------------------------------------------------------------- 1 | /* Copyright 2019 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | import { clContext as nodenCLContext, OpenCLBuffer, ImageDims, RunTimings } from 'nodencl' 17 | import { Loader, Saver } from './loadSave' 18 | import { PackImpl, Interlace } from './packer' 19 | import ImageProcess from './imageProcess' 20 | import Resize from './resize' 21 | 22 | export class ToRGBA { 23 | private readonly clContext: nodenCLContext 24 | private readonly loader: Loader 25 | private readonly numBytes: Array 26 | private readonly numBytesRGBA: number 27 | private readonly totalBytes: number 28 | 29 | constructor( 30 | clContext: nodenCLContext, 31 | colSpecRead: string, 32 | colSpecWrite: string, 33 | readImpl: PackImpl 34 | ) { 35 | this.clContext = clContext 36 | this.loader = new Loader(this.clContext, colSpecRead, colSpecWrite, readImpl) 37 | this.numBytes = readImpl.getNumBytes() 38 | this.numBytesRGBA = readImpl.getNumBytesRGBA() 39 | this.totalBytes = readImpl.getTotalBytes() 40 | } 41 | 42 | async init(): Promise { 43 | await this.loader.init() 44 | } 45 | 46 | getNumBytes(): Array { 47 | return this.numBytes 48 | } 49 | getNumBytesRGBA(): number { 50 | return this.numBytesRGBA 51 | } 52 | getTotalBytes(): number { 53 | return this.totalBytes 54 | } 55 | 56 | async createSources(): Promise> { 57 | return Promise.all( 58 | this.numBytes.map((bytes) => 59 | this.clContext.createBuffer(bytes, 'readonly', 'coarse', undefined, 'ToRGBA') 60 | ) 61 | ) 62 | } 63 | 64 | async createDest(imageDims: ImageDims): Promise { 65 | return this.clContext.createBuffer(this.numBytesRGBA, 'readonly', 'coarse', imageDims, 'ToRGBA') 66 | } 67 | 68 | async loadFrame( 69 | input: Buffer | Array, 70 | sources: Array, 71 | clQueue?: number | undefined 72 | ): Promise> { 73 | const inputs = Array.isArray(input) ? input : [input] 74 | return Promise.all( 75 | sources.map(async (src, i) => { 76 | await src.hostAccess( 77 | 'writeonly', 78 | clQueue ? clQueue : 0, 79 | inputs[i].slice(0, this.numBytes[i]) 80 | ) 81 | return src.hostAccess('none', clQueue ? clQueue : 0) 82 | }) 83 | ) 84 | } 85 | 86 | async processFrame( 87 | sources: Array, 88 | dest: OpenCLBuffer, 89 | clQueue?: number 90 | ): Promise { 91 | return this.loader.run({ sources: sources, dest: dest }, clQueue ? clQueue : 0) 92 | } 93 | } 94 | 95 | export class FromRGBA { 96 | private readonly clContext: nodenCLContext 97 | private readonly width: number 98 | private readonly height: number 99 | private readonly saver: Saver 100 | private readonly numBytes: Array 101 | private readonly numBytesRGBA: number 102 | private readonly totalBytes: number 103 | private readonly srcWidth: number 104 | private readonly srcHeight: number 105 | private resizer: ImageProcess | null = null 106 | private rgbaSz: OpenCLBuffer | null = null 107 | 108 | constructor( 109 | clContext: nodenCLContext, 110 | colSpecRead: string, 111 | writeImpl: PackImpl, 112 | srcWidth?: number, 113 | srcHeight?: number 114 | ) { 115 | this.clContext = clContext 116 | this.width = writeImpl.getWidth() 117 | this.height = writeImpl.getHeight() 118 | this.saver = new Saver(this.clContext, colSpecRead, writeImpl) 119 | this.numBytes = writeImpl.getNumBytes() 120 | this.numBytesRGBA = writeImpl.getNumBytesRGBA() 121 | this.totalBytes = writeImpl.getTotalBytes() 122 | this.srcWidth = srcWidth ? srcWidth : this.width 123 | this.srcHeight = srcHeight ? srcHeight : this.height 124 | } 125 | 126 | async init(): Promise { 127 | await this.saver.init() 128 | 129 | if (!(this.srcWidth === this.width && this.srcHeight === this.height)) { 130 | this.resizer = new ImageProcess( 131 | this.clContext, 132 | new Resize(this.clContext, this.width, this.height) 133 | ) 134 | await this.resizer.init() 135 | 136 | this.rgbaSz = await this.clContext.createBuffer( 137 | this.numBytesRGBA, 138 | 'readwrite', 139 | 'coarse', 140 | { width: this.width, height: this.height }, 141 | 'rgbaSz' 142 | ) 143 | } 144 | } 145 | 146 | getNumBytes(): Array { 147 | return this.numBytes 148 | } 149 | getNumBytesRGBA(): number { 150 | return this.numBytesRGBA 151 | } 152 | getTotalBytes(): number { 153 | return this.totalBytes 154 | } 155 | 156 | async createDests(): Promise> { 157 | return Promise.all( 158 | this.numBytes.map((bytes) => 159 | this.clContext.createBuffer(bytes, 'readonly', 'coarse', undefined, 'ToRGBA') 160 | ) 161 | ) 162 | } 163 | 164 | async processFrame( 165 | source: OpenCLBuffer, 166 | dests: Array, 167 | clQueue?: number, 168 | interlace?: Interlace 169 | ): Promise { 170 | let saveSource = source 171 | if (this.resizer && this.rgbaSz) { 172 | await this.resizer.run({ input: source, output: this.rgbaSz }, clQueue ? clQueue : 0) 173 | saveSource = this.rgbaSz 174 | } 175 | 176 | return this.saver.run( 177 | { source: saveSource, dests: dests, interlace: interlace }, 178 | clQueue ? clQueue : 0 179 | ) 180 | } 181 | 182 | async saveFrame( 183 | output: OpenCLBuffer | Array, 184 | clQueue?: number | undefined 185 | ): Promise> { 186 | const outputs = Array.isArray(output) ? output : [output] 187 | return Promise.all(outputs.map((op) => op.hostAccess('readonly', clQueue ? clQueue : 0))) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/process/loadSave.ts: -------------------------------------------------------------------------------- 1 | /* Copyright 2019 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | import Packer, { PackImpl } from './packer' 17 | import { clContext as nodenCLContext, OpenCLBuffer, KernelParams, RunTimings } from 'nodencl' 18 | import { 19 | gamma2linearLUT, 20 | ycbcr2rgbMatrix, 21 | matrixFlatten, 22 | rgb2rgbMatrix, 23 | linear2gammaLUT, 24 | rgb2ycbcrMatrix 25 | } from './colourMaths' 26 | 27 | export class Loader extends Packer { 28 | private readonly gammaArray: Float32Array 29 | private readonly colMatrixArray: Float32Array | null = null 30 | private readonly gamutMatrixArray: Float32Array 31 | private gammaLut: OpenCLBuffer | null = null 32 | private colMatrix: OpenCLBuffer | null = null 33 | private gamutMatrix: OpenCLBuffer | null = null 34 | 35 | constructor(clContext: nodenCLContext, colSpec: string, outColSpec: string, packImpl: PackImpl) { 36 | super(clContext, packImpl) 37 | 38 | this.gammaArray = gamma2linearLUT(colSpec) 39 | if (!this.packImpl.getIsRGB()) { 40 | const colMatrix2d = ycbcr2rgbMatrix( 41 | colSpec, 42 | this.packImpl.numBits, 43 | this.packImpl.lumaBlack, 44 | this.packImpl.lumaWhite, 45 | this.packImpl.chromaRange 46 | ) 47 | this.colMatrixArray = matrixFlatten(colMatrix2d) 48 | } 49 | 50 | const gamutMatrix2d = rgb2rgbMatrix(colSpec, outColSpec) 51 | this.gamutMatrixArray = matrixFlatten(gamutMatrix2d) 52 | } 53 | 54 | async init(): Promise { 55 | await super.init() 56 | 57 | this.gammaLut = await this.clContext.createBuffer( 58 | this.gammaArray.byteLength, 59 | 'readonly', 60 | 'coarse' 61 | ) 62 | await this.gammaLut.hostAccess('writeonly') 63 | Buffer.from(this.gammaArray.buffer).copy(this.gammaLut) 64 | 65 | if (this.colMatrixArray) { 66 | this.colMatrix = await this.clContext.createBuffer( 67 | this.colMatrixArray.byteLength, 68 | 'readonly', 69 | 'none' 70 | ) 71 | await this.colMatrix.hostAccess('writeonly') 72 | Buffer.from(this.colMatrixArray.buffer).copy(this.colMatrix) 73 | } 74 | 75 | this.gamutMatrix = await this.clContext.createBuffer( 76 | this.gamutMatrixArray.byteLength, 77 | 'readonly', 78 | 'none' 79 | ) 80 | await this.gamutMatrix.hostAccess('writeonly') 81 | Buffer.from(this.gamutMatrixArray.buffer).copy(this.gamutMatrix) 82 | } 83 | 84 | async run(params: KernelParams, queueNum: number): Promise { 85 | if (this.program === null) throw new Error('Loader.run failed with no program available') 86 | 87 | const kernelParams = this.packImpl.getKernelParams(params) 88 | kernelParams.gammaLut = this.gammaLut 89 | kernelParams.gamutMatrix = this.gamutMatrix 90 | if (this.colMatrix) kernelParams.colMatrix = this.colMatrix 91 | 92 | return this.clContext.runProgram(this.program, kernelParams, queueNum) 93 | } 94 | } 95 | 96 | export class Saver extends Packer { 97 | private readonly gammaArray: Float32Array 98 | private readonly colMatrixArray: Float32Array | null = null 99 | private gammaLut: OpenCLBuffer | null = null 100 | private colMatrix: OpenCLBuffer | null = null 101 | 102 | constructor(clContext: nodenCLContext, colSpec: string, packImpl: PackImpl) { 103 | super(clContext, packImpl) 104 | 105 | this.gammaArray = linear2gammaLUT(colSpec) 106 | if (!this.packImpl.getIsRGB()) { 107 | const colMatrix2d = rgb2ycbcrMatrix( 108 | colSpec, 109 | this.packImpl.numBits, 110 | this.packImpl.lumaBlack, 111 | this.packImpl.lumaWhite, 112 | this.packImpl.chromaRange 113 | ) 114 | this.colMatrixArray = matrixFlatten(colMatrix2d) 115 | } 116 | } 117 | 118 | async init(): Promise { 119 | await super.init() 120 | 121 | this.gammaLut = await this.clContext.createBuffer( 122 | this.gammaArray.byteLength, 123 | 'readonly', 124 | 'coarse' 125 | ) 126 | await this.gammaLut.hostAccess('writeonly') 127 | 128 | Buffer.from(this.gammaArray.buffer).copy(this.gammaLut) 129 | if (this.colMatrixArray) { 130 | this.colMatrix = await this.clContext.createBuffer( 131 | this.colMatrixArray.byteLength, 132 | 'readonly', 133 | 'none' 134 | ) 135 | await this.colMatrix.hostAccess('writeonly') 136 | Buffer.from(this.colMatrixArray.buffer).copy(this.colMatrix) 137 | } 138 | } 139 | 140 | async run(params: KernelParams, queueNum: number): Promise { 141 | if (this.program === null) throw new Error('Saver.run failed with no program available') 142 | 143 | const kernelParams = this.packImpl.getKernelParams(params) 144 | kernelParams.gammaLut = this.gammaLut 145 | if (this.colMatrix) kernelParams.colMatrix = this.colMatrix 146 | 147 | return this.clContext.runProgram(this.program, kernelParams, queueNum) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/process/mix.ts: -------------------------------------------------------------------------------- 1 | /* Copyright 2019 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | import { ProcessImpl } from './imageProcess' 17 | import { KernelParams } from 'nodencl' 18 | 19 | const mixKernel = ` 20 | __constant sampler_t sampler1 = 21 | CLK_NORMALIZED_COORDS_FALSE 22 | | CLK_ADDRESS_CLAMP_TO_EDGE 23 | | CLK_FILTER_NEAREST; 24 | 25 | __kernel void mixer( 26 | __read_only image2d_t input0, 27 | __read_only image2d_t input1, 28 | __private float mix, 29 | __write_only image2d_t output) { 30 | 31 | int x = get_global_id(0); 32 | int y = get_global_id(1); 33 | float4 in0 = read_imagef(input0, sampler1, (int2)(x,y)); 34 | float4 in1 = read_imagef(input1, sampler1, (int2)(x,y)); 35 | 36 | float rmix = 1.0f - mix; 37 | float4 out = fma(in0, mix, in1 * rmix); 38 | 39 | write_imagef(output, (int2)(x, y), out); 40 | }; 41 | ` 42 | 43 | export default class Mix extends ProcessImpl { 44 | constructor(width: number, height: number) { 45 | super('mixer', width, height, mixKernel, 'mixer') 46 | } 47 | 48 | async init(): Promise { 49 | return Promise.resolve() 50 | } 51 | 52 | async getKernelParams(params: KernelParams): Promise { 53 | return Promise.resolve({ 54 | input0: params.input0, 55 | input1: params.input1, 56 | mix: params.mix, 57 | output: params.output 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/process/packer.ts: -------------------------------------------------------------------------------- 1 | /* Copyright 2019 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | import { clContext as nodenCLContext, OpenCLProgram, KernelParams, RunTimings } from 'nodencl' 17 | 18 | export enum Interlace { 19 | Progressive = 0, 20 | TopField = 1, 21 | BottomField = 3 22 | } 23 | 24 | export abstract class PackImpl { 25 | protected readonly name: string 26 | protected readonly width: number 27 | protected readonly height: number 28 | protected interlaced = false 29 | readonly kernel: string 30 | readonly programName: string 31 | numBits = 10 32 | lumaBlack = 64 33 | lumaWhite = 940 34 | chromaRange = 896 35 | protected isRGB = true 36 | protected numBytes: Array = [0] 37 | protected globalWorkItems = 0 38 | protected workItemsPerGroup = 0 39 | 40 | constructor(name: string, width: number, height: number, kernel: string, programName: string) { 41 | this.name = name 42 | this.width = width 43 | this.height = height 44 | this.kernel = kernel 45 | this.programName = programName 46 | } 47 | 48 | getWidth(): number { 49 | return this.width 50 | } 51 | getHeight(): number { 52 | return this.height 53 | } 54 | getNumBytes(): Array { 55 | return this.numBytes 56 | } 57 | getNumBytesRGBA(): number { 58 | return this.width * this.height * 4 * 4 59 | } 60 | getIsRGB(): boolean { 61 | return this.isRGB 62 | } 63 | getTotalBytes(): number { 64 | return this.numBytes.reduce((acc, n) => acc + n, 0) 65 | } 66 | getGlobalWorkItems(): number { 67 | return this.globalWorkItems 68 | } 69 | getWorkItemsPerGroup(): number { 70 | return this.workItemsPerGroup 71 | } 72 | 73 | abstract getKernelParams(params: KernelParams): KernelParams 74 | } 75 | 76 | export default abstract class Packer { 77 | protected readonly clContext: nodenCLContext 78 | protected readonly packImpl: PackImpl 79 | protected program: OpenCLProgram | null = null 80 | 81 | constructor(clContext: nodenCLContext, packImpl: PackImpl) { 82 | this.clContext = clContext 83 | this.packImpl = packImpl 84 | } 85 | 86 | async init(): Promise { 87 | this.program = await this.clContext.createProgram(this.packImpl.kernel, { 88 | name: this.packImpl.programName, 89 | globalWorkItems: this.packImpl.getGlobalWorkItems(), 90 | workItemsPerGroup: this.packImpl.getWorkItemsPerGroup() 91 | }) 92 | } 93 | 94 | abstract async run(kernelParams: KernelParams, queueNum: number): Promise 95 | } 96 | -------------------------------------------------------------------------------- /src/process/resize.ts: -------------------------------------------------------------------------------- 1 | /* Copyright 2019 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | import { ProcessImpl } from './imageProcess' 17 | import { clContext as nodenCLContext, OpenCLBuffer, KernelParams } from 'nodencl' 18 | 19 | const resizeKernel = ` 20 | __constant sampler_t samplerIn = 21 | CLK_NORMALIZED_COORDS_TRUE | 22 | CLK_ADDRESS_CLAMP | 23 | CLK_FILTER_LINEAR; 24 | 25 | __constant sampler_t samplerOut = 26 | CLK_NORMALIZED_COORDS_FALSE | 27 | CLK_ADDRESS_CLAMP | 28 | CLK_FILTER_NEAREST; 29 | 30 | __kernel void resize( 31 | __read_only image2d_t input, 32 | __private float scale, 33 | __private float offsetX, 34 | __private float offsetY, 35 | __global float* restrict flip, 36 | __write_only image2d_t output) { 37 | 38 | int w = get_image_width(output); 39 | int h = get_image_height(output); 40 | 41 | int outX = get_global_id(0); 42 | int outY = get_global_id(1); 43 | int2 posOut = {outX, outY}; 44 | 45 | float2 inPos = (float2)(outX / (float) w, outY / (float) h); 46 | float centreOffX = (-0.5f - offsetX) / scale + 0.5f; 47 | float centreOffY = (-0.5f - offsetY) / scale + 0.5f; 48 | float2 off = (float2)(fma(centreOffX, flip[1], flip[0]), fma(centreOffY, flip[3], flip[2])); 49 | float2 mul = (float2)(flip[1] / scale, flip[3] / scale); 50 | float2 posIn = fma(inPos, mul, off); 51 | 52 | float4 in = read_imagef(input, samplerIn, posIn); 53 | write_imagef(output, posOut, in); 54 | } 55 | ` 56 | export default class Resize extends ProcessImpl { 57 | private readonly clContext: nodenCLContext 58 | private flipH: boolean 59 | private flipV: boolean 60 | private flipArr: Float32Array 61 | private readonly flipArrBytes: number 62 | private flipVals: OpenCLBuffer | null = null 63 | 64 | constructor(clContext: nodenCLContext, width: number, height: number) { 65 | super('resize', width, height, resizeKernel, 'resize') 66 | 67 | this.clContext = clContext 68 | this.flipH = false 69 | this.flipV = false 70 | this.flipArr = Float32Array.from([0.0, 1.0, 0.0, 1.0]) 71 | this.flipArrBytes = this.flipArr.length * this.flipArr.BYTES_PER_ELEMENT 72 | } 73 | 74 | private async updateFlip(flipH: boolean, flipV: boolean, clQueue: number): Promise { 75 | if (this.flipVals === null) 76 | throw new Error('Resize.updateFlip failed with no program available') 77 | 78 | this.flipH = flipH 79 | this.flipV = flipV 80 | this.flipArr = Float32Array.from([ 81 | this.flipH ? 1.0 : 0.0, 82 | this.flipH ? -1.0 : 1.0, 83 | this.flipV ? 1.0 : 0.0, 84 | this.flipV ? -1.0 : 1.0 85 | ]) 86 | await this.flipVals.hostAccess('writeonly', clQueue, Buffer.from(this.flipArr.buffer)) 87 | return this.flipVals.hostAccess('none', clQueue) 88 | } 89 | 90 | async init(): Promise { 91 | this.flipVals = await this.clContext.createBuffer(this.flipArrBytes, 'readonly', 'coarse') 92 | return this.updateFlip(false, false, this.clContext.queue.load) 93 | } 94 | 95 | async getKernelParams(params: KernelParams, clQueue: number): Promise { 96 | const flipH = params.flipH as boolean 97 | const flipV = params.flipV as boolean 98 | const scale = params.scale as number 99 | const offsetX = params.offsetX as number 100 | const offsetY = params.offsetY as number 101 | 102 | if (!(this.flipH === flipH && this.flipV === flipV)) 103 | await this.updateFlip(flipH, flipV, clQueue) 104 | 105 | if (scale && !(scale > 0.0)) throw 'resize scale factor must be greater than zero' 106 | 107 | if (offsetX && !(offsetX >= -1.0 && offsetX <= 1.0)) 108 | throw 'resize offsetX must be between -1.0 and +1.0' 109 | 110 | if (offsetY && !(offsetY >= -1.0 && offsetY <= 1.0)) 111 | throw 'resize offsetX must be between -1.0 and +1.0' 112 | 113 | return Promise.resolve({ 114 | input: params.input, 115 | scale: params.scale || 1.0, 116 | offsetX: params.offsetX || 0.0, 117 | offsetY: params.offsetY || 0.0, 118 | flip: this.flipVals, 119 | output: params.output 120 | }) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/process/switch.ts: -------------------------------------------------------------------------------- 1 | /* Copyright 2019 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | import { clContext as nodenCLContext, OpenCLBuffer, KernelParams, RunTimings } from 'nodencl' 17 | import ImageProcess from './imageProcess' 18 | import Transform from './transform' 19 | import Mix from './mix' 20 | import Wipe from './wipe' 21 | import Combine from './combine' 22 | 23 | export default class Switch { 24 | private readonly clContext: nodenCLContext 25 | private readonly width: number 26 | private readonly height: number 27 | private readonly numInputs: number 28 | private readonly numOverlays: number 29 | private xform0: ImageProcess | null = null 30 | private xform1: ImageProcess | null = null 31 | private rgbaXf0: OpenCLBuffer | null = null 32 | private rgbaXf1: OpenCLBuffer | null = null 33 | private rgbaMx: OpenCLBuffer | null = null 34 | private mixer: ImageProcess | null = null 35 | private wiper: ImageProcess | null = null 36 | private combiner: ImageProcess | null = null 37 | 38 | constructor( 39 | clContext: nodenCLContext, 40 | width: number, 41 | height: number, 42 | numInputs: number, 43 | numOverlays: number 44 | ) { 45 | this.clContext = clContext 46 | this.width = width 47 | this.height = height 48 | this.numInputs = numInputs 49 | this.numOverlays = numOverlays 50 | } 51 | 52 | async init(): Promise { 53 | const numBytesRGBA = this.width * this.height * 4 * 4 54 | 55 | this.xform0 = new ImageProcess( 56 | this.clContext, 57 | new Transform(this.clContext, this.width, this.height) 58 | ) 59 | await this.xform0.init() 60 | 61 | this.rgbaXf0 = await this.clContext.createBuffer( 62 | numBytesRGBA, 63 | 'readwrite', 64 | 'coarse', 65 | { 66 | width: this.width, 67 | height: this.height 68 | }, 69 | 'switch' 70 | ) 71 | 72 | if (this.numInputs > 1) { 73 | this.xform1 = new ImageProcess( 74 | this.clContext, 75 | new Transform(this.clContext, this.width, this.height) 76 | ) 77 | await this.xform1.init() 78 | 79 | this.rgbaXf1 = await this.clContext.createBuffer( 80 | numBytesRGBA, 81 | 'readwrite', 82 | 'coarse', 83 | { 84 | width: this.width, 85 | height: this.height 86 | }, 87 | 'switch' 88 | ) 89 | 90 | this.mixer = new ImageProcess(this.clContext, new Mix(this.width, this.height)) 91 | await this.mixer.init() 92 | 93 | this.wiper = new ImageProcess(this.clContext, new Wipe(this.width, this.height)) 94 | await this.wiper.init() 95 | } 96 | 97 | this.combiner = new ImageProcess( 98 | this.clContext, 99 | new Combine(this.width, this.height, this.numOverlays) 100 | ) 101 | await this.combiner.init() 102 | 103 | this.rgbaMx = await this.clContext.createBuffer( 104 | numBytesRGBA, 105 | 'readwrite', 106 | 'coarse', 107 | { 108 | width: this.width, 109 | height: this.height 110 | }, 111 | 'switch' 112 | ) 113 | } 114 | 115 | async processFrame( 116 | inParams: Array, 117 | mixParams: KernelParams, 118 | overlays: Array, 119 | output: OpenCLBuffer, 120 | clQueue: number 121 | ): Promise { 122 | if (!(this.xform0 && this.xform1 && this.mixer && this.wiper && this.combiner)) 123 | throw new Error('Switch needs to be initialised') 124 | 125 | inParams[0].output = this.rgbaXf0 126 | await this.xform0.run(inParams[0], clQueue) 127 | 128 | if (this.numInputs > 1) { 129 | inParams[1].output = this.rgbaXf1 130 | await this.xform1.run(inParams[1], clQueue) 131 | if (mixParams.wipe) { 132 | /*mixParams.frac*/ 133 | await this.wiper.run( 134 | { input0: this.rgbaXf0, input1: this.rgbaXf1, wipe: mixParams.frac, output: this.rgbaMx }, 135 | clQueue 136 | ) 137 | } else { 138 | await this.mixer.run( 139 | { input0: this.rgbaXf0, input1: this.rgbaXf1, mix: mixParams.frac, output: this.rgbaMx }, 140 | clQueue 141 | ) 142 | } 143 | } 144 | 145 | return await this.combiner.run({ bgIn: this.rgbaMx, ovIn: overlays, output: output }, clQueue) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/process/transform.ts: -------------------------------------------------------------------------------- 1 | /* Copyright 2019 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | import { ProcessImpl } from './imageProcess' 17 | import { clContext as nodenCLContext, OpenCLBuffer, KernelParams } from 'nodencl' 18 | import { matrixFlatten, matrixMultiply } from './colourMaths' 19 | 20 | const transformKernel = ` 21 | __constant sampler_t samplerIn = 22 | CLK_NORMALIZED_COORDS_TRUE | 23 | CLK_ADDRESS_CLAMP | 24 | CLK_FILTER_LINEAR; 25 | 26 | __constant sampler_t samplerOut = 27 | CLK_NORMALIZED_COORDS_FALSE | 28 | CLK_ADDRESS_CLAMP | 29 | CLK_FILTER_NEAREST; 30 | 31 | __kernel void transform( 32 | __read_only image2d_t input, 33 | __global float4* restrict transformMatrix, 34 | __write_only image2d_t output) { 35 | 36 | int w = get_image_width(output); 37 | int h = get_image_height(output); 38 | 39 | // Load two rows of the 3x3 transform matrix via two float4s 40 | float4 tmpMat0 = transformMatrix[0]; 41 | float4 tmpMat1 = transformMatrix[1]; 42 | float3 mat0 = (float3)(tmpMat0.s0, tmpMat0.s1, tmpMat0.s2); 43 | float3 mat1 = (float3)(tmpMat0.s3, tmpMat1.s0, tmpMat1.s1); 44 | 45 | int outX = get_global_id(0); 46 | int outY = get_global_id(1); 47 | int2 posOut = {outX, outY}; 48 | 49 | float3 inPos = (float3)(outX / (float) w - 0.5f, outY / (float) h - 0.5f, 1.0f); 50 | float2 posIn = (float2)(dot(mat0, inPos) + 0.5f, dot(mat1, inPos) + 0.5f); 51 | 52 | float4 in = read_imagef(input, samplerIn, posIn); 53 | write_imagef(output, posOut, in); 54 | } 55 | ` 56 | 57 | export default class Transform extends ProcessImpl { 58 | clContext: nodenCLContext 59 | transformMatrix: Array 60 | transformArray: Float32Array 61 | matrixBuffer: OpenCLBuffer | null = null 62 | 63 | constructor(clContext: nodenCLContext, width: number, height: number) { 64 | super('transform', width, height, transformKernel, 'transform') 65 | 66 | this.clContext = clContext 67 | this.transformMatrix = [...new Array(3)].map(() => new Float32Array(3)) 68 | this.transformMatrix[0] = Float32Array.from([1.0, 0.0, 0.0]) 69 | this.transformMatrix[1] = Float32Array.from([0.0, 1.0, 0.0]) 70 | this.transformMatrix[2] = Float32Array.from([0.0, 0.0, 1.0]) 71 | this.transformArray = matrixFlatten(this.transformMatrix) 72 | } 73 | 74 | private async updateMatrix(clQueue: number): Promise { 75 | if (!this.matrixBuffer) throw new Error('Transform needs to be initialised') 76 | 77 | this.transformArray = matrixFlatten(this.transformMatrix) 78 | await this.matrixBuffer.hostAccess( 79 | 'writeonly', 80 | clQueue, 81 | Buffer.from(this.transformArray.buffer) 82 | ) 83 | return this.matrixBuffer.hostAccess('none', clQueue) 84 | } 85 | 86 | async init(): Promise { 87 | this.matrixBuffer = await this.clContext.createBuffer( 88 | this.transformArray.byteLength, 89 | 'readonly', 90 | 'coarse' 91 | ) 92 | return this.updateMatrix(this.clContext.queue.load) 93 | } 94 | 95 | async getKernelParams(params: KernelParams, clQueue: number): Promise { 96 | const aspect = this.width / this.height 97 | const flipX = (params.flipH as boolean) || false ? -1.0 : 1.0 98 | const flipY = (params.flipV as boolean) || false ? -1.0 : 1.0 99 | const scaleX = ((params.scale as number) || 1.0) * flipX * aspect 100 | const scaleY = ((params.scale as number) || 1.0) * flipY 101 | const offsetX = (params.offsetX as number) || 0.0 102 | const offsetY = (params.offsetY as number) || 0.0 103 | const rotate = (params.rotate as number) || 0.0 104 | 105 | const scaleMatrix = [...new Array(3)].map(() => new Float32Array(3)) 106 | scaleMatrix[0] = Float32Array.from([1.0 / scaleX, 0.0, 0.0]) 107 | scaleMatrix[1] = Float32Array.from([0.0, 1.0 / scaleY, 0.0]) 108 | scaleMatrix[2] = Float32Array.from([0.0, 0.0, 1.0]) 109 | 110 | const translateMatrix = [...new Array(3)].map(() => new Float32Array(3)) 111 | translateMatrix[0] = Float32Array.from([1.0, 0.0, offsetX]) 112 | translateMatrix[1] = Float32Array.from([0.0, 1.0, offsetY]) 113 | translateMatrix[2] = Float32Array.from([0.0, 0.0, 1.0]) 114 | 115 | const rotateMatrix = [...new Array(3)].map(() => new Float32Array(3)) 116 | rotateMatrix[0] = Float32Array.from([Math.cos(rotate), -Math.sin(rotate), 0.0]) 117 | rotateMatrix[1] = Float32Array.from([Math.sin(rotate), Math.cos(rotate), 0.0]) 118 | rotateMatrix[2] = Float32Array.from([0.0, 0.0, 1.0]) 119 | 120 | const projectMatrix = [...new Array(3)].map(() => new Float32Array(3)) 121 | projectMatrix[0] = Float32Array.from([aspect, 0.0, 0.0]) 122 | projectMatrix[1] = Float32Array.from([0.0, 1.0, 0.0]) 123 | projectMatrix[2] = Float32Array.from([0.0, 0.0, 1.0]) 124 | 125 | this.transformMatrix = matrixMultiply( 126 | matrixMultiply(matrixMultiply(scaleMatrix, translateMatrix), rotateMatrix), 127 | projectMatrix 128 | ) 129 | 130 | await this.updateMatrix(clQueue) 131 | return Promise.resolve({ 132 | input: params.input, 133 | transformMatrix: this.matrixBuffer, 134 | output: params.output 135 | }) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/process/wipe.ts: -------------------------------------------------------------------------------- 1 | /* Copyright 2019 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | import { ProcessImpl } from './imageProcess' 17 | import { KernelParams } from 'nodencl' 18 | 19 | const wipeKernel = ` 20 | __constant sampler_t sampler1 = 21 | CLK_NORMALIZED_COORDS_FALSE 22 | | CLK_ADDRESS_CLAMP_TO_EDGE 23 | | CLK_FILTER_NEAREST; 24 | 25 | __kernel void wipe( 26 | __read_only image2d_t input0, 27 | __read_only image2d_t input1, 28 | __private float wipe, 29 | __write_only image2d_t output) { 30 | 31 | int w = get_image_width(output); 32 | int h = get_image_height(output); 33 | 34 | int x = get_global_id(0); 35 | int y = get_global_id(1); 36 | float4 in0 = read_imagef(input0, sampler1, (int2)(x,y)); 37 | float4 in1 = read_imagef(input1, sampler1, (int2)(x,y)); 38 | 39 | float4 out = x > w * wipe ? in1 : in0; 40 | 41 | write_imagef(output, (int2)(x, y), out); 42 | }; 43 | ` 44 | export default class Wipe extends ProcessImpl { 45 | constructor(width: number, height: number) { 46 | super('wipe', width, height, wipeKernel, 'wipe') 47 | } 48 | 49 | async init(): Promise { 50 | return Promise.resolve() 51 | } 52 | 53 | async getKernelParams(params: KernelParams): Promise { 54 | return Promise.resolve({ 55 | input0: params.input0, 56 | input1: params.input1, 57 | wipe: params.wipe, 58 | output: params.output 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/producer/ffmpegProducer.ts: -------------------------------------------------------------------------------- 1 | /* Copyright 2020 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | import { SourceFrame } from '../chanLayer' 17 | import { ProducerFactory, Producer, InvalidProducerError } from './producer' 18 | import { clContext as nodenCLContext, OpenCLBuffer } from 'nodencl' 19 | import { Demuxer, demuxer, Decoder, decoder, Filterer, filterer, Packet, Frame } from 'beamcoder' 20 | import redio, { RedioPipe, nil, isEnd, isNil } from 'redioactive' 21 | import { ToRGBA } from '../process/io' 22 | import { Reader } from '../process/yuv422p10' 23 | 24 | export class FFmpegProducer implements Producer { 25 | private readonly id: string 26 | private params: string[] 27 | private clContext: nodenCLContext 28 | private demuxer: Demuxer | undefined 29 | private readonly decoders: Decoder[] 30 | private readonly filterers: Filterer[] 31 | private vidSource: RedioPipe | undefined 32 | private vidDecode: RedioPipe | undefined 33 | private vidFilter: RedioPipe | undefined 34 | private vidLoader: RedioPipe> | undefined 35 | private vidProcess: RedioPipe | undefined 36 | private toRGBA: ToRGBA | undefined 37 | 38 | constructor(id: string, params: string[], context: nodenCLContext) { 39 | this.id = id 40 | this.params = params 41 | this.clContext = context 42 | this.decoders = [] 43 | this.filterers = [] 44 | } 45 | 46 | async initialise(): Promise | null> { 47 | const url = this.params[0] 48 | let width = 0 49 | let height = 0 50 | try { 51 | this.demuxer = await demuxer(url) 52 | await this.demuxer.seek({ time: 20 }) 53 | // console.log('NumStreams:', this.demuxer.streams.length) 54 | this.demuxer.streams.forEach((_s, i) => { 55 | // eslint-disable-next-line @typescript-eslint/camelcase 56 | this.decoders.push(decoder({ demuxer: this.demuxer as Demuxer, stream_index: i })) 57 | }) 58 | 59 | const vidStream = this.demuxer.streams[0] 60 | width = vidStream.codecpar.width 61 | height = vidStream.codecpar.height 62 | this.filterers[0] = await filterer({ 63 | filterType: 'video', 64 | inputParams: [ 65 | { 66 | width: width, 67 | height: height, 68 | pixelFormat: vidStream.codecpar.format, 69 | timeBase: vidStream.time_base, 70 | pixelAspect: vidStream.codecpar.sample_aspect_ratio 71 | } 72 | ], 73 | outputParams: [ 74 | { 75 | pixelFormat: vidStream.codecpar.format 76 | } 77 | ], 78 | filterSpec: 'yadif=mode=send_field:parity=auto:deint=all' 79 | }) 80 | 81 | this.toRGBA = new ToRGBA( 82 | this.clContext, 83 | '709', 84 | '709', 85 | new Reader(vidStream.codecpar.width, vidStream.codecpar.height) 86 | ) 87 | await this.toRGBA.init() 88 | } catch (err) { 89 | throw new InvalidProducerError(err) 90 | } 91 | 92 | this.vidSource = redio( 93 | async (push, next) => { 94 | const packet = await this.demuxer?.read() 95 | // console.log('PKT:', packet?.stream_index, packet?.pts) 96 | if (packet && packet?.stream_index === 0) push(packet) 97 | next() 98 | }, 99 | { bufferSizeMax: 3 } 100 | ) 101 | 102 | this.vidDecode = this.vidSource.valve( 103 | async (packet) => { 104 | if (!isEnd(packet) && !isNil(packet)) { 105 | const pkt = packet as Packet 106 | const frm = await this.decoders[pkt.stream_index].decode(pkt) 107 | return frm.frames 108 | } else { 109 | return packet 110 | } 111 | }, 112 | { bufferSizeMax: 3, oneToMany: true } 113 | ) 114 | 115 | this.vidFilter = this.vidDecode.valve( 116 | async (frame) => { 117 | if (!isEnd(frame) && !isNil(frame)) { 118 | const frm = frame as Frame 119 | const ff = await this.filterers[0].filter([frm]) 120 | return ff[0].frames.length > 0 ? ff[0].frames : nil 121 | } else { 122 | return frame 123 | } 124 | }, 125 | { bufferSizeMax: 3, oneToMany: true } 126 | ) 127 | 128 | this.vidLoader = this.vidFilter.valve>( 129 | async (frame) => { 130 | if (!isEnd(frame) && !isNil(frame)) { 131 | const frm = frame as Frame 132 | const toRGBA = this.toRGBA as ToRGBA 133 | const clSources = await toRGBA.createSources() 134 | await toRGBA.loadFrame(frm.data, clSources, this.clContext.queue.load) 135 | await this.clContext.waitFinish(this.clContext.queue.load) 136 | return clSources 137 | } else { 138 | return frame 139 | } 140 | }, 141 | { bufferSizeMax: 3, oneToMany: false } 142 | ) 143 | 144 | this.vidProcess = this.vidLoader.valve( 145 | async (clSources) => { 146 | if (!isEnd(clSources) && !isNil(clSources)) { 147 | const clSrcs = clSources as Array 148 | const toRGBA = this.toRGBA as ToRGBA 149 | const clDest = await toRGBA.createDest({ width: width, height: height }) 150 | await toRGBA.processFrame(clSrcs, clDest, this.clContext.queue.process) 151 | await this.clContext.waitFinish(this.clContext.queue.process) 152 | clSrcs.forEach((s) => s.release()) 153 | const sourceFrame: SourceFrame = { video: clDest, audio: Buffer.alloc(0), timestamp: 0 } 154 | return sourceFrame 155 | } else { 156 | return clSources 157 | } 158 | }, 159 | { bufferSizeMax: 3, oneToMany: false } 160 | ) 161 | 162 | console.log(`Created FFmpeg producer ${this.id} for path ${url}`) 163 | return this.vidProcess 164 | } 165 | } 166 | 167 | export class FFmpegProducerFactory implements ProducerFactory { 168 | private clContext: nodenCLContext 169 | 170 | constructor(clContext: nodenCLContext) { 171 | this.clContext = clContext 172 | } 173 | 174 | createProducer(id: string, params: string[]): FFmpegProducer { 175 | return new FFmpegProducer(id, params, this.clContext) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/producer/producer.ts: -------------------------------------------------------------------------------- 1 | /* Copyright 2020 Streampunk Media Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | import { clContext as nodenCLContext } from 'nodencl' 17 | import { ChanLayer, SourceFrame } from '../chanLayer' 18 | import { FFmpegProducerFactory } from './ffmpegProducer' 19 | import { RedioPipe } from 'redioactive' 20 | 21 | export interface Producer { 22 | initialise(): Promise | null> 23 | } 24 | 25 | export interface ProducerFactory { 26 | createProducer(id: string, params: string[]): T 27 | } 28 | 29 | export class InvalidProducerError extends Error { 30 | constructor(message?: string) { 31 | super(message) 32 | // see: typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html 33 | Object.setPrototypeOf(this, new.target.prototype) // restore prototype chain 34 | this.name = InvalidProducerError.name // stack traces display correctly now 35 | } 36 | } 37 | export class ProducerRegistry { 38 | private readonly producerFactories: ProducerFactory[] 39 | 40 | constructor(clContext: nodenCLContext) { 41 | this.producerFactories = [] 42 | this.producerFactories.push(new FFmpegProducerFactory(clContext)) 43 | } 44 | 45 | async createSource(chanLay: ChanLayer, params: string[]): Promise | null> { 46 | const id = `${chanLay.channel}-${chanLay.layer}` 47 | let p: RedioPipe | null = null 48 | for (const f of this.producerFactories) { 49 | try { 50 | const producer = f.createProducer(id, params) as Producer 51 | if ((p = await producer.initialise()) !== null) break 52 | } catch (err) { 53 | if (!(err instanceof InvalidProducerError)) { 54 | throw err 55 | } 56 | } 57 | } 58 | 59 | if (p === null) { 60 | console.log(`Failed to find producer for params: '${params}'`) 61 | } 62 | 63 | return p 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "ES2018" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true /* Generates corresponding '.d.ts' file. */, 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./lib" /* Redirect output structure to the directory. */, 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true /* Enable all strict type-checking options. */, 27 | // "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 28 | // "strictNullChecks": true /* Enable strict null checks. */, 29 | // "strictFunctionTypes": true /* Enable strict checking of function types. */, 30 | // "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */, 31 | // "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 32 | // "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 33 | // "alwaysStrict": false /* Parse in strict mode and emit "use strict" for each source file. */, 34 | 35 | /* Additional Checks */ 36 | "noUnusedLocals": true /* Report errors on unused locals. */, 37 | "noUnusedParameters": true /* Report errors on unused parameters. */, 38 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 39 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 40 | 41 | /* Module Resolution Options */ 42 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | }, 66 | "exclude": [ 67 | "lib/**/*", 68 | "src/scratch/**/*" 69 | ] 70 | } --------------------------------------------------------------------------------