Line | Hits | Source |
---|---|---|
1 | ||
2 | 1 | exports = module.exports = function Calculate(command) { |
3 | 1 | this._aspectIsEqual = function(ar1, ar2) { |
4 | 5 | var p1 = this.toAspectRatio(ar1); |
5 | 5 | var p2 = this.toAspectRatio(ar2); |
6 | 5 | if (p1 === undefined || p2 === undefined) { |
7 | 0 | return false; |
8 | } else { | |
9 | 5 | return (p1.x === p2.x && p1.y === p2.y); |
10 | } | |
11 | }; | |
12 | ||
13 | 1 | this._calculatePadding = function(data) { |
14 | 5 | if (data.video.aspect) { |
15 | 5 | var newaspect, padAmount; |
16 | // check if the aspect ratio has changed | |
17 | 5 | if (this.options.video.aspect && !this.options.video.size) { |
18 | 2 | newaspect = this.options.video.aspect; |
19 | 3 | } else if (!this.options.video.aspect) { |
20 | // check aspect ratio change by calculating new aspect ratio from size (using greatest common divider, GCD) | |
21 | 1 | var ratio = this.gcd(this.options.video.width, this.options.video.height); |
22 | 1 | newaspect = this.options.video.width / ratio + ':' + this.options.video.height / ratio; |
23 | } else { | |
24 | // we have both aspect ratio and size set, all calculations are fine | |
25 | 2 | newaspect = this.options.video.aspect; |
26 | } | |
27 | ||
28 | // if there are still no sizes for our output video, assume input size | |
29 | 5 | if (!this.options.video.width && !this.options.video.height) { |
30 | 2 | this.options.video.width = data.video.resolution.w; |
31 | 2 | this.options.video.height = data.video.resolution.h; |
32 | } | |
33 | ||
34 | 5 | if (!this._aspectIsEqual(data.video.aspectString, newaspect)) { |
35 | 5 | var ardata = this.toAspectRatio(newaspect); |
36 | ||
37 | 5 | if (newaspect === '16:9') { |
38 | // assume conversion from 4:3 to 16:9, pad output video stream left- / right-sided | |
39 | 1 | var newWidth = parseInt(this.options.video.width / (4 / 3), 10); |
40 | 1 | newWidth += (newWidth % 2); |
41 | 1 | var wdiff = this.options.video.width - newWidth; |
42 | 1 | padAmount = parseInt(wdiff / 2, 10); |
43 | 1 | padAmount += (padAmount % 2); |
44 | ||
45 | // set pad filter options | |
46 | 1 | this.options.video.pad = { |
47 | x: padAmount, | |
48 | y: 0, | |
49 | w: this.options.video.width, | |
50 | h: this.options.video.height | |
51 | }; | |
52 | 1 | this.options.video.size = newWidth + 'x' + this.options.video.height; |
53 | 4 | } else if (newaspect === '4:3') { |
54 | // assume conversion from 16:9 to 4:3, add padding to top and bottom | |
55 | 4 | var newHeight = parseInt(this.options.video.height / (4 / 3), 10); |
56 | 4 | newHeight -= (newHeight % 2); |
57 | 4 | var hdiff = this.options.video.height - newHeight; |
58 | 4 | padAmount = parseInt(hdiff / 2, 10); |
59 | 4 | padAmount += (padAmount % 2); |
60 | ||
61 | // set pad filter options | |
62 | 4 | this.options.video.pad = { |
63 | x: 0, | |
64 | y: padAmount, | |
65 | w: this.options.video.width, | |
66 | h: this.options.video.height | |
67 | }; | |
68 | 4 | this.options.video.size = this.options.video.pad.w + 'x' + newHeight; |
69 | } | |
70 | } | |
71 | } else { | |
72 | // aspect ratio could not be read from source file | |
73 | 0 | return; |
74 | } | |
75 | }; | |
76 | ||
77 | 1 | this._calculateDimensions = function(data) { |
78 | // load metadata and prepare size calculations | |
79 | 18 | var fixedWidth = /([0-9]+)x\?/.exec(this.options.video.size); |
80 | 18 | var fixedHeight = /\?x([0-9]+)/.exec(this.options.video.size); |
81 | 18 | var percentRatio = /\b([0-9]{1,2})%/.exec(this.options.video.size); |
82 | ||
83 | 18 | var resolution = this.options.keepPixelAspect ? data.video.resolution : data.video.resolutionSquare; |
84 | 18 | var w, h; |
85 | ||
86 | 18 | if (!resolution) { |
87 | 0 | return new Error('could not determine video resolution, check your ffmpeg setup'); |
88 | } | |
89 | ||
90 | 18 | var ratio, ardata; |
91 | 18 | if (fixedWidth && fixedWidth.length > 0) { |
92 | // calculate height of output | |
93 | 15 | if (!resolution.w) { |
94 | 0 | return new Error('could not determine width of source video, aborting execution'); |
95 | } | |
96 | ||
97 | 15 | ratio = resolution.w / parseInt(fixedWidth[1], 10); |
98 | // if we have an aspect ratio target set, calculate new size using AR | |
99 | 15 | if (this.options.video.aspect !== undefined) { |
100 | 1 | ardata = this.toAspectRatio(this.options.video.aspect); |
101 | 1 | if (ardata) { |
102 | 1 | w = parseInt(fixedWidth[1], 10); |
103 | 1 | h = Math.round((w / ardata.x) * ardata.y); |
104 | } else { | |
105 | // aspect ratio could not be parsed, return error | |
106 | 0 | return new Error('could not parse aspect ratio set using withAspect(), aborting execution'); |
107 | } | |
108 | } else { | |
109 | 14 | w = parseInt(fixedWidth[1], 10); |
110 | 14 | h = Math.round(resolution.h / ratio); |
111 | } | |
112 | 3 | } else if (fixedHeight && fixedHeight.length > 0) { |
113 | // calculate width of output | |
114 | 2 | if (!resolution.h) { |
115 | 0 | return new Error('could not determine height of source video, aborting execution'); |
116 | } | |
117 | ||
118 | 2 | ratio = resolution.h / parseInt(fixedHeight[1], 10); |
119 | ||
120 | // if we have an aspect ratio target set, calculate new size using AR | |
121 | 2 | if (this.options.video.aspect !== undefined) { |
122 | 1 | ardata = this.toAspectRatio(this.options.video.aspect); |
123 | 1 | if (ardata) { |
124 | 1 | h = parseInt(fixedHeight[1], 10); |
125 | 1 | w = Math.round((h / ardata.y) * ardata.x); |
126 | } else { | |
127 | // aspect ratio could not be parsed, return error | |
128 | 0 | return new Error('could not parse aspect ratio set using withAspect(), aborting execution'); |
129 | } | |
130 | } else { | |
131 | 1 | w = Math.round(resolution.w / ratio); |
132 | 1 | h = parseInt(fixedHeight[1], 10); |
133 | } | |
134 | 1 | } else if (percentRatio && percentRatio.length > 0) { |
135 | // calculate both height and width of output | |
136 | 1 | if (!resolution.w || !resolution.h) { |
137 | 0 | return new Error('could not determine resolution of source video, aborting execution'); |
138 | } | |
139 | ||
140 | 1 | ratio = parseInt(percentRatio[1], 10) / 100; |
141 | 1 | w = Math.round(resolution.w * ratio); |
142 | 1 | h = Math.round(resolution.h * ratio); |
143 | } else { | |
144 | 0 | return new Error('could not determine type of size string, aborting execution'); |
145 | } | |
146 | ||
147 | // for video resizing, width and height have to be a multiple of 2 | |
148 | 18 | if (w % 2 === 1) { |
149 | 0 | w -= 1; |
150 | } | |
151 | 18 | if (h % 2 === 1) { |
152 | 0 | h -= 1; |
153 | } | |
154 | ||
155 | 18 | this.options.video.size = w + 'x' + h; |
156 | ||
157 | 18 | this.options.video.width = w; |
158 | 18 | this.options.video.height = h; |
159 | ||
160 | }; | |
161 | 1 | exports.calculateDimensions = this._calculateDimensions; |
162 | }; |
Line | Hits | Source |
---|---|---|
1 | 1 | exports = module.exports = function Debug(command) { |
2 | 1 | this.getCommand = function(outputmethod, callback) { |
3 | 1 | var self = this; |
4 | 1 | this._prepare(function(err, meta) { |
5 | 1 | if (err) { |
6 | 0 | callback(null, err); |
7 | } else { | |
8 | 1 | var args = self.buildFfmpegArgs(true, meta); |
9 | // kinda hacky, have to make sure the returned object is no array | |
10 | 1 | if (args.length === undefined) { |
11 | 0 | callback(null, args); |
12 | } else { | |
13 | 1 | var cmd = ''; |
14 | 1 | cmd += 'ffmpeg'; |
15 | 1 | args.forEach(function(el) { |
16 | 18 | cmd += ' ' + el; |
17 | }); | |
18 | 1 | callback(cmd, null); |
19 | } | |
20 | } | |
21 | }); | |
22 | 1 | return this; |
23 | }; | |
24 | ||
25 | 1 | this.getArgs = function(callback) { |
26 | 35 | if (callback) { |
27 | 35 | var self = this; |
28 | 35 | this._prepare(function(err, meta) { |
29 | 35 | if (err) { |
30 | 0 | callback(null, err); |
31 | } else { | |
32 | 35 | var args = self.buildFfmpegArgs(true, meta); |
33 | // kinda hacky, have to make sure the returned object is no array | |
34 | 35 | if (args.length === undefined) { |
35 | 0 | callback(null, args); |
36 | } else { | |
37 | 35 | callback(args, null); |
38 | } | |
39 | } | |
40 | }); | |
41 | } else { | |
42 | 0 | return this.buildFfmpegArgs(true, null); |
43 | } | |
44 | }; | |
45 | }; |
Line | Hits | Source |
---|---|---|
1 | ||
2 | 1 | exports = module.exports = function Extensions() { |
3 | ||
4 | 2 | this.ffmpegPath = process.env.FFMPEG_PATH || 'ffmpeg'; |
5 | ||
6 | 2 | this.setFfmpegPath = function(path) { |
7 | 0 | this.ffmpegPath = path; |
8 | }; | |
9 | ||
10 | 2 | this.determineFfmpegPath = function() { |
11 | 0 | if (process.env.FFMPEG_PATH) { |
12 | 0 | return process.env.FFMPEG_PATH; |
13 | } | |
14 | 0 | return 'ffmpeg'; |
15 | }; | |
16 | ||
17 | 2 | this.gcd = function(a, b) { |
18 | 1 | if (!a && !b) { |
19 | 0 | return 0; |
20 | } | |
21 | 1 | while (a !== 0) { |
22 | 3 | var z = b % a; |
23 | 3 | b = a; |
24 | 3 | a = z; |
25 | } | |
26 | 1 | return b; |
27 | }; | |
28 | ||
29 | 2 | this.toAspectRatio = function(ar) { |
30 | 19 | var p = ar.split(':'); |
31 | 19 | if (p.length !== 2) { |
32 | 1 | return undefined; |
33 | } else { | |
34 | 18 | return { |
35 | x: parseInt(p[0], 10), | |
36 | y: parseInt(p[1], 10) | |
37 | }; | |
38 | } | |
39 | }; | |
40 | ||
41 | 2 | this.ffmpegTimemarkToSeconds = function(timemark) { |
42 | 47 | var parts = timemark.split(':'); |
43 | 47 | var secs = 0; |
44 | ||
45 | // add hours | |
46 | 47 | secs += parseInt(parts[0], 10) * 3600; |
47 | // add minutes | |
48 | 47 | secs += parseInt(parts[1], 10) * 60; |
49 | ||
50 | // split sec/msec part | |
51 | 47 | var secParts = parts[2].split('.'); |
52 | ||
53 | // add seconds | |
54 | 47 | secs += parseInt(secParts[0], 10); |
55 | ||
56 | 47 | return secs; |
57 | }; | |
58 | ||
59 | 2 | this.parseVersionString = function(versionstr) { |
60 | 19 | if (typeof versionstr != 'string' || versionstr.indexOf('.') == -1) { |
61 | 1 | return false; |
62 | } | |
63 | 18 | var x = versionstr.split('.'); |
64 | // parse from string or default to 0 if can't parse | |
65 | 18 | var maj = parseInt(x[0], 10) || 0; |
66 | 18 | var min = parseInt(x[1], 10) || 0; |
67 | 18 | var pat = parseInt(x[2], 10) || 0; |
68 | 18 | return { |
69 | major: maj, | |
70 | minor: min, | |
71 | patch: pat | |
72 | }; | |
73 | }; | |
74 | ||
75 | 2 | this.atLeastVersion = function(actualVersion, minVersion) { |
76 | 9 | var minimum = this.parseVersionString(minVersion); |
77 | 9 | var running = this.parseVersionString(actualVersion); |
78 | ||
79 | // if we can't even parse the version string (affects git builds for windows), | |
80 | // we simply return true and assume a current build | |
81 | 9 | if (!running) |
82 | 1 | return true; |
83 | ||
84 | 8 | if (running.major !== minimum.major) { |
85 | 5 | return (running.major > minimum.major); |
86 | } else { | |
87 | 3 | if (running.minor !== minimum.minor) { |
88 | 1 | return (running.minor > minimum.minor); |
89 | } else { | |
90 | 2 | if (running.patch !== minimum.patch) { |
91 | 1 | return (running.patch > minimum.patch); |
92 | } else { | |
93 | 1 | return true; |
94 | } | |
95 | } | |
96 | } | |
97 | }; | |
98 | }; |
Line | Hits | Source |
---|---|---|
1 | 1 | var path = require('path'), |
2 | async = require('../support/async.min.js'), | |
3 | exec = require('child_process').exec, | |
4 | spawn = require('child_process').spawn; | |
5 | ||
6 | /* options object consists of the following keys: | |
7 | * - source: either a ReadableStream or the path to a file (required) | |
8 | * - timeout: timeout in seconds for all ffmpeg sub-processes (optional, defaults to 30) | |
9 | * - priority: default-priority for all ffmpeg sub-processes (optional, defaults to 0) | |
10 | * - logger: add a winston logging instance (optional, default is clumsy console logging) | |
11 | * - nolog: completely disables any logging | |
12 | */ | |
13 | 1 | function FfmpegCommand(args) { |
14 | 49 | var source = args.source, |
15 | timeout = args.timeout != null ? args.timeout : 30, | |
16 | priority = args.priority || 0, | |
17 | logger = args.logger || null, | |
18 | nologging = args.nolog || false, | |
19 | inputlive = args.inputlive || false; | |
20 | ||
21 | 49 | if (!logger && !nologging) { |
22 | // create new winston instance | |
23 | 2 | logger = require('winston'); |
24 | 47 | } else if (!logger && nologging) { |
25 | // create fake object to route log calls | |
26 | 47 | logger = { |
27 | debug: function() {}, | |
28 | info: function() {}, | |
29 | warn: function() {}, | |
30 | error: function() {} | |
31 | }; | |
32 | } | |
33 | ||
34 | // make sure execution is not killed on error | |
35 | 49 | logger.exitOnError = false; |
36 | ||
37 | // check if argument is a stream | |
38 | 49 | var srcstream, srcfile; |
39 | 49 | if (typeof source === 'object') { |
40 | 2 | if (source.readable) { |
41 | // streaming mode | |
42 | 2 | source.pause(); |
43 | 2 | srcstream = source; |
44 | 2 | srcfile = source.path; |
45 | } else { | |
46 | 0 | logger.error('Source is not a ReadableStream instance'); |
47 | 0 | throw new Error('Source is not a ReadableStream instance'); |
48 | } | |
49 | } else { | |
50 | // file mode | |
51 | 47 | srcfile = source; |
52 | } | |
53 | ||
54 | 49 | this.options = { |
55 | _isStreamable: true, | |
56 | _updateFlvMetadata: false, | |
57 | _useConstantVideoBitrate: false, | |
58 | _nice: { level: priority }, | |
59 | keepPixelAspect: false, | |
60 | inputfile: srcfile, | |
61 | inputstream: srcstream, | |
62 | inputlive: inputlive, | |
63 | timeout: timeout, | |
64 | mergeList:[], | |
65 | video: {}, | |
66 | audio: {}, | |
67 | additional: [], | |
68 | otherInputs: [], | |
69 | informInputAudioCodec: null, | |
70 | informInputVideoCodec: null, | |
71 | logger: logger | |
72 | }; | |
73 | ||
74 | // public chaining methods | |
75 | 49 | FfmpegCommand.prototype.usingPreset = function(preset) { |
76 | // require preset (since require() works like a singleton, multiple calls generate no overhead) | |
77 | 12 | try { |
78 | 12 | var module = require('./presets/' + preset); |
79 | 11 | if (typeof module.load === 'function') { |
80 | 11 | module.load(this); |
81 | } | |
82 | 11 | return this; |
83 | } catch (err) { | |
84 | 1 | throw new Error('preset ' + preset + ' could not be loaded'); |
85 | } | |
86 | 0 | return this; |
87 | }; | |
88 | 49 | FfmpegCommand.prototype.withNoVideo = function() { |
89 | 2 | this.options.video.skip = true; |
90 | 2 | return this; |
91 | }; | |
92 | 49 | FfmpegCommand.prototype.withNoAudio = function() { |
93 | 2 | this.options.audio.skip = true; |
94 | 2 | return this; |
95 | }; | |
96 | 49 | FfmpegCommand.prototype.withVideoBitrate = function(vbitrate, type) { |
97 | 13 | if (typeof vbitrate === 'string' && vbitrate.indexOf('k') > 0) { |
98 | 13 | vbitrate = vbitrate.replace('k', ''); |
99 | } | |
100 | 13 | if (type && type === exports.CONSTANT_BITRATE) { |
101 | 1 | this.options._useConstantVideoBitrate = true; |
102 | } | |
103 | 13 | this.options.video.bitrate = parseInt(vbitrate, 10); |
104 | 13 | return this; |
105 | }; | |
106 | 49 | FfmpegCommand.prototype.withSize = function(sizeString) { |
107 | 22 | this.options.video.size = sizeString; |
108 | 22 | return this; |
109 | }; | |
110 | 49 | FfmpegCommand.prototype.applyAutopadding = function(autopad, color) { |
111 | 5 | this.options._applyAutopad = autopad; |
112 | 5 | if (!color) { |
113 | 4 | this.options.video.padcolor = 'black'; |
114 | } else { | |
115 | 1 | this.options.video.padcolor = color; |
116 | } | |
117 | 5 | return this; |
118 | }; | |
119 | 49 | FfmpegCommand.prototype.withFps = function(fps) { |
120 | 9 | this.options.video.fps = fps; |
121 | 9 | return this; |
122 | }; | |
123 | 49 | FfmpegCommand.prototype.withFpsInput = function(fps) { |
124 | 0 | this.options.video.fpsInput = fps; |
125 | 0 | return this; |
126 | }; | |
127 | 49 | FfmpegCommand.prototype.withFpsOutput = function(fps) { |
128 | 0 | this.options.video.fpsOutput = fps; |
129 | 0 | return this; |
130 | }; | |
131 | 49 | FfmpegCommand.prototype.withAspect = function(aspectRatio) { |
132 | 5 | this.options.video.aspect = aspectRatio; |
133 | 5 | return this; |
134 | }; | |
135 | 49 | FfmpegCommand.prototype.keepPixelAspect = function(bool) { |
136 | 0 | this.options.keepPixelAspect = bool ? true : false; |
137 | 0 | return this; |
138 | }; | |
139 | 49 | FfmpegCommand.prototype.withVideoCodec = function(codec) { |
140 | 12 | this.options.video.codec = codec; |
141 | 12 | return this; |
142 | }; | |
143 | 49 | FfmpegCommand.prototype.loop = function(duration) { |
144 | 3 | this.options.video.loop = true; |
145 | 3 | if (duration) { |
146 | 2 | this.options.duration = duration; |
147 | } | |
148 | 3 | return this; |
149 | }; | |
150 | 49 | FfmpegCommand.prototype.takeFrames = function(frameCount) { |
151 | 1 | this.options.video.framecount = frameCount; |
152 | 1 | return this; |
153 | }; | |
154 | 49 | FfmpegCommand.prototype.withAudioBitrate = function(abitrate) { |
155 | 13 | if (typeof abitrate === 'string' && abitrate.indexOf('k') > 0) { |
156 | 12 | abitrate = abitrate.replace('k', ''); |
157 | } | |
158 | 13 | this.options.audio.bitrate = parseInt(abitrate, 10); |
159 | 13 | return this; |
160 | }; | |
161 | 49 | FfmpegCommand.prototype.withAudioCodec = function(audiocodec){ |
162 | 12 | this.options.audio.codec = audiocodec; |
163 | 12 | return this; |
164 | }; | |
165 | 49 | FfmpegCommand.prototype.withAudioChannels = function(audiochannels) { |
166 | 13 | this.options.audio.channels = audiochannels; |
167 | 13 | return this; |
168 | }; | |
169 | 49 | FfmpegCommand.prototype.withAudioFrequency = function(frequency) { |
170 | 9 | this.options.audio.frequency = frequency; |
171 | 9 | return this; |
172 | }; | |
173 | 49 | FfmpegCommand.prototype.withAudioQuality = function(quality) { |
174 | 1 | this.options.audio.quality = parseInt(quality, 10); |
175 | 1 | return this; |
176 | }; | |
177 | 49 | FfmpegCommand.prototype.setStartTime = function(timestamp) { |
178 | 1 | this.options.starttime = timestamp; |
179 | 1 | return this; |
180 | }; | |
181 | 49 | FfmpegCommand.prototype.setDuration = function(duration) { |
182 | 1 | this.options.duration = duration; |
183 | 1 | return this; |
184 | }; | |
185 | 49 | FfmpegCommand.prototype.addInput = function(inputFile) { |
186 | 1 | this.options.otherInputs.push(inputFile); |
187 | 1 | return this; |
188 | }; | |
189 | 49 | FfmpegCommand.prototype.addOptions = function(optionArray) { |
190 | 4 | if (typeof optionArray.length !== undefined) { |
191 | 4 | var self = this; |
192 | 4 | optionArray.forEach(function(el) { |
193 | 41 | if (el.indexOf(' ') > 0) { |
194 | 19 | var values = el.split(' '); |
195 | 19 | self.options.additional.push(values[0], values[1]); |
196 | } else { | |
197 | 22 | self.options.additional.push(el); |
198 | } | |
199 | }); | |
200 | } | |
201 | 4 | return this; |
202 | }; | |
203 | 49 | FfmpegCommand.prototype.addOption = function(option, value) { |
204 | 1 | this.options.additional.push(option, value); |
205 | 1 | return this; |
206 | }; | |
207 | 49 | FfmpegCommand.prototype.mergeAdd = function(path){ |
208 | 0 | this.options.mergeList.push(path) |
209 | 0 | return this; |
210 | }; | |
211 | 49 | FfmpegCommand.prototype.toFormat = function(format) { |
212 | 12 | this.options.format = format; |
213 | ||
214 | // some muxers require the output stream to be seekable, disable streaming for those formats | |
215 | 12 | if (this.options.format === 'mp4') { |
216 | 1 | this.options._isStreamable = false; |
217 | } | |
218 | 12 | return this; |
219 | }; | |
220 | 49 | FfmpegCommand.prototype.updateFlvMetadata = function() { |
221 | 8 | this.options._updateFlvMetadata = true; |
222 | 8 | return this; |
223 | }; | |
224 | 49 | FfmpegCommand.prototype.renice = function(level) { |
225 | 1 | if (!level) { |
226 | // use 0 as default nice level (os default) | |
227 | 0 | level = 0; |
228 | } | |
229 | ||
230 | // make sure niceness is within allowed boundaries | |
231 | 1 | if (level > 20 || level < -20) { |
232 | 1 | this.options.logger.warn('niceness ' + level + ' is not valid, consider a value between -20 and +20 (whereas -20 is the highest priority)'); |
233 | 1 | level = 0; |
234 | } | |
235 | 1 | this.options._nice.level = level; |
236 | 1 | return this; |
237 | }; | |
238 | 49 | FfmpegCommand.prototype.onCodecData = function(callback) { |
239 | 1 | this.options.onCodecData = callback; |
240 | 1 | return this; |
241 | }; | |
242 | 49 | FfmpegCommand.prototype.onProgress = function(callback) { |
243 | 1 | this.options.onProgress = callback; |
244 | 1 | return this; |
245 | }; | |
246 | ||
247 | // private methods | |
248 | 49 | FfmpegCommand.prototype._prepare = function(callback) { |
249 | 47 | var calcDimensions = false, calcPadding = false; |
250 | ||
251 | // check for allowed sizestring formats and handle them accordingly | |
252 | 47 | var fixedWidth = /([0-9]+)x\?/.exec(this.options.video.size); |
253 | 47 | var fixedHeight = /\?x([0-9]+)/.exec(this.options.video.size); |
254 | 47 | var percentRatio = /\b([0-9]{1,2})%/.exec(this.options.video.size); |
255 | ||
256 | 47 | if (!fixedWidth && !fixedHeight && !percentRatio) { |
257 | // check for invalid size string | |
258 | 29 | var defaultSizestring = /([0-9]+)x([0-9]+)/.exec(this.options.video.size); |
259 | 29 | if (this.options.video.size && !defaultSizestring) { |
260 | 1 | callback(new Error('could not parse size string, aborting execution')); |
261 | 1 | return; |
262 | } else { | |
263 | // get width and height as integers (used for padding calculation) | |
264 | 28 | if (defaultSizestring) { |
265 | 3 | this.options.video.width = parseInt(defaultSizestring[1], 10); |
266 | 3 | this.options.video.height = parseInt(defaultSizestring[2], 10); |
267 | } | |
268 | 28 | calcDimensions = false; |
269 | } | |
270 | } else { | |
271 | 18 | calcDimensions = true; |
272 | } | |
273 | ||
274 | // check if we have to check aspect ratio for changes and auto-pad the output | |
275 | 46 | if (this.options._applyAutopad) { |
276 | 5 | calcPadding = true; |
277 | } | |
278 | ||
279 | 46 | var self = this; |
280 | 46 | this.getMetadata(this.options.inputfile, function(meta, err) { |
281 | 46 | if (calcDimensions || calcPadding) { |
282 | 21 | var dimErr, padErr; |
283 | // calculate dimensions | |
284 | 21 | if (calcDimensions) { |
285 | 18 | dimErr = self._calculateDimensions(meta); |
286 | } | |
287 | ||
288 | // calculate padding | |
289 | 21 | if (calcPadding) { |
290 | 5 | padErr = self._calculatePadding(meta); |
291 | } | |
292 | ||
293 | 21 | if (dimErr || padErr) { |
294 | 0 | callback(new Error('error while preparing: dimension -> ' + dimErr + ' padding -> ' + padErr)); |
295 | } else { | |
296 | 21 | callback(undefined, meta); |
297 | } | |
298 | } else { | |
299 | 25 | callback(undefined, meta); |
300 | } | |
301 | }); | |
302 | }; | |
303 | } | |
304 | ||
305 | // add module methods | |
306 | 1 | require('./extensions').apply(FfmpegCommand.prototype); |
307 | 1 | require('./metadata').apply(FfmpegCommand.prototype); |
308 | 1 | require('./processor').apply(FfmpegCommand.prototype); |
309 | 1 | require('./calculate').apply(FfmpegCommand.prototype); |
310 | 1 | require('./debug').apply(FfmpegCommand.prototype); |
311 | ||
312 | // module exports | |
313 | 1 | exports = module.exports = function(args) { |
314 | 49 | return new FfmpegCommand(args); |
315 | }; | |
316 | ||
317 | // export meta data discovery | |
318 | 1 | exports.Metadata = require('./metadata'); |
319 | 1 | exports.Calculate = require('./calculate'); |
320 | ||
321 | 1 | exports.CONSTANT_BITRATE = 1; |
322 | 1 | exports.VARIABLE_BITRATE = 2; |
Line | Hits | Source |
---|---|---|
1 | 1 | var exec = require('child_process').exec |
2 | , os = require('os').platform(); | |
3 | ||
4 | 1 | exports = module.exports = function Metadata(inputfile) { |
5 | 1 | this.escapedPath = function(path, enclose) { |
6 | 60 | if(/http/.exec(path)) { |
7 | 0 | path = path.replace(' ', '%20'); |
8 | } else { | |
9 | 60 | if (os.match(/win(32|64)/)) { |
10 | // on windows, we have to fix up the filename | |
11 | 0 | var parts = path.split(/\\/gi); |
12 | 0 | var fName = parts[parts.length - 1]; |
13 | 0 | parts[parts.length - 1] = fName.replace(/[\s\\:"'*?<>|\/]+/mig, '-'); |
14 | 0 | path = parts.join('\\'); |
15 | 0 | if (enclose && path[0] != '"' && path[path.length-1] != '"') |
16 | 0 | path = '"' + path + '"' |
17 | } else { | |
18 | 60 | if (enclose && path[0] != '"' && path[path.length-1] != '"') |
19 | 8 | path = '"' + path + '"'; |
20 | } | |
21 | } | |
22 | 60 | return path; |
23 | }; | |
24 | ||
25 | 1 | this.inputfile = inputfile; |
26 | ||
27 | 1 | this.setFfmpegPath = function(path) { |
28 | 0 | this.ffmpegPath = path; |
29 | }; | |
30 | ||
31 | // for internal use | |
32 | 1 | this.getMetadata = function(inputfile, callback) { |
33 | 46 | this.inputfile = inputfile; |
34 | 46 | this._loadDataInternal(callback); |
35 | }; | |
36 | ||
37 | // for external use | |
38 | 1 | this.get = function(callback) { |
39 | // import extensions for external call | |
40 | 0 | require('./extensions').apply(Metadata.prototype); |
41 | 0 | this._loadDataInternal(callback); |
42 | }; | |
43 | ||
44 | 1 | this._loadDataInternal = function(callback) { |
45 | 46 | var inputfile = this.escapedPath(this.inputfile); |
46 | 46 | var self = this; |
47 | 46 | exec(this.ffmpegPath + ' -i ' + inputfile, function(err, stdout, stderr) { |
48 | // parse data from stderr | |
49 | ||
50 | 46 | var none = [] |
51 | , aspect = /DAR ([0-9\:]+)/.exec(stderr) || none | |
52 | , pixel = /[SP]AR ([0-9\:]+)/.exec(stderr) || none | |
53 | , video_bitrate = /bitrate: ([0-9]+) kb\/s/.exec(stderr) || none | |
54 | , fps = /([0-9\.]+) (fps|tb\(r\))/.exec(stderr) || none | |
55 | , container = /Input #0, ([a-zA-Z0-9]+),/.exec(stderr) || none | |
56 | , title = /(INAM|title)\s+:\s(.+)/i.exec(stderr) || none | |
57 | , artist = /artist\s+:\s(.+)/i.exec(stderr) || none | |
58 | , album = /album\s+:\s(.+)/i.exec(stderr) || none | |
59 | , track = /track\s+:\s(.+)/i.exec(stderr) || none | |
60 | , date = /date\s+:\s(.+)/i.exec(stderr) || none | |
61 | , video_stream = /Stream #([0-9\.]+)([a-z0-9\(\)\[\]]*)[:] Video/.exec(stderr) || none | |
62 | , video_codec = /Video: ([\w]+)/.exec(stderr) || none | |
63 | , duration = /Duration: (([0-9]+):([0-9]{2}):([0-9]{2}).([0-9]+))/.exec(stderr) || none | |
64 | , resolution = /(([0-9]{2,5})x([0-9]{2,5}))/.exec(stderr) || none | |
65 | , audio_bitrate = /Audio:(.)*, ([0-9]+) kb\/s/.exec(stderr) || none | |
66 | , sample_rate = /([0-9]+) Hz/i.exec(stderr) || none | |
67 | , audio_codec = /Audio: ([\w]+)/.exec(stderr) || none | |
68 | , channels = /Audio: [\w]+, [0-9]+ Hz, ([a-z0-9:]+)[a-z0-9\/,]*/.exec(stderr) || none | |
69 | , audio_stream = /Stream #([0-9\.]+)([a-z0-9\(\)\[\]]*)[:] Audio/.exec(stderr) || none | |
70 | , is_synched = (/start: 0.000000/.exec(stderr) !== null) | |
71 | , rotate = /rotate[\s]+:[\s]([\d]{2,3})/.exec(stderr) || none | |
72 | , getVersion = /ffmpeg version (?:(\d+)\.)?(?:(\d+)\.)?(\*|\d+)/i.exec(stderr) | |
73 | , major_brand = /major_brand\s+:\s([^\s]+)/.exec(stderr) || none | |
74 | , ffmpegVersion = 0; | |
75 | ||
76 | 46 | if (getVersion) { |
77 | 46 | ffmpegVersion = [ |
78 | getVersion[1]>=0 ? getVersion[1] : null, | |
79 | getVersion[2]>=0 ? getVersion[2] : null, | |
80 | getVersion[3]>=0 ? getVersion[3] : null | |
81 | ].filter(function(val) { | |
82 | 138 | return val !== null; |
83 | }).join('.'); | |
84 | } | |
85 | ||
86 | // build return object | |
87 | 46 | var _ref |
88 | , ret = { | |
89 | ffmpegversion: ffmpegVersion | |
90 | , title: title[2] || '' | |
91 | , artist: artist[1] || '' | |
92 | , album: album[1] || '' | |
93 | , track: track[1] || '' | |
94 | , date: date[1] || '' | |
95 | , durationraw: duration[1] || '' | |
96 | , durationsec: duration[1] ? self.ffmpegTimemarkToSeconds(duration[1]) : 0 | |
97 | , synched: is_synched | |
98 | , major_brand: major_brand[1] | |
99 | , video: { | |
100 | container: container[1] || '' | |
101 | , bitrate: (video_bitrate.length > 1) ? parseInt(video_bitrate[1], 10) : 0 | |
102 | , codec: video_codec[1] || '' | |
103 | , resolution: { | |
104 | w: resolution.length > 2 ? parseInt(resolution[2], 10) : 0 | |
105 | , h: resolution.length > 3 ? parseInt(resolution[3], 10) : 0 | |
106 | } | |
107 | , resolutionSquare: {} | |
108 | , rotate: rotate.length > 1 ? parseInt(rotate[1], 10) : 0 | |
109 | , fps: fps.length > 1 ? parseFloat(fps[1]) : 0.0 | |
110 | , stream: video_stream.length > 1 ? parseFloat(video_stream[1]) : 0.0 | |
111 | } | |
112 | , audio: { | |
113 | codec: audio_codec[1] || '' | |
114 | , bitrate: parseInt((_ref = audio_bitrate[audio_bitrate.length - 1]) != null ? _ref : 0, 10) | |
115 | , sample_rate: sample_rate.length > 1 ? parseInt(sample_rate[1], 10) : 0 | |
116 | , stream: audio_stream.length > 1 ? parseFloat(audio_stream[1]) : 0.0 | |
117 | } | |
118 | }; | |
119 | ||
120 | 46 | if (channels.length > 0) { |
121 | 0 | ret.audio.channels = {stereo:2, mono:1}[channels[1]] || 0; |
122 | } | |
123 | ||
124 | // save aspect ratio for auto-padding | |
125 | 46 | if (aspect.length > 0) { |
126 | 45 | ret.video.aspectString = aspect[1]; |
127 | 45 | var n = aspect[1].split(":"); |
128 | 45 | ret.video.aspect = parseFloat((parseInt(n[0], 10) / parseInt(n[1], 10))); |
129 | } else { | |
130 | 1 | if(ret.video.resolution.w !== 0) { |
131 | 0 | var f = self.gcd(ret.video.resolution.w, ret.video.resolution.h); |
132 | 0 | ret.video.aspectString = ret.video.resolution.w/f + ':' + ret.video.resolution.h/f; |
133 | 0 | ret.video.aspect = parseFloat((ret.video.resolution.w / ret.video.resolution.h)); |
134 | } else { | |
135 | 1 | ret.video.aspect = 0.0; |
136 | } | |
137 | } | |
138 | ||
139 | // save pixel ratio for output size calculation | |
140 | 46 | if (pixel.length > 0) { |
141 | 45 | ret.video.pixelString = pixel[1]; |
142 | 45 | var n = pixel[1].split(":"); |
143 | 45 | ret.video.pixel = parseFloat((parseInt(n[0], 10) / parseInt(n[1], 10))); |
144 | } else { | |
145 | 1 | if (ret.video.resolution.w !== 0) { |
146 | 0 | var f = self.gcd(ret.video.resolution.w, ret.video.resolution.h); |
147 | 0 | ret.video.pixelString = '1:1'; |
148 | 0 | ret.video.pixel = 1; |
149 | } else { | |
150 | 1 | ret.video.pixel = 0.0; |
151 | } | |
152 | } | |
153 | ||
154 | // correct video.resolution when pixel aspectratio is not 1 | |
155 | 46 | if (ret.video.pixel !== 1 || ret.video.pixel !== 0) { |
156 | 46 | if( ret.video.pixel > 1 ) { |
157 | 0 | ret.video.resolutionSquare.w = parseInt(ret.video.resolution.w * ret.video.pixel, 10); |
158 | 0 | ret.video.resolutionSquare.h = ret.video.resolution.h; |
159 | } else { | |
160 | 46 | ret.video.resolutionSquare.w = ret.video.resolution.w; |
161 | 46 | ret.video.resolutionSquare.h = parseInt(ret.video.resolution.h / ret.video.pixel, 10); |
162 | } | |
163 | } | |
164 | ||
165 | 46 | callback(ret); |
166 | }); | |
167 | }; | |
168 | }; |
Line | Hits | Source |
---|---|---|
1 | 1 | exports.load = function(ffmpeg) { |
2 | 1 | ffmpeg |
3 | .toFormat('avi') | |
4 | .withVideoBitrate('1024k') | |
5 | .withVideoCodec('mpeg4') | |
6 | .withSize('720x?') | |
7 | .withAudioBitrate('128k') | |
8 | .withAudioChannels(2) | |
9 | .withAudioCodec('libmp3lame') | |
10 | .addOptions([ '-vtag DIVX' ]); | |
11 | 1 | return ffmpeg; |
12 | }; |
Line | Hits | Source |
---|---|---|
1 | 1 | exports.load = function(ffmpeg) { |
2 | 8 | ffmpeg |
3 | .toFormat('flv') | |
4 | .updateFlvMetadata() | |
5 | .withSize('320x?') | |
6 | .withVideoBitrate('512k') | |
7 | .withVideoCodec('libx264') | |
8 | .withFps(24) | |
9 | .withAudioBitrate('96k') | |
10 | .withAudioCodec('libfaac') | |
11 | .withAudioFrequency(22050) | |
12 | .withAudioChannels(2); | |
13 | 8 | return ffmpeg; |
14 | }; |
Line | Hits | Source |
---|---|---|
1 | 1 | exports.load = function(ffmpeg) { |
2 | 2 | ffmpeg |
3 | .toFormat('m4v') | |
4 | .withVideoBitrate('512k') | |
5 | .withVideoCodec('libx264') | |
6 | .withSize('320x176') | |
7 | .withAudioBitrate('128k') | |
8 | .withAudioCodec('libfaac') | |
9 | .withAudioChannels(1) | |
10 | .addOptions(['-flags', '+loop', '-cmp', '+chroma', '-partitions','+parti4x4+partp8x8+partb8x8', '-flags2', | |
11 | '+mixed_refs', '-me_method umh', '-subq 5', '-bufsize 2M', '-rc_eq \'blurCplx^(1-qComp)\'', | |
12 | '-qcomp 0.6', '-qmin 10', '-qmax 51', '-qdiff 4', '-level 13' ]); | |
13 | 2 | return ffmpeg; |
14 | }; |
Line | Hits | Source |
---|---|---|
1 | 1 | var fs = require('fs'), |
2 | path = require('path'), | |
3 | async = require('../support/async.min.js'), | |
4 | os = require('os').platform(), | |
5 | exec = require('child_process').exec, | |
6 | spawn = require('child_process').spawn, | |
7 | Registry = require('./registry'), | |
8 | ||
9 | exports = module.exports = function Processor(command) { | |
10 | // constant for timeout checks | |
11 | 1 | this.E_PROCESSTIMEOUT = -99; |
12 | 1 | this._codecDataAlreadySent = false; |
13 | ||
14 | 1 | this.saveToFile = function(targetfile, callback) { |
15 | ||
16 | 6 | callback = callback || function() {}; |
17 | ||
18 | 6 | this.options.outputfile = targetfile; |
19 | ||
20 | 6 | var self = this; |
21 | 6 | var options = this.options; |
22 | ||
23 | // parse options to command | |
24 | 6 | this._prepare(function(err, meta) { |
25 | ||
26 | 6 | if (err) { |
27 | 0 | return callback(null, null, err); |
28 | } | |
29 | ||
30 | 6 | var args = self.buildFfmpegArgs(false, meta); |
31 | ||
32 | 6 | if (!args instanceof Array) { |
33 | 0 | return callback (null, null, args); |
34 | } | |
35 | ||
36 | // start conversion of file using spawn | |
37 | 6 | var ffmpegProc = self._spawnProcess(args); |
38 | 6 | if (options.inputstream) { |
39 | // pump input stream to stdin | |
40 | 1 | options.inputstream.resume(); |
41 | 1 | options.inputstream.pipe(ffmpegProc.stdin); |
42 | } | |
43 | ||
44 | //handle timeout if set | |
45 | 6 | var processTimer; |
46 | 6 | if (options.timeout) { |
47 | 6 | processTimer = setTimeout(function() { |
48 | 1 | ffmpegProc.removeAllListeners('exit'); |
49 | 1 | ffmpegProc.kill('SIGKILL'); |
50 | 1 | options.logger.warn('process ran into a timeout (' + self.options.timeout + 's)'); |
51 | 1 | callback(self.E_PROCESSTIMEOUT, 'timeout'); |
52 | }, options.timeout * 1000); | |
53 | } | |
54 | ||
55 | 6 | var stdout = ''; |
56 | 6 | var stderr = ''; |
57 | 6 | ffmpegProc.on('exit', function(code) { |
58 | 5 | if (processTimer) { |
59 | 5 | clearTimeout(processTimer); |
60 | } | |
61 | // check if we have to run flvtool2 to update flash video meta data | |
62 | 5 | if (self.options._updateFlvMetadata === true) { |
63 | // make sure we didn't try to determine this capability before | |
64 | 5 | if (!Registry.instance.get('capabilityFlvTool2')) { |
65 | // check if flvtool2 is installed | |
66 | 5 | exec('which flvtool2', function(whichErr, whichStdOut, whichStdErr) { |
67 | 5 | if (whichStdOut !== '') { |
68 | 0 | Registry.instance.set('capabilityFlvTool2', true); |
69 | // update metadata in flash video | |
70 | 0 | exec('flvtool2 -U ' + self.options.outputfile, function(flvtoolErr, flvtoolStdout, flvtoolStderr) { |
71 | 0 | callback(stdout, stderr, null); |
72 | }); | |
73 | } else { | |
74 | // flvtool2 is not installed, skip further checks | |
75 | 5 | Registry.instance.set('capabilityFlvTool2', false); |
76 | 5 | callback(stdout, stderr, null); |
77 | } | |
78 | }); | |
79 | 0 | } else if (!Registry.instance.get('capabilityFlvTool2')) { |
80 | // flvtool2 capability was checked before, execute update | |
81 | 0 | exec('flvtool2 -U ' + self.options.outputfile, function(flvtoolErr, flvtoolStdout, flvtoolStderr) { |
82 | 0 | callback(stdout, stderr, null); |
83 | }); | |
84 | } else { | |
85 | // flvtool2 not installed, skip update | |
86 | 0 | callback(stdout, stderr, null); |
87 | } | |
88 | } else { | |
89 | 0 | callback(stdout, stderr, null); |
90 | } | |
91 | }); | |
92 | 6 | ffmpegProc.stdout.on('data', function (data) { |
93 | 0 | stdout += data; |
94 | }); | |
95 | ||
96 | 6 | ffmpegProc.stderr.on('data', function (data) { |
97 | 119 | stderr += data; |
98 | 119 | if (options.onCodecData) { |
99 | 11 | self._checkStdErrForCodec(stderr); |
100 | } | |
101 | 119 | if (options.onProgress) { |
102 | 17 | self._getProgressFromStdErr(stderr, meta.durationsec); |
103 | } | |
104 | }); | |
105 | }); | |
106 | }; | |
107 | ||
108 | 1 | this.mergeToFile = function(targetfile,callback){ |
109 | 0 | this.options.outputfile = targetfile; |
110 | 0 | var self = this; |
111 | 0 | var options = this.options; |
112 | ||
113 | 0 | var getExtension = function(filename) { |
114 | 0 | var ext = path.extname(filename||'').split('.'); |
115 | 0 | return ext[ext.length - 1]; |
116 | }; | |
117 | ||
118 | // creates intermediate copies of each video. | |
119 | 0 | var makeIntermediateFile = function(_mergeSource,_callback){ |
120 | 0 | var fname = _mergeSource+".temp.mpg"; |
121 | 0 | var command = [ |
122 | self.ffmpegPath, | |
123 | [ | |
124 | '-i', _mergeSource, | |
125 | '-qscale:v',1, | |
126 | fname | |
127 | ].join(' ') | |
128 | ]; | |
129 | 0 | exec(command.join(' '),function(err, stdout, stderr) { |
130 | 0 | if(err)throw err; |
131 | 0 | _callback(fname); |
132 | }); | |
133 | }; | |
134 | ||
135 | // concat all created intermediate copies | |
136 | 0 | var concatIntermediates = function(target,intermediatesList,_callback){ |
137 | 0 | var fname = target+".temp.merged.mpg"; |
138 | ||
139 | // unescape paths | |
140 | 0 | for(var i=0; i<intermediatesList.length; i++){ |
141 | 0 | intermediatesList[i] = unescapePath(intermediatesList[i]); |
142 | } | |
143 | ||
144 | 0 | var command = [ |
145 | self.ffmpegPath, | |
146 | [ | |
147 | '-loglevel','panic', //Generetes too much muxing warnings and fills default buffer of exec. This is to ignore them. | |
148 | '-i', 'concat:"'+intermediatesList.join("|")+'"', | |
149 | '-c',"copy", | |
150 | fname | |
151 | ].join(' ') | |
152 | ]; | |
153 | 0 | exec(command.join(' '), function(err, stdout, stderr) { |
154 | 0 | if(err)throw err; |
155 | 0 | _callback(fname); |
156 | }); | |
157 | }; | |
158 | ||
159 | 0 | var quantizeConcat = function(concatResult,numFiles,_callback){ |
160 | 0 | var command = [ |
161 | self.ffmpegPath, | |
162 | [ | |
163 | '-i', concatResult, | |
164 | '-qscale:v',numFiles, | |
165 | targetfile | |
166 | ].join(' ') | |
167 | ]; | |
168 | 0 | exec(command.join(' '), function(err, stdout, stderr) { |
169 | 0 | if(err)throw err; |
170 | 0 | _callback(); |
171 | }); | |
172 | } | |
173 | ||
174 | 0 | var deleteIntermediateFiles = function(intermediates){ |
175 | 0 | for(var i=0 ; i<intermediates.length ; i++){ |
176 | 0 | fs.unlinkSync( unescapePath(intermediates[i])); |
177 | } | |
178 | } | |
179 | ||
180 | 0 | var unescapePath = function(path){ |
181 | 0 | var f = path+""; |
182 | 0 | if(f.indexOf('"')==0)f = f.substring(1); |
183 | 0 | if(f.lastIndexOf('"')== f.length-1)f = f.substring(0, f.length-1); |
184 | 0 | return f; |
185 | } | |
186 | ||
187 | 0 | if(options.mergeList.length<=0)throw new Error("No file added to be merged"); |
188 | 0 | var mergeList = options.mergeList; |
189 | 0 | mergeList.unshift(options.inputfile) |
190 | ||
191 | 0 | var intermediateFiles = []; |
192 | ||
193 | 0 | async.whilst(function(){ |
194 | 0 | return (mergeList.length != 0); |
195 | },function(callback){ | |
196 | 0 | makeIntermediateFile(mergeList.shift(),function(createdIntermediateFile){ |
197 | 0 | if(!createdIntermediateFile)throw new Error("Invalid intermediate file"); |
198 | 0 | intermediateFiles.push(createdIntermediateFile); |
199 | 0 | callback(); |
200 | }) | |
201 | },function(err){ | |
202 | 0 | if(err)throw err; |
203 | 0 | concatIntermediates(targetfile,intermediateFiles,function(concatResult){ |
204 | 0 | if(!concatResult)throw new Error("Invalid concat result file"); |
205 | 0 | quantizeConcat(concatResult,intermediateFiles.length,function(){ |
206 | 0 | intermediateFiles.push(concatResult); // add concatResult to intermediates list so it can be deleted too. |
207 | 0 | deleteIntermediateFiles(intermediateFiles); |
208 | 0 | callback(); // completed; |
209 | }); | |
210 | }); | |
211 | }); | |
212 | ||
213 | } | |
214 | ||
215 | 1 | this.writeToStream = function(stream, callback) { |
216 | ||
217 | 2 | callback = callback || function(){}; |
218 | ||
219 | 2 | if (!this.options._isStreamable) { |
220 | 0 | this.options.logger.error('selected output format is not streamable'); |
221 | 0 | return callback(null, new Error('selected output format is not streamable')); |
222 | } | |
223 | ||
224 | 2 | var self = this; |
225 | 2 | var options = this.options; |
226 | ||
227 | // parse options to command | |
228 | 2 | this._prepare(function(err, meta) { |
229 | 2 | if (err) { |
230 | 0 | return callback(null, err); |
231 | } | |
232 | ||
233 | 2 | var args = self.buildFfmpegArgs(true, meta); |
234 | ||
235 | 2 | if (!args instanceof Array) { |
236 | 0 | return callback(null, args); |
237 | } | |
238 | // write data to stdout | |
239 | 2 | args.push('pipe:1'); |
240 | ||
241 | // start conversion of file using spawn | |
242 | 2 | var ffmpegProc = self._spawnProcess(args); |
243 | ||
244 | 2 | if (options.inputstream) { |
245 | // pump input stream to stdin | |
246 | 1 | options.inputstream.resume(); |
247 | 1 | options.inputstream.pipe(ffmpegProc.stdin); |
248 | } | |
249 | ||
250 | //handle timeout if set | |
251 | 2 | var processTimer; |
252 | 2 | if (options.timeout) { |
253 | 2 | processTimer = setTimeout(function() { |
254 | 0 | ffmpegProc.removeAllListeners('exit'); |
255 | 0 | ffmpegProc.kill('SIGKILL'); |
256 | 0 | options.logger.warn('process ran into a timeout (' + options.timeout + 's)'); |
257 | 0 | callback(self.E_PROCESSTIMEOUT, 'timeout'); |
258 | }, options.timeout * 1000); | |
259 | } | |
260 | ||
261 | 2 | var stderr = ''; |
262 | ||
263 | 2 | ffmpegProc.stderr.on('data', function(data) { |
264 | 43 | stderr += data; |
265 | 43 | if (options.onCodecData) { |
266 | 0 | self._checkStdErrForCodec(stderr); |
267 | } | |
268 | 43 | if (options.onProgress) { |
269 | 0 | self._getProgressFromStdErr(stderr, meta.durationsec); |
270 | } | |
271 | }); | |
272 | ||
273 | 2 | ffmpegProc.stdout.on('data', function(chunk) { |
274 | 23 | stream.write(chunk); |
275 | }); | |
276 | ||
277 | 2 | ffmpegProc.on('exit', function(code, signal) { |
278 | 2 | if (processTimer) { |
279 | 2 | clearTimeout(processTimer); |
280 | } | |
281 | // close file descriptor on outstream | |
282 | 2 | if(/^[a-z]+:\/\//.test(options.inputfile)) { |
283 | 0 | return callback(code, stderr); |
284 | } | |
285 | ||
286 | 2 | var cb_ = function() { |
287 | 2 | if (!options.inputstream || !options.inputstream.fd) { |
288 | 1 | return callback(code, stderr); |
289 | } | |
290 | 1 | if (!options.inputstream.fd) { |
291 | 0 | options.inputstream.destroy(); |
292 | 0 | return callback(code, stderr); |
293 | } | |
294 | 1 | fs.close(options.inputstream.fd, function() { |
295 | 1 | callback(code, stderr); |
296 | }); | |
297 | }; | |
298 | ||
299 | 2 | if (stream.fd) { |
300 | 2 | return fs.close(stream.fd, cb_); |
301 | } | |
302 | 0 | if (stream.end) { |
303 | 0 | stream.end(); |
304 | } else { | |
305 | 0 | callback(code, "stream will not be closed"); |
306 | } | |
307 | 0 | cb_(); |
308 | }); | |
309 | ||
310 | 2 | stream.on("close", function() |
311 | { | |
312 | 0 | options.logger.debug("Output stream closed, killing ffmpgeg process"); |
313 | 0 | ffmpegProc.kill(); |
314 | }); | |
315 | }); | |
316 | }; | |
317 | ||
318 | 1 | this.takeScreenshots = function(config, folder, callback) { |
319 | ||
320 | 3 | callback = callback || function(){}; |
321 | ||
322 | 3 | function _zeroPad(number, len) { |
323 | 4 | len = len-String(number).length+2; |
324 | 4 | return new Array(len<0?0:len).join('0')+number; |
325 | } | |
326 | ||
327 | 3 | function _renderOutputName(j, offset) { |
328 | 4 | var result = filename; |
329 | 4 | if(/%0*i/.test(result)) { |
330 | 4 | var numlen = String(result.match(/%(0*)i/)[1]).length; |
331 | 4 | result = result.replace(/%0*i/, _zeroPad(j, numlen)); |
332 | } | |
333 | 4 | result = result.replace('%s', offset); |
334 | 4 | result = result.replace('%w', self.options.video.width); |
335 | 4 | result = result.replace('%h', self.options.video.height); |
336 | 4 | result = result.replace('%r', self.options.video.width+'x'+self.options.video.height); |
337 | 4 | result = result.replace('%f', self.options.inputfile); |
338 | 4 | result = result.replace('%b', self.options.inputfile.substr(0,self.options.inputfile.lastIndexOf('.'))); |
339 | 4 | return result; |
340 | } | |
341 | ||
342 | 3 | function _screenShotInternal(callback) { |
343 | ||
344 | // get correct dimensions | |
345 | 3 | self._prepare(function(err, meta) { |
346 | 3 | if(err) { |
347 | 1 | return callback(err); |
348 | } | |
349 | 2 | if (!meta.durationsec) { |
350 | 0 | var errString = 'meta data contains no duration, aborting screenshot creation'; |
351 | 0 | self.options.logger.warn(errString); |
352 | 0 | return callback(new Error(errString)); |
353 | } | |
354 | ||
355 | // check if all timemarks are inside duration | |
356 | 2 | if (Array.isArray(timemarks)) { |
357 | 2 | for (var i = 0; i < timemarks.length; i++) { |
358 | /* convert percentage to seconds */ | |
359 | 4 | if( timemarks[i].indexOf('%') > 0 ) { |
360 | 0 | timemarks[i] = (parseInt(timemarks[i], 10) / 100) * meta.durationsec; |
361 | } | |
362 | 4 | if (parseInt(timemarks[i], 10) > meta.durationsec) { |
363 | // remove timemark from array | |
364 | 0 | timemarks.splice(i, 1); |
365 | 0 | --i; |
366 | } | |
367 | } | |
368 | // if there are no more timemarks around, add one at end of the file | |
369 | 2 | if (timemarks.length === 0) { |
370 | 0 | timemarks[0] = (meta.durationsec * 0.9); |
371 | } | |
372 | } | |
373 | // get positions for screenshots (using duration of file minus 10% to remove fade-in/fade-out) | |
374 | 2 | var secondOffset = (meta.durationsec * 0.9) / screenshotcount; |
375 | 2 | var donecount = 0; |
376 | 2 | var series = []; |
377 | ||
378 | // reset iterator | |
379 | 2 | var j = 1; |
380 | ||
381 | 2 | var filenames = []; |
382 | ||
383 | // use async helper function to generate all screenshots and | |
384 | // fire callback just once after work is done | |
385 | 2 | async.until( |
386 | function() { | |
387 | 6 | return j > screenshotcount; |
388 | }, | |
389 | function(taskcallback) { | |
390 | 4 | var offset; |
391 | 4 | if (Array.isArray(timemarks)) { |
392 | // get timemark for current iteration | |
393 | 4 | offset = timemarks[(j - 1)]; |
394 | } else { | |
395 | 0 | offset = secondOffset * j; |
396 | } | |
397 | 4 | var fname = _renderOutputName(j, offset) + '.jpg'; |
398 | 4 | var target = self.escapedPath(path.join(folder, fname), true); |
399 | 4 | var input = self.escapedPath(self.options.inputfile, true); |
400 | ||
401 | // build screenshot command | |
402 | 4 | var command = [ |
403 | self.ffmpegPath, | |
404 | [ | |
405 | '-ss', Math.floor(offset * 100) / 100, | |
406 | '-i', input, | |
407 | '-vcodec', 'mjpeg', | |
408 | '-vframes', '1', | |
409 | '-an', | |
410 | '-f', 'rawvideo', | |
411 | '-s', self.options.video.size, | |
412 | '-y', target | |
413 | ].join(' ') | |
414 | ]; | |
415 | ||
416 | 4 | j++; |
417 | ||
418 | // only set niceness if running on a non-windows platform | |
419 | 4 | if (self.options.hasOwnProperty('_nice.level') && !os.match(/win(32|64)/)) { |
420 | // execute ffmpeg through nice | |
421 | 0 | command.unshift('nice -n', self.options._nice.level||0); |
422 | } | |
423 | ||
424 | 4 | exec(command.join(' '), taskcallback); |
425 | 4 | filenames.push(fname); |
426 | }, | |
427 | function(err) { | |
428 | 2 | callback(err, filenames); |
429 | } | |
430 | ); | |
431 | }); | |
432 | } | |
433 | ||
434 | 3 | var timemarks, screenshotcount, filename; |
435 | 3 | if (typeof config === 'object') { |
436 | // use json object as config | |
437 | 2 | if (config.count) { |
438 | 2 | screenshotcount = config.count; |
439 | } | |
440 | 2 | if (config.timemarks) { |
441 | 2 | timemarks = config.timemarks; |
442 | } | |
443 | } else { | |
444 | // assume screenshot count as parameter | |
445 | 1 | screenshotcount = config; |
446 | 1 | timemarks = null; |
447 | } | |
448 | 3 | if (!this.options.video.size) { |
449 | 0 | this.options.logger.warn("set size of thumbnails using 'withSize' method"); |
450 | 0 | callback(new Error("set size of thumbnails using 'withSize' method")); |
451 | } | |
452 | ||
453 | 3 | filename = config.filename || 'tn_%ss'; |
454 | 3 | if(!/%0*i/.test(filename) && Array.isArray(timemarks) && timemarks.length > 1 ) { |
455 | // if there are multiple timemarks but no %i in filename add one | |
456 | // so we won't overwrite the same thumbnail with each timemark | |
457 | 1 | filename += '_%i'; |
458 | } | |
459 | 3 | folder = folder || '.'; |
460 | ||
461 | 3 | var self = this; |
462 | ||
463 | // WORKAROUND: exists will be moved from path to fs with node v0.7 | |
464 | 3 | var check = fs.exists; |
465 | 3 | if (!check) { |
466 | 0 | check = path.exists; |
467 | } | |
468 | ||
469 | // check target folder | |
470 | 3 | check(folder, function(exists) { |
471 | 3 | if (!exists) { |
472 | 2 | fs.mkdir(folder, '0755', function(err) { |
473 | 2 | if (err !== null) { |
474 | 0 | callback(err); |
475 | } else { | |
476 | 2 | _screenShotInternal(callback); |
477 | } | |
478 | }); | |
479 | } else { | |
480 | 1 | _screenShotInternal(callback); |
481 | } | |
482 | }); | |
483 | }; | |
484 | ||
485 | 1 | this._getProgressFromStdErr = function(stderrString, totalDurationSec) { |
486 | // get last stderr line | |
487 | 17 | var lastLine = stderrString.split(/\r\n|\r|\n/g); |
488 | 17 | var ll = lastLine[lastLine.length - 2]; |
489 | 17 | var progress; |
490 | 17 | if (ll) { |
491 | 17 | progress = ll.split(/frame=([0-9\s]+)fps=([0-9\.\s]+)q=([0-9\.\s]+)(L?)size=([0-9\s]+)kB time=(([0-9]{2}):([0-9]{2}):([0-9]{2}).([0-9]{2})) bitrate=([0-9\.\s]+)kbits/ig); |
492 | } | |
493 | 17 | if (progress && progress.length > 10) { |
494 | // build progress report object | |
495 | 0 | var ret = { |
496 | frames: parseInt(progress[1], 10), | |
497 | currentFps: parseInt(progress[2], 10), | |
498 | currentKbps: parseFloat(progress[10]), | |
499 | targetSize: parseInt(progress[5], 10), | |
500 | timemark: progress[6] | |
501 | }; | |
502 | ||
503 | // calculate percent progress using duration | |
504 | 0 | if (totalDurationSec && totalDurationSec > 0) { |
505 | 0 | ret.percent = (this.ffmpegTimemarkToSeconds(ret.timemark) / totalDurationSec) * 100; |
506 | } | |
507 | ||
508 | 0 | this.options.onProgress(ret); |
509 | } | |
510 | }; | |
511 | ||
512 | 1 | this._checkStdErrForCodec = function(stderrString) { |
513 | 11 | var format= /Input #[0-9]+, ([^ ]+),/.exec(stderrString); |
514 | 11 | var dur = /Duration\: ([^,]+)/.exec(stderrString); |
515 | 11 | var audio = /Audio\: (.*)/.exec(stderrString); |
516 | 11 | var video = /Video\: (.*)/.exec(stderrString); |
517 | 11 | var codecObject = { format: '', audio: '', video: '', duration: '' }; |
518 | ||
519 | 11 | if (format && format.length > 1) { |
520 | 9 | codecObject.format = format[1]; |
521 | } | |
522 | ||
523 | 11 | if (dur && dur.length > 1) { |
524 | 9 | codecObject.duration = dur[1]; |
525 | } | |
526 | ||
527 | 11 | if (audio && audio.length > 1) { |
528 | 0 | audio = audio[1].split(', '); |
529 | 0 | codecObject.audio = audio[0]; |
530 | 0 | codecObject.audio_details = audio; |
531 | } | |
532 | 11 | if (video && video.length > 1) { |
533 | 8 | video = video[1].split(', '); |
534 | 8 | codecObject.video = video[0]; |
535 | 8 | codecObject.video_details = video; |
536 | } | |
537 | ||
538 | 11 | var codecInfoPassed = /Press (\[q\]|ctrl-c) to stop/.test(stderrString); |
539 | 11 | if (codecInfoPassed) { |
540 | 1 | this.options.onCodecData(codecObject); |
541 | 1 | this.options.onCodecData = null; |
542 | } | |
543 | }; | |
544 | ||
545 | 1 | this._spawnProcess = function(args, options) { |
546 | 8 | var retProc = spawn(this.ffmpegPath, args, options); |
547 | // only re-nice if running on a non-windows platform | |
548 | 8 | if (this.options.hasOwnProperty('_nice.level') && !os.match(/win(32|64)/)) { |
549 | 0 | var niceLevel = this.options._nice.level || 0; |
550 | 0 | if (niceLevel > 0) { |
551 | 0 | niceLevel = '+' + niceLevel; |
552 | } | |
553 | // renice the spawned process without waiting for callback | |
554 | 0 | var self = this; |
555 | 0 | var command = [ |
556 | 'renice -n', niceLevel, | |
557 | '-p', retProc.pid | |
558 | ].join(' '); | |
559 | ||
560 | 0 | exec(command, function(err, stderr, stdout) { |
561 | 0 | if (!err) { |
562 | 0 | self.options.logger.info('successfully reniced process ' + retProc.pid + ' to ' + niceLevel + ' niceness!'); |
563 | } | |
564 | }); | |
565 | } | |
566 | 8 | if (retProc.stderr) { |
567 | 8 | retProc.stderr.setEncoding('utf8'); |
568 | } | |
569 | 8 | return retProc; |
570 | }; | |
571 | ||
572 | 1 | this.buildFfmpegArgs = function(overrideOutputCheck, meta) { |
573 | 44 | var args = []; |
574 | ||
575 | // add startoffset and duration | |
576 | 44 | if (this.options.starttime) { |
577 | 1 | args.push('-ss', this.options.starttime); |
578 | } | |
579 | ||
580 | 44 | if (this.options.video.loop) { |
581 | 3 | args.push('-loop', 1); |
582 | } | |
583 | ||
584 | ||
585 | // add input file (if using fs mode) | |
586 | 44 | if (this.options.inputfile && !this.options.inputstream && !this.options.inputlive) { |
587 | // add input file fps | |
588 | 42 | if (this.options.video.fpsInput) { |
589 | 0 | args.push('-r', this.options.video.fpsInput); |
590 | } | |
591 | 42 | if (/^[a-z]+:\/\//.test(this.options.inputfile)) { |
592 | 0 | args.push('-i', this.options.inputfile.replace(' ', '%20')); |
593 | 42 | } else if (/%\d*d/.test(this.options.inputfile)) { // multi-file format - http://ffmpeg.org/ffmpeg.html#image2-1 |
594 | 1 | args.push('-i', this.options.inputfile.replace(' ', '\ ')); |
595 | } else { | |
596 | 41 | var fstats = fs.statSync(this.options.inputfile); |
597 | 41 | if (fstats.isFile()) { |
598 | // fix for spawn call with path containing spaces and quotes | |
599 | 41 | args.push('-i', this.options.inputfile.replace(/ /g, "\ ") |
600 | .replace(/'/g, "\'") | |
601 | .replace(/"/g, "\"")); | |
602 | } else { | |
603 | 0 | this.options.logger.error('input file is not readable'); |
604 | 0 | throw new Error('input file is not readable'); |
605 | } | |
606 | } | |
607 | // check for input stream | |
608 | 2 | } else if (this.options.inputstream) { |
609 | // push args to make ffmpeg read from stdin | |
610 | 2 | args.push('-i', '-'); |
611 | 0 | } else if (this.options.inputlive){ |
612 | //Check if input URI | |
613 | 0 | if(/^[a-z]+:\/\//.test(this.options.inputfile)) { |
614 | // add input with live flag | |
615 | 0 | args.push('-i', this.options.inputfile.replace(' ', '%20')+' live=1'); |
616 | }else { | |
617 | 0 | this.options.logger.error('live input URI is not valid'); |
618 | 0 | throw new Error('live input URI is not valid'); |
619 | } | |
620 | } | |
621 | ||
622 | 44 | if (this.options.otherInputs) { |
623 | 44 | if (this.options.otherInputs.length > 0) { |
624 | 1 | this.options.otherInputs.forEach(function(el) { |
625 | 1 | args.push('-i', el); |
626 | }); | |
627 | } | |
628 | } | |
629 | ||
630 | 44 | if (this.options.duration) { |
631 | 3 | args.push('-t', this.options.duration); |
632 | } | |
633 | ||
634 | 44 | if (this.options.video.framecount) { |
635 | 1 | args.push('-vframes', this.options.video.framecount); |
636 | } | |
637 | ||
638 | // add format | |
639 | 44 | if (this.options.format) { |
640 | 12 | args.push('-f', this.options.format); |
641 | } | |
642 | ||
643 | // add video options | |
644 | 44 | if (this.options.video.skip) { |
645 | // skip video stream completely (#45) | |
646 | 2 | args.push('-vn'); |
647 | } else { | |
648 | 42 | if (this.options.video.bitrate) { |
649 | 13 | args.push('-b', this.options.video.bitrate + 'k'); |
650 | 13 | if (this.options._useConstantVideoBitrate) { |
651 | // add parameters to ensure constant bitrate encoding | |
652 | 1 | args.push('-maxrate', this.options.video.bitrate + 'k'); |
653 | 1 | args.push('-minrate', this.options.video.bitrate + 'k'); |
654 | 1 | args.push('-bufsize', '3M'); |
655 | } | |
656 | } | |
657 | 42 | if (this.options.video.codec) { |
658 | 12 | args.push('-vcodec', this.options.video.codec); |
659 | } | |
660 | 42 | if (this.options.video.fps) { |
661 | 9 | args.push('-r', this.options.video.fps); |
662 | } | |
663 | 42 | if (this.options.video.aspect) { |
664 | 5 | args.push('-aspect', this.options.video.aspect); |
665 | } | |
666 | } | |
667 | ||
668 | // add video options | |
669 | 44 | if (this.options.audio.skip) { |
670 | // skip audio stream completely (#45) | |
671 | 2 | args.push('-an'); |
672 | } else { | |
673 | 42 | if (this.options.audio.bitrate) { |
674 | 13 | args.push('-ab', this.options.audio.bitrate + 'k'); |
675 | } | |
676 | 42 | if (this.options.audio.channels) { |
677 | 12 | args.push('-ac', this.options.audio.channels); |
678 | } | |
679 | 42 | if (this.options.audio.codec) { |
680 | 12 | args.push('-acodec', this.options.audio.codec); |
681 | } | |
682 | 42 | if (this.options.audio.frequency) { |
683 | 9 | args.push('-ar', this.options.audio.frequency); |
684 | } | |
685 | 42 | if (this.options.audio.quality || this.options.audio.quality === 0) { |
686 | 1 | args.push('-aq', this.options.audio.quality); |
687 | } | |
688 | } | |
689 | ||
690 | // add additional options | |
691 | 44 | if (this.options.additional) { |
692 | 44 | if (this.options.additional.length > 0) { |
693 | 5 | this.options.additional.forEach(function(el) { |
694 | 62 | args.push(el); |
695 | }); | |
696 | } | |
697 | } | |
698 | ||
699 | 44 | if (this.options.video.pad && !this.options.video.skip) { |
700 | // we have padding arguments, push | |
701 | 5 | if (this.atLeastVersion(meta.ffmpegversion, '0.7')) { |
702 | // padding is not supported ffmpeg < 0.7 (only using legacy commands which were replaced by vfilter calls) | |
703 | 5 | args.push('-vf'); |
704 | 5 | args.push('pad=' + this.options.video.pad.w + |
705 | ':' + this.options.video.pad.h + | |
706 | ':' + this.options.video.pad.x + | |
707 | ':' + this.options.video.pad.y + | |
708 | ':' + this.options.video.padcolor); | |
709 | } else { | |
710 | 0 | return new Error("Your ffmpeg version " + meta.ffmpegversion + " does not support padding"); |
711 | } | |
712 | } | |
713 | ||
714 | // add size and output file | |
715 | 44 | if (this.options.video.size && !this.options.video.skip) { |
716 | 20 | args.push('-s', this.options.video.size); |
717 | } | |
718 | ||
719 | // add output file fps | |
720 | 44 | if (this.options.video.fpsOutput) { |
721 | 0 | args.push('-r', this.options.video.fpsOutput); |
722 | } | |
723 | ||
724 | 44 | if (this.options.outputfile) { |
725 | 6 | var target = this.escapedPath(this.options.outputfile, false); |
726 | 6 | if (!os.match(/win(32|64)/)) { |
727 | 6 | args.push('-y', target.replace(' ', '\\ ')); |
728 | } else { | |
729 | 0 | args.push('-y', target); |
730 | } | |
731 | } else { | |
732 | 38 | if (!overrideOutputCheck) { |
733 | 0 | this.options.logger.error('no outputfile specified'); |
734 | } | |
735 | } | |
736 | 44 | return args; |
737 | }; | |
738 | }; |
Line | Hits | Source |
---|---|---|
1 | 1 | exports = module.exports = { |
2 | instance: { | |
3 | values: [], | |
4 | getIndex: function(name) { | |
5 | 2 | for (var i = 0; i < this.values.length; i++) { |
6 | 2 | if (this.values[i].name === name) { |
7 | 2 | return i; |
8 | } | |
9 | } | |
10 | }, | |
11 | set : function(name, value) { | |
12 | 8 | if (this.get(name) === false) { |
13 | 6 | this.values.push({ name: name, value: value }); |
14 | } else { | |
15 | 2 | this.values[this.getIndex(name)].value = value; |
16 | } | |
17 | }, | |
18 | get: function(name) { | |
19 | 16 | for (var i = 0; i < this.values.length; i++) { |
20 | 13 | if (this.values[i].name === name) { |
21 | 12 | return this.values[i].value; |
22 | } | |
23 | } | |
24 | 4 | return false; |
25 | }, | |
26 | reset: function() { | |
27 | 1 | this.values = []; |
28 | } | |
29 | } | |
30 | }; |