Skip to content

Commit 3326540

Browse files
author
Sjoerd Tieleman
committed
Allow thumbnail-only jobs. Extra platform check for path separator / nul file.
1 parent d79ba80 commit 3326540

File tree

2 files changed

+66
-40
lines changed

2 files changed

+66
-40
lines changed

README.markdown

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,14 @@ The `thumbnail_options` object is optional and contains a set of thumbnails that
157157
* A size can be specified in pixels (width x height). If omitted it will generate thumbnails the size of the source video. (optional)
158158
* A format for the thumbnails. The format must be supported by your ffmpeg binary. If omitted it will generate thumbnails in the JPEG format. Most people will use either "jpg" or "png". (optional)
159159

160-
If you specify thumbnails but somehow they can't be generated, your job will be marked as failed.
160+
If you specify thumbnails but an error occurs during generation, your job will be marked as failed. If you don't specify a valid `seconds` or `percentages` option thumbnail generation will be skipped but the job can still be completed successfully.
161161

162162
All other options are required (`source_file`, `destination_file` and `encoder_options`). Input and output files must be *absolute* paths.
163163

164+
### Thumbnail-only job
165+
166+
It's possible to only generate thumbnails from a video and not do any transcoding at all. This might come in handy if you're transcoding to lots of different formats and want to keep thumbnail generation separate from transcoding. You achieve this by POSTing a job with `"encoder_options": ""` (empty string) and of course specifying your `thumbnail_options`. In this case `destination_file` should be a _prefix_ for the output file, e.g. `"destination_file": "/Users/codem/output/my_video"` results in thumbnails in `/Users/codem/output/` with filenames such as `my_video-$offset.$format` (where `$offset` is the thumbnail offset in the video and `$format` of course the thumbnail format). All other options remain the same. See the examples.
167+
164168
* * *
165169
Request: `GET /jobs`
166170

@@ -249,6 +253,12 @@ Probe a file using `ffprobe`.
249253

250254
Output: {"ffprobe":{"streams":[ ... stream info ... ],"format":{ ... format info ... }}}}
251255

256+
Thumbnail-only job (160x90 in PNG format every 10% of the video).
257+
258+
# curl -d '{"source_file": "/tmp/video.mp4","destination_file":"/tmp/thumbnails/video","encoder_options": "", "thumbnail_options": { "percentages": 0.1, "size": "160x90", "format": "png"} }' http://localhost:8080/jobs
259+
260+
Output: {"message":"The transcoder accepted your job.","job_id":"d4b1dfebe6860839b2c21b70f35938d870011682"}
261+
252262
## Issues and support
253263

254264
If you run into any issues while using codem-transcode please use the Github issue tracker to see if it is a known problem

lib/job.js

Lines changed: 55 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ var util = require('util'),
88
notifyHandler = require('./notify-handler'),
99
logger = require('./logger'),
1010
Sequelize = require('sequelize'),
11-
async = require('async')
11+
async = require('async'),
12+
os = require('os');
1213

