├── README.md ├── package.json ├── index.html └── src └── index.js /README.md: -------------------------------------------------------------------------------- 1 | # FFmpeg CMD Viewer 2 | 鉴于 FFmpeg 命令经常又臭又长,导致阅读十分困难。所以本项目用来格式化 FFmpeg 命令,以更清晰地阅读命令中的参数。 3 | 4 | [Demo](http://shangxin.me/ffmpeg-cmd-viewer/) 5 | 6 | # TODO 7 | ## Feature 8 | * ~~filter complex graph 绘制输入音视频流~~ 9 | * ~~filter complex graph 绘制输出~~ 10 | * filter complex graph 标明 pads 输入输出顺序 11 | 12 | ## Bug 13 | * 无法清除上一次绘制 14 | * formatted string 输出丢失 15 | * description 字样丢失 16 | * ~~filter complex 两个节点间无法同时存在两条边~~ 17 | * ~~替换转义符~~ 18 | * 负数被误解析成命令 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ffmpeg-cmd-viewer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./src/index.js", 6 | "type": "module", 7 | "directories": { 8 | "test": "test" 9 | }, 10 | "scripts": { 11 | "test": "node ./test/index.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/YoungSx/ffmpeg-cmd-viewer.git" 16 | }, 17 | "author": { 18 | "name": "Shangxin", 19 | "email": "shangxin@outlook.com", 20 | "url": "http://shangxin.me/" 21 | }, 22 | "license": "Apache-2.0", 23 | "bugs": { 24 | "url": "https://github.com/YoungSx/ffmpeg-cmd-viewer/issues" 25 | }, 26 | "homepage": "https://github.com/YoungSx/ffmpeg-cmd-viewer#readme" 27 | } 28 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | FFmpeg CMD Viewer Demo 7 | 28 | 37 | 38 | 39 | 40 |

FFmpeg CMD Viewer

41 | 45 | 58 | 59 |
60 | 61 |
62 |
 63 |         
64 |
65 |

Filter Complex Graph

