1 |
|
2 | "use strict";
|
3 |
|
4 | const spawn = require('child_process').spawn;
|
5 |
|
6 | const debug = require('debug')('upnpserver:contentHandlers:FFprobe');
|
7 | const debugData = require('debug')('upnpserver:contentHandlers:FFprobe:data');
|
8 | const logger = require('../logger');
|
9 |
|
10 | const Abstract_Metas = require('./abstract_metas');
|
11 |
|
12 | class ffprobe extends Abstract_Metas {
|
13 | constructor(configuration) {
|
14 | super(configuration);
|
15 |
|
16 | var ffprobe = this._configuration.ffprobe_path;
|
17 | if (!ffprobe) {
|
18 | ffprobe = process.env.FFPROBE_PATH;
|
19 |
|
20 | if (!ffprobe) {
|
21 |
|
22 | }
|
23 | }
|
24 |
|
25 | this.ffprobe_path = ffprobe;
|
26 |
|
27 | debug("ffprobe", "set EXE path=", this.ffprobe_path);
|
28 | }
|
29 |
|
30 | get name() {
|
31 | return "ffprobe";
|
32 | }
|
33 |
|
34 | |
35 |
|
36 |
|
37 | prepareMetas(contentInfos, context, callback) {
|
38 | if (!this.ffprobe_path) {
|
39 | return callback();
|
40 | }
|
41 |
|
42 | var contentURL = contentInfos.contentURL;
|
43 |
|
44 | var localPath = '-';
|
45 |
|
46 | if (contentURL.contentProvider.isLocalFilesystem) {
|
47 | localPath = contentURL;
|
48 | }
|
49 |
|
50 |
|
51 | var parameters = ['-show_streams', '-show_format', '-print_format', 'json',
|
52 | '-loglevel', 'warning', localPath];
|
53 |
|
54 | debug("prepareMetas", "Launch ffprobe", this.ffprobe_path, "parameters=", parameters, "localPath=", localPath);
|
55 |
|
56 | var proc = spawn(this.ffprobe_path, parameters);
|
57 | var probeData = [];
|
58 | var errData = [];
|
59 | var exitCode;
|
60 | var start = Date.now();
|
61 |
|
62 | var callbackCalled = false;
|
63 |
|
64 | proc.stdout.setEncoding('utf8');
|
65 | proc.stderr.setEncoding('utf8');
|
66 |
|
67 | proc.stdout.on('data', (data) => {
|
68 | debugData("prepareMetas", "receive stdout=", data);
|
69 | probeData.push(data);
|
70 | });
|
71 | proc.stderr.on('data', (data) => {
|
72 | debugData("prepareMetas", "receive stderr=", data);
|
73 | errData.push(data);
|
74 | });
|
75 |
|
76 | proc.on('exit', (code) => {
|
77 | debug("prepareMetas", "Exit event received code=", code);
|
78 | exitCode = code;
|
79 | });
|
80 | proc.on('error', (error) => {
|
81 | debug("prepareMetas", "Error event received error=", error, "callbackCalled=", callbackCalled);
|
82 |
|
83 | if (error) {
|
84 | logger.error("parseURL", contentURL, error);
|
85 | }
|
86 |
|
87 | if (callbackCalled) {
|
88 | return;
|
89 | }
|
90 | callbackCalled = true;
|
91 | callback();
|
92 | });
|
93 |
|
94 | proc.on('close', () => {
|
95 | debug("prepareMetas", "Close event received exitCode=", exitCode, "callbackCalled=", callbackCalled);
|
96 | debugData("prepareMetas", "probeData=", probeData);
|
97 | debugData("prepareMetas", "errData=", errData);
|
98 |
|
99 | if (callbackCalled) {
|
100 | return;
|
101 | }
|
102 | callbackCalled = true;
|
103 |
|
104 | if (!probeData) {
|
105 | setImmediate(callback);
|
106 | return;
|
107 | }
|
108 |
|
109 | if (exitCode) {
|
110 | var err_output = errData.join('');
|
111 |
|
112 | var error = new Error("FFProbe error: " + err_output);
|
113 | logger.error(error);
|
114 | return callback(error);
|
115 | }
|
116 |
|
117 | var json = JSON.parse(probeData.join(''));
|
118 | json.probe_time = Date.now() - start;
|
119 |
|
120 | try {
|
121 | this._processProbe(json, callback);
|
122 |
|
123 | } catch (x) {
|
124 | logger.error(x);
|
125 | }
|
126 | });
|
127 |
|
128 | if (localPath === '-') {
|
129 | debug("prepareMetas", "Read stream", contentURL, "...");
|
130 | contentURL.createReadStream(null, null, (error, stream) => {
|
131 |
|
132 | if (error) {
|
133 | logger.error("Can not get stream of '" + contentURL + "'", error);
|
134 | return callback(error);
|
135 | }
|
136 |
|
137 | debug("prepareMetas", "Pipe stream", contentURL, " to ffprobe");
|
138 |
|
139 | stream.pipe(proc.stdin);
|
140 | });
|
141 | }
|
142 | }
|
143 |
|
144 | |
145 |
|
146 |
|
147 | _processProbe(json, callback) {
|
148 | debug("_processProbe", "Process json=", json);
|
149 |
|
150 | var video = false;
|
151 | var audio = false;
|
152 |
|
153 | var res = {};
|
154 |
|
155 | var components = [];
|
156 |
|
157 | var componentInfos = [{
|
158 | groupId: 0,
|
159 | components: components
|
160 | }];
|
161 |
|
162 | var streams = json.streams;
|
163 | if (streams.length) {
|
164 | streams.forEach((stream) => {
|
165 |
|
166 | if (stream.codec_type === "video") {
|
167 |
|
168 | var component = {
|
169 | componentID: "video_" + components.length,
|
170 | componentClass: "Video"
|
171 | };
|
172 | components.push(component);
|
173 |
|
174 | switch (stream.codec_name) {
|
175 | case "mpeg1video":
|
176 | component.mimeType = "video/mpeg";
|
177 | break;
|
178 | case "mpeg4":
|
179 | component.mimeType = "video/mpeg4";
|
180 | break;
|
181 | case "h261":
|
182 | component.mimeType = "video/h261";
|
183 | break;
|
184 | case "h263":
|
185 | component.mimeType = "video/h263";
|
186 | break;
|
187 | case "h264":
|
188 | component.mimeType = "video/h264";
|
189 | break;
|
190 | case "hevc":
|
191 | component.mimeType = "video/hevc";
|
192 | break;
|
193 | case "vorbis":
|
194 | component.mimeType = "video/ogg";
|
195 | break;
|
196 | }
|
197 | var tags = stream.tags;
|
198 | if (tags) {
|
199 | if (tags.title) {
|
200 | component.title = tags.title;
|
201 | }
|
202 | }
|
203 |
|
204 | if (!video) {
|
205 | video = true;
|
206 |
|
207 | if (stream.width && stream.height) {
|
208 | res.resolution = stream.width + "x" + stream.height;
|
209 | }
|
210 |
|
211 | if (stream.duration) {
|
212 | res.duration = parseFloat(stream.duration);
|
213 | }
|
214 | if (stream.codec_name) {
|
215 | res.vcodec = stream.codec_name;
|
216 | }
|
217 | }
|
218 | return;
|
219 | }
|
220 | if (stream.codec_type === "audio") {
|
221 | let component = {
|
222 | componentID: "audio_" + components.length,
|
223 | componentClass: "Audio"
|
224 | };
|
225 |
|
226 | switch (stream.codec_name) {
|
227 | case "mp2":
|
228 | component.mimeType = "audio/mpeg";
|
229 | break;
|
230 | case "mp4":
|
231 | component.mimeType = "audio/mpeg4";
|
232 | break;
|
233 | case "dca":
|
234 | component.mimeType = "audio/dca";
|
235 | break;
|
236 | case "aac":
|
237 | component.mimeType = "audio/ac3";
|
238 | break;
|
239 | case "aac":
|
240 | component.mimeType = "audio/aac";
|
241 | break;
|
242 | case "webm":
|
243 | component.mimeType = "audio/webm";
|
244 | break;
|
245 | case "wav":
|
246 | component.mimeType = "audio/wave";
|
247 | break;
|
248 | case "flac":
|
249 | component.mimeType = "audio/flac";
|
250 | break;
|
251 | case "vorbis":
|
252 | component.mimeType = "audio/ogg";
|
253 | break;
|
254 | }
|
255 | if (stream.channels) {
|
256 | component.nrAudioChannels = stream.channels;
|
257 | }
|
258 |
|
259 | let tags = stream.tags;
|
260 | if (tags) {
|
261 | if (tags.language) {
|
262 | component.language = convertLanguage(tags.language);
|
263 | }
|
264 | if (tags.title) {
|
265 | component.title = tags.title;
|
266 | }
|
267 | }
|
268 |
|
269 | if (!audio) {
|
270 | audio = true;
|
271 |
|
272 | if (stream.duration) {
|
273 | res.duration = parseFloat(stream.duration);
|
274 | }
|
275 | if (stream.codec_name) {
|
276 | res.acodec = stream.codec_name;
|
277 | }
|
278 | if (stream.bit_rate) {
|
279 | res.bitrate = stream.bit_rate;
|
280 | }
|
281 | if (stream.channels) {
|
282 | res.nrAudioChannels = stream.channels;
|
283 | }
|
284 | if (stream.sample_rate) {
|
285 | res.sampleFrequency = stream.sample_rate;
|
286 | }
|
287 | if (stream.bit_rate) {
|
288 | res.bitrate = stream.bit_rate;
|
289 | }
|
290 | }
|
291 | return;
|
292 | }
|
293 | if (stream.codec_type === "subtitle") {
|
294 | let component = {
|
295 | componentID: "sub_" + components.length,
|
296 | componentClass: "Subtitle"
|
297 | };
|
298 |
|
299 | let tags = stream.tags;
|
300 | if (tags) {
|
301 | if (tags.language) {
|
302 | component.language = convertLanguage(tags.language);
|
303 | }
|
304 | if (tags.title) {
|
305 | component.title = tags.title;
|
306 | }
|
307 | }
|
308 | }
|
309 | });
|
310 |
|
311 |
|
312 | if (components.length) {
|
313 | res.componentInfos = componentInfos;
|
314 | }
|
315 | }
|
316 |
|
317 | debug("_processProbe", "FFProbe res=", res);
|
318 |
|
319 | var metas = {
|
320 | res: [res]
|
321 | };
|
322 |
|
323 | callback(null, metas);
|
324 | }
|
325 | }
|
326 |
|
327 | function convertLanguage(lang) {
|
328 | return lang;
|
329 | }
|
330 |
|
331 | module.exports = ffprobe;
|