1314
var StatusCodes = {
1415
SUCCESS: "success",
@@ -233,25 +234,35 @@ var Job = JobUtils.getDatabase().define('Job', {
233234
spawn: function() {
234235
var job = this;
235236
if (this.hasInput && this.hasOutputDir && !this.the_process) {
236-
var args = this.parsedOpts()['encoder_options'].replace(/\s+/g, " ").split(' ');
237-
args.unshift('-i', this.parsedOpts()['source_file']);
237+
var args = [];
238+
args.push('-i', this.parsedOpts()['source_file']);
238239

239240
var extension = path.extname(this.parsedOpts()['destination_file']);
240241
var outputDir = path.dirname(this.parsedOpts()['destination_file']);
241242

242-
var tmpFile = outputDir + "/" + this.internalId + extension;
243+
if (this.parsedOpts()['encoder_options'].length > 0) {
244+
// "proper" transcoding job
245+
args = args.concat(this.parsedOpts()['encoder_options'].replace(/\s+/g, " ").split(' '));
246+
var tmpFile = outputDir + path.sep + this.internalId + extension;
243247

244-
if (config['use_scratch_dir'] == true) {
245-
tmpFile = config['scratch_dir'] + "/" + this.internalId + extension;
246-
}
248+
if (config['use_scratch_dir'] == true) {
249+
tmpFile = config['scratch_dir'] + path.sep + this.internalId + extension;
250+
}
247251

248-
args.push(tmpFile);
252+
args.push(tmpFile);
249253

254+
job.tmpFile = tmpFile;
255+
} else {
256+
// thumbnail only job, but still need to find duration, so we start a "null" job
257+
var null_file = (!!os.platform().match(/^win/) ? 'nul' : '/dev/null');
258+
args.push('-f', 'null', '-acodec', 'copy', '-vcodec', 'copy', '-y', null_file);
259+
}
260+
250261
var the_process = child_process.spawn(config['encoder'], args);
251262
the_process.stderr.on('data', function(data) { job.progressHandler(data) });
252-
the_process.on('exit', function(code) { job.didFinish(code, tmpFile); });
263+
the_process.on('exit', function(code) { job.didFinish(code); });
253264

254-
this.the_process = the_process;
265+
this.the_process = the_process;
255266
}
256267
},
257268
cancel: function() {
@@ -260,14 +271,14 @@ var Job = JobUtils.getDatabase().define('Job', {
260271
this.exitHandler(-1, 'job was cancelled');
261272
}
262273
},
263-
didFinish: function(code, tmpFile) {
274+
didFinish: function(code) {
264275
if (code == 0 && this.parsedOpts()['thumbnail_options']) {
265-
this.processThumbnails(tmpFile);
276+
this.processThumbnails();
266277
} else {
267-
this.finalize(code, tmpFile);
278+
this.finalize(code);
268279
}
269280
},
270-
processThumbnails: function(tmpFile) {
281+
processThumbnails: function() {
271282
logger.log("Processing thumbnails for job " + this.internalId + ".");
272283
var thumbOpts = this.parsedOpts()['thumbnail_options'];
273284
var range = JobUtils.generateRangeFromThumbOpts(thumbOpts, this.duration);
@@ -279,23 +290,23 @@ var Job = JobUtils.getDatabase().define('Job', {
279290
function(err, results) {
280291
if (err) {
281292
job.lastMessage = err.message;
282-
job.finalize(1, tmpFile);
293+
job.finalize(1);
283294
} else {
284-
job.finalize(0, tmpFile, results);
295+
job.finalize(0, results);
285296
}
286297
}
287298
);
288299
} else {
289300
// no valid range
290301
logger.log("No valid thumbnails to process for job " + this.internalId + ". Skipping...");
291-
this.finalize(0, tmpFile);
302+
this.finalize(0);
292303
}
293304
},
294305
execThumbJob: function(offset) {
295306
var job = this;
296307
return function(callback) {
297308
var thumbOpts = job.parsedOpts()['thumbnail_options'];
298-
var args = ['-ss', offset, '-i', job.parsedOpts()['source_file'], '-vframes', '1'];
309+
var args = ['-ss', offset, '-i', job.parsedOpts()['source_file'], '-vframes', '1', '-y'];
299310

300311
// Explicit size provided
301312
if (thumbOpts['size'] && thumbOpts['size'] != 'src') {
@@ -318,35 +329,40 @@ var Job = JobUtils.getDatabase().define('Job', {
318329
});
319330
}
320331
},
321-
finalize: function(code, tmpFile, thumbnails) {
332+
finalize: function(code, thumbnails) {
322333
var job = this;
323334
if (thumbnails) job.thumbnails = JSON.stringify(thumbnails);
324335

325336
if (code == 0) {
326-
fs.rename(tmpFile, job.parsedOpts()['destination_file'], function (err) {
327-
if (err) {
328-
if ( (err.message).match(/EXDEV/) ) {
329-
/*
330-
EXDEV fix, since util.pump is deprecated, using stream.pipe
331-
example from http://stackoverflow.com/questions/11293857/fastest-way-to-copy-file-in-node-js
332-
*/
333-
try {
334-
logger.log('ffmpeg finished successfully, trying to copy across partitions');
335-
fs.createReadStream(tmpFile).pipe(fs.createWriteStream(job.parsedOpts()['destination_file']));
336-
job.exitHandler(code, 'ffmpeg finished succesfully.');
337-
} catch (err) {
337+
if (job.tmpFile) {
338+
fs.rename(job.tmpFile, job.parsedOpts()['destination_file'], function (err) {
339+
if (err) {
340+
if ( (err.message).match(/EXDEV/) ) {
341+
/*
342+
EXDEV fix, since util.pump is deprecated, using stream.pipe
343+
example from http://stackoverflow.com/questions/11293857/fastest-way-to-copy-file-in-node-js
344+
*/
345+
try {
346+
logger.log('ffmpeg finished successfully, trying to copy across partitions');
347+
fs.createReadStream(job.tmpFile).pipe(fs.createWriteStream(job.parsedOpts()['destination_file']));
348+
job.exitHandler(code, 'ffmpeg finished succesfully.');
349+
} catch (err) {
350+
logger.log(err);
351+
job.exitHandler(-1, 'ffmpeg finished succesfully, but unable to move file to different partition (' + job.parsedOpts()['destination_file'] + ').');
352+
}
353+
354+
} else {
338355
logger.log(err);
339-
job.exitHandler(-1, 'ffmpeg finished succesfully, but unable to move file to different partition (' + job.parsedOpts()['destination_file'] + ').');
356+
job.exitHandler(-1, 'ffmpeg finished succesfully, but unable to move file to destination (' + job.parsedOpts()['destination_file'] + ').');
340357
}
341-
342358
} else {
343-
logger.log(err);
344-
job.exitHandler(-1, 'ffmpeg finished succesfully, but unable to move file to destination (' + job.parsedOpts()['destination_file'] + ').');
359+
job.exitHandler(code, 'ffmpeg finished succesfully.');
345360
}
346-
} else {
347-
job.exitHandler(code, 'ffmpeg finished succesfully.');
348-
}
349-
});
361+
});
362+
} else {
363+
// No tmpFile, hence no transcoding, only thumbnails
364+
job.exitHandler(code, 'finished thumbnail job.');
365+
}
350366
} else {
351367
job.exitHandler(code, "ffmpeg finished with an error: '" + job.lastMessage + "' (" + code + ").")
352368
}

0 commit comments

Comments
 (0)