├── 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 |
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 |
--------------------------------------------------------------------------------