UNPKG

7.62 kBJavaScriptView Raw
1/*jslint node: true, esversion: 6 */
2"use strict";
3
4const spawn = require('child_process').spawn;
5
6const debug = require('debug')('upnpserver:contentHandlers:FFprobe');
7const debugData = require('debug')('upnpserver:contentHandlers:FFprobe:data');
8const logger = require('../logger');
9
10const Abstract_Metas = require('./abstract_metas');
11
12class 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 // ffprobe = "ffprobe";
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 // TODO use ContentProvider
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"; // MPEG-4 part 4
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"; // DTS
235 break;
236 case "aac":
237 component.mimeType = "audio/ac3"; // Dolby
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 // One property ?
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
327function convertLanguage(lang) {
328 return lang;
329}
330
331module.exports = ffprobe;