66 | 67 | 71 | 72 | 73 | 74 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | class FFmpegCmdViewer { 2 | _removeEscapeChar (str) { 3 | return str.replace(/[\\]/g, '') 4 | } 5 | 6 | haveCodecParams (str) { 7 | return str.search(/-[a-zA-Z0-9]+-params/i) >= 0 8 | } 9 | 10 | isStreamSpecifier (str) { 11 | return str.search(/[0-9]:[vVasdt](:[0-9])?/i) >= 0 12 | } 13 | 14 | filterPadsParser (str) { 15 | const regex = new RegExp(`(?<=\\[)(.+?)(?=\\])`, 'g') 16 | const result = str.match(regex) 17 | return result instanceof Array ? result.map(x => x.trim()).filter(x => '' !== x) : [] 18 | } 19 | 20 | filterPadsInParser (str) { 21 | const filterPadsInStringParser = (str) => { 22 | const regex = new RegExp(`^(\\[\\S*?(?:\\]))+`, 'g') 23 | const result = str.match(regex) 24 | return result instanceof Array ? result.map(x => x.trim()).filter(x => '' !== x) : [] 25 | } 26 | 27 | const padsInStrings = filterPadsInStringParser(str) 28 | const padsInString = padsInStrings.length > 0 ? padsInStrings[0] : '' 29 | 30 | return this.filterPadsParser(padsInString) 31 | } 32 | 33 | filterPadsOutParser (str) { 34 | const filterPadsOutStringParser = (str) => { 35 | const regex = new RegExp(`(?<=[^\\]])((\\[\\w*(?:\\]))+$)`, 'g') 36 | const result = str.match(regex) 37 | return result instanceof Array ? result.map(x => x.trim()).filter(x => '' !== x) : [] 38 | } 39 | 40 | const padsOutStrings = filterPadsOutStringParser(str) 41 | const padsOutString = padsOutStrings.length > 0 ? padsOutStrings[0] : '' 42 | 43 | return this.filterPadsParser(padsOutString) 44 | } 45 | 46 | filterOptParser (str) { 47 | const regex = new RegExp(`(?:\\[)[^\\[\\]]*(?:\\])`) 48 | return str.trim().split(regex).map(x => x.trim()).filter(x => '' !== x) 49 | } 50 | 51 | codecParamsParser (str) { 52 | const regex = new RegExp(`(-[a-zA-Z0-9]+-params[ ]+[^ ]+)`) 53 | return str.trim().split(regex).map(x => x.trim()).filter(x => '' !== x) 54 | } 55 | 56 | quoteParamsParser (str) { 57 | const regex = new RegExp(`(-[a-zA-Z0-9_-]+[ ]+(?:"(?:[^"]+)"|\'(?:[^\']+)\'))`) 58 | return str.trim().split(regex).map(x => x.trim()).filter(x => '' !== x) 59 | } 60 | 61 | ffmpegSingleParamParser (str) { 62 | const regex = new RegExp(`(-[a-zA-Z0-9_-]+[ ]+)`) 63 | const regexQuote = new RegExp(`(((?<=\\')(.*)(?=\\'))|((?<=\\")(.*)(?=\\")))`, 'g') 64 | const result = str.trim().split(regex).map(x => x.trim()).filter(x => '' !== x) 65 | 66 | if (result.length >= 2) { 67 | if (result[1].search(regexQuote) > 0) result[1] = result[1].match(regexQuote)[0] 68 | } 69 | return result 70 | } 71 | 72 | insertStartPlaceholder (filters) { 73 | let result = Object.assign([], filters) 74 | const startFakeFilter = { 75 | in: [], 76 | opt: ['start'], 77 | type: 'placeholder', 78 | out: [] 79 | } 80 | result.forEach(filter => { 81 | const inPads = filter['in'] 82 | inPads.forEach(pad => { 83 | if (this.isStreamSpecifier(pad)) startFakeFilter['out'].push(pad) 84 | }) 85 | }) 86 | if (startFakeFilter['out'].length > 0) result.unshift(startFakeFilter) 87 | 88 | return result 89 | } 90 | 91 | insertEndPlaceholder (filters) { 92 | let result = Object.assign([], filters) 93 | const filtersRelation = this.filterComplexRelation(filters) 94 | 95 | const endFakeFilter = { 96 | in: [], 97 | opt: ['end'], 98 | type: 'placeholder', 99 | out: [] 100 | } 101 | for (let i = 0; i < filters.length; i++) { 102 | const sourceFilter = filters[i] 103 | for (let j = 0; j < sourceFilter['out'].length; j++) { 104 | const outPad = sourceFilter['out'][j] 105 | const targetPos = filtersRelation['toMap'].get(outPad) // [filterIndex, inPadIndex] 106 | // no out 107 | if (!targetPos) endFakeFilter['in'].push(outPad) 108 | } 109 | } 110 | if (endFakeFilter['in'].length > 0) result.push(endFakeFilter) 111 | 112 | return result 113 | } 114 | 115 | /** 116 | * Interface FilterStruct { 117 | * in: Array; 118 | * opt: string; 119 | * type: string; 120 | * out: Array; 121 | * } 122 | * 123 | * Interface FilterStruct: Array 124 | */ 125 | filterComplexParser (filters) { 126 | const result = [] 127 | 128 | filters.forEach(filter => { 129 | filter = filter['name'] 130 | const filterStruct = { 131 | in: this.filterPadsInParser(filter), 132 | opt: this.filterOptParser(filter), 133 | type: 'opt', 134 | out: this.filterPadsOutParser(filter) 135 | } 136 | 137 | result.push(filterStruct) 138 | }) 139 | 140 | return result 141 | } 142 | 143 | filterComplexRelation (filterStructs) { 144 | const padsFromMap = new Map() 145 | const padsToMap = new Map() 146 | 147 | for (let i = 0; i < filterStructs.length; i++) { 148 | const filterStruct = filterStructs[i] 149 | const inPads = filterStruct['in'] 150 | const outPads = filterStruct['out'] 151 | 152 | for (let j = 0; j < inPads.length; j++) { 153 | const inPad = inPads[j] 154 | padsToMap.set(inPad, [i, j]) // [filterIndex, inPadIndex] 155 | } 156 | 157 | for (let j = 0; j < outPads.length; j++) { 158 | const outPad = outPads[j] 159 | padsFromMap.set(outPad, [i, j]) // [filterIndex, inPadIndex] 160 | } 161 | } 162 | 163 | return { 164 | "fromMap": padsFromMap, 165 | "toMap": padsToMap 166 | } 167 | } 168 | 169 | filterComplexGraphD3Data (list) { 170 | let filterDicts = [] 171 | 172 | for (let i = 0; i < list.length; i++) 173 | if ('-lavfi' == list[i]['name'] || '-filter_complex' == list[i]['name']) { 174 | filterDicts = list[i]['value'] 175 | break 176 | } 177 | 178 | let filters = this.filterComplexParser(filterDicts) 179 | filters = this.insertStartPlaceholder(filters) 180 | filters = this.insertEndPlaceholder(filters) 181 | const filtersRelation = this.filterComplexRelation(filters) 182 | 183 | return { 184 | "nodes": this.filterComplexGraphNode(filters), 185 | "edges": this.filterComplexGraphLink(filters, filtersRelation) 186 | } 187 | } 188 | 189 | filterComplexGraphNode (filters) { 190 | const result = [] 191 | 192 | for (let i = 0; i < filters.length; i++) { 193 | result.push({ 194 | "id": `${i}:${filters[i]['opt']}`, 195 | "label": `${filters[i]['opt']}`, 196 | "type": filters[i]['type'], 197 | "shape": "rect" 198 | }) 199 | } 200 | return result 201 | } 202 | 203 | filterComplexGraphLink (filters, filtersRelation) { 204 | const result = [] 205 | 206 | for (let i = 0; i < filters.length; i++) { 207 | const sourceFilter = filters[i] 208 | for (let j = 0; j < sourceFilter['out'].length; j++) { 209 | const outPad = sourceFilter['out'][j] 210 | const targetPos = filtersRelation['toMap'].get(outPad) // [filterIndex, inPadIndex] 211 | // no out 212 | if (!targetPos) continue 213 | const targetFilter = filters[targetPos[0]] 214 | result.push({ 215 | "source": `${i}:${sourceFilter['opt']}`, 216 | "target": `${targetPos[0]}:${targetFilter['opt'][0]}`, 217 | "label": outPad 218 | }) 219 | } 220 | } 221 | 222 | return result 223 | } 224 | 225 | ffmpegParamsParser (str, separator = '-', position = -1) { 226 | const result = [] 227 | const regex = position < 0 ? new RegExp(`(?:${separator})`) : new RegExp(`(?=[ ]${separator})`) 228 | const params = str.trim().split(regex).map(x => x.trim()).filter(x => '' !== x) 229 | 230 | params.forEach(param => { 231 | const paramArr = this.ffmpegSingleParamParser(param) 232 | 233 | if ('-lavfi' == paramArr[0] || '-filter_complex' == paramArr[0]) { 234 | result.push({ 235 | name: paramArr[0], 236 | value: this.ffmpegParamsParser(paramArr[paramArr.length - 1], ';', -1) 237 | }) 238 | } else { 239 | result.push({ 240 | name: paramArr[0], 241 | value: paramArr.length > 1 ? paramArr[paramArr.length - 1] : '' 242 | }) 243 | } 244 | }) 245 | return result 246 | } 247 | 248 | dump (params, deep = -1) { 249 | const indentUnit = ' ' 250 | let indent = '' 251 | let result = '' 252 | for (let i = 0; i < deep; i++) indent += indentUnit 253 | if (params instanceof Array) { 254 | for (let i = 0; i < params.length; i++) { 255 | result += this.dump(params[i], deep + 1) 256 | } 257 | } else if (params instanceof Object) { 258 | if ('string' == typeof params['value']) result += `${indent}${params['name']} ${this.dump(params['value'], deep + 1)}\n` 259 | else result += `${indent}${params['name']}\n${this.dump(params['value'], deep + 1)}` 260 | } else result = `${params}` 261 | return result 262 | } 263 | 264 | parser (cmd) { 265 | let result = [] 266 | const codectedParams = this.codecParamsParser(this._removeEscapeChar(cmd)) 267 | 268 | codectedParams.forEach(param => { 269 | if (this.haveCodecParams(param)) { 270 | const paramArr = this.ffmpegSingleParamParser(param) 271 | result.push({ 272 | name: paramArr[0], 273 | value: this.ffmpegParamsParser(paramArr[paramArr.length - 1], ':', -1) 274 | }) 275 | } else { 276 | const quotedParams = this.quoteParamsParser(param) 277 | quotedParams.forEach(quotedParam => { 278 | const t = this.ffmpegParamsParser(quotedParam, '-', 1) 279 | result = [...result, ...t] 280 | }) 281 | 282 | } 283 | }) 284 | return result 285 | } 286 | 287 | format (cmd) { 288 | return this.dump(this.parser(cmd)) 289 | } 290 | } 291 | 292 | export default new FFmpegCmdViewer() 293 | --------------------------------------------------------------------------------