Commit 21120302 authored by Alexander Makarenko's avatar Alexander Makarenko

first commit

parent f35b881f
'use strict'
var path = require('path')
, PlainWriter = require('./writer/plain')
, SpriteSheetWriter = require('./writer/spritesheet')
, utils = require('./utils')
/**
* Main GodLike callback
*
* @callback thumbgenCallback
* @param {Error} err Any error
* @param {object} metadata Metadata
*/
/**
* Generate thumbnails and pack them into WebVTT file
*
* @param {string} source Video file
* @param {object} options Various options
* @param {thumbgenCallback} callback Accepts arguments: (err, metadata)
*/
module.exports = function(source, options, callback) {
if (!source) {
return callback(new Error('Source video file is not specified'))
}
else if (!options.numThumbnails && !options.timemarks) {
return callback(new Error('You should specify either timemarks or number of thumbnails to generate'))
}
var sourceExt = path.extname(source)
, sourceBase = path.basename(source, sourceExt)
, outputDir
, metadata
if (!options.output) {
options.output = path.join(path.dirname(source), sourceBase + '.vtt')
}
outputDir = path.dirname(options.output)
if (!options.assetsDirectory) {
options.assetsDirectory = sourceBase
}
options.thumbnailsDirectory = path.join(outputDir, options.assetsDirectory)
utils.metadata(source, onmetadata)
function onmetadata(err, data) {
if (err) {
return callback(err)
}
metadata = data
if (!options.timemarks) {
var diff = metadata.duration * 0.9 / options.numThumbnails
, i = 0
options.timemarks = []
options.bounds = []
while (i < options.numThumbnails) {
options.bounds.push(Number(i*diff).toFixed(3))
options.timemarks.push(Number(i * diff + diff / 2).toFixed(3))
i++
}
}
if (!options.size) {
options.size = {
width: metadata.width,
height: metadata.height
}
}
else if (!options.size.height) {
options.size.height = options.size.width * metadata.height / metadata.width
}
else if (!options.size.width) {
options.size.width = options.size.height * metadata.width / metadata.height
}
utils.generateThumbnails(source, {
directory: options.thumbnailsDirectory,
size: options.size,
timemarks: options.timemarks
}, ongenerate)
}
function ongenerate(err, filenames) {
if (err) {
return callback(err)
}
var writer
if (options.spritesheet) {
writer = new SpriteSheetWriter(metadata, options, filenames)
}
else {
writer = new PlainWriter(metadata, options, filenames)
}
writer.on('error', onerror)
writer.on('success', onsuccess)
}
function onerror(err) {
callback(err)
}
function onsuccess(data) {
callback(null, {
thumbnailsData: data
})
}
}
\ No newline at end of file
'use strict'
var FFmpeg = require('fluent-ffmpeg')
, moment = require('moment')
/**
* generateThumbnails() callback
*
* @callback thumbnailsCallback
* @param {Error} err Any kind of error
* @param {string[]} filenames Thumbnails names
*/
/**
* Generate thumbnails
*
* @param {string} source Path to video file
* @param {object} options No comments
* @param {thumbnailsCallback} callback Accepts arguments: (err, filenames)
*/
exports.generateThumbnails = function(source, options, callback) {
new FFmpeg({source: source})
.withSize(toSizeString(options.size))
.on('error', onerror)
.on('end', success)
.takeScreenshots(
{
count: options.timemarks.length,
timemarks: options.timemarks
},
options.directory
)
function onerror(err) {
callback(err)
}
function success(filenames) {
callback(null, filenames)
}
}
/**
* metadata() callback
*
* @callback metadataCallback
* @param {Error} err Any kind of error
* @param {object} metadata Duration, size, etc.
*/
/**
* Get simple metadata for video
*
* @param {string} source Path to video file
* @param {metadataCallback} callback Accepts arguments: (err, metadata)
*/
exports.metadata = function(source, callback) {
FFmpeg.ffprobe(source, ondata)
function ondata(err, metadata) {
if (err) {
return callback(err)
}
var streams = metadata.streams
, stream
if (!streams) {
return callback(new Error('Unknown error running ffprobe'))
}
while (stream = streams.shift()) {
if (stream.codec_type === 'video') {
return callback(null, {
duration: parseFloat(metadata.format.duration),
width: parseInt(stream.width, 10),
height: parseInt(stream.height, 10)
})
}
}
return callback(new Error('Source video file does not have video stream.'))
}
}
/**
* Get widthxheight string from dimensions object
*
* @param {object} dimensions width/height object
* @returns {string}
*/
function toSizeString(dimensions) {
return dimensions.width + 'x' + dimensions.height
}
exports.toSizeString = toSizeString
/**
* Create timemark from number
*
* @param {float|string} mark
* @returns {string} Formatted timemark
*/
function toTimemark(mark) {
var m = moment(mark + '', 'X.SSS')
return m.utc().format('HH:mm:ss.SSS')
}
exports.toTimemark = toTimemark
\ No newline at end of file
'use strict'
var fs = require('fs')
, util = require('util')
, utils = require('./utils')
, Writable = require('stream').Writable
/**
* Abstract WebVTT writer. Should be extended
*
* @constructor
* @extends {Writable}
* @param {object} metadata Video file metadata
* @param {object} options Generator options
* @param {string[]} filenames Thumbnail filenames
*/
function Writer(metadata, options, filenames) {
Writable.call(this, options)
var self = this
this.metadata = metadata
this.options = options
this.filenames = filenames
this.ws = fs.createWriteStream(options.output)
// write header first
this.ws.write('WEBVTT', 'utf8')
this.ws.on('error', onerror)
this.on('internalError', oniternalerror)
this.on('finish', onfinish)
function onerror(err) {
self.emit('error', err)
}
function oniternalerror() {
self.emit('error')
self.ws.end()
}
function onfinish() {
self.ws.end()
}
}
util.inherits(Writer, Writable)
/**
* Write thumbnail data
*
* @private
* @param {string} str Data to write
* @param {string} encoding Data encoding
* @param {function} callback Accepts arguments: (err, data)
*/
Writer.prototype._write = function(str, encoding, callback) {
this.ws.write(str, encoding, callback)
}
/**
* Write thumbnails specs to VTT
*
* @protected
* @param {string[]} thumbnails List of thumbnails filenames
*/
Writer.prototype._writeInfo = function(thumbnails) {
var self = this
, bounds = this.options.bounds
, length = thumbnails.length
, thumbnail
, i = 0
, bound
, out = []
while (thumbnail = thumbnails[i]) {
bound = bounds[i]
var data = {
path: thumbnail,
from: utils.toTimemark(bound)
}
if (i === length - 1) {
data.to = utils.toTimemark(Number(this.metadata.duration).toFixed(3))
}
else {
data.to = utils.toTimemark(bounds[i+1])
}
out.push(data)
this.write(this.toThumbString(data))
i++
}
this.end()
this.on('finish', function() {
self.emit('success', out)
})
}
/**
* Write thumbnail data
*
* @protected
* @param {object} data Thumbnail data
* @returns {string} Thumbnail string
*/
Writer.prototype.toThumbString = function(data) {
return ['\n\n', data.from, ' --> ', data.to, '\n', data.path].join('')
}
module.exports = Writer
\ No newline at end of file
'use strict'
var path = require('path')
, util = require('util')
, Writer = require('../writer')
/**
* Plain writer writes info about thumbnails to WebVTT
* without creating spritesheet.
*
* @constructor
* @extends {Writer}
*/
function PlainWriter(metadata, options, filenames) {
Writer.call(this, metadata, options, filenames)
var assetsDirectory = options.assetsDirectory
, paths = []
, thumbnail
, i = 0
, thumbnailPath
while (thumbnail = filenames[i++]) {
thumbnailPath = util.format('/%s/%s', assetsDirectory.replace(path.sep, '/'), thumbnail)
paths.push(thumbnailPath)
}
this._writeInfo(paths)
}
util.inherits(PlainWriter, Writer)
module.exports = PlainWriter
\ No newline at end of file
'use strict'
var fs = require('fs-extra')
, lodash = require('lodash')
, path = require('path')
, sprite = require('node-sprite')
, util = require('util')
, utils = require('../utils')
, Writer = require('../writer')
/**
* Small helper to be sure we're not getting EEXIST
*/
function unlinkAndMove(src, dst, callback) {
fs.exists(dst, onexists)
function onexists(exists) {
if (!exists) {
move(null)
}
else {
fs.unlink(dst, move)
}
}
function move(err) {
if (err) {
callback(err)
}
else {
fs.move(src, dst, callback)
}
}
}
/**
* Creates spritesheet then writes files
*
* @constructor
* @extends {Writer}
*/
function SpriteSheetWriter(metadata, options, filenames) {
Writer.call(this, metadata, options, filenames)
var self = this
, spriteOptions
, sheetName = options.spriteSheetName || 'thumbnails'
, spritesDirectory = path.join(options.thumbnailsDirectory, sheetName)
, paths = []
spriteOptions = lodash.extend({
path: options.thumbnailsDirectory
}, options.spriteSheetOptions || {})
fs.exists(spritesDirectory, onexists)
function onexists(exists) {
if (exists) {
onmkdir(null)
}
else {
fs.mkdir(spritesDirectory, onmkdir)
}
}
function onmkdir(err) {
if (err) {
return self.emit('internalError', err)
}
var tomove = filenames.length
, error = null
, i = 0
, thumbnail
while (thumbnail = filenames[i++]) {
move(thumbnail)
}
function move(thumbnail) {
var src = path.join(options.thumbnailsDirectory, thumbnail)
, dst = path.join(spritesDirectory, thumbnail)
unlinkAndMove(src, dst, ondone)
}
function ondone(err) {
if (error) {
return
}
else if (err) {
error = err
self.emit('internalError', err)
}
else if (!--tomove) {
createSpriteSheet()
}
}
}
function createSpriteSheet() {
sprite.sprite(sheetName, spriteOptions, function(err, globalSprite) {
if (err) {
return self.emit('internalError', err)
}
var sprites = globalSprite.images
, sheetFilename = globalSprite.filename()
, sheetExt = path.extname(sheetFilename)
, filename
, i = 0
, spritePath
while (filename = filenames[i++]) {
sprite = lodash.find(sprites, {filename: filename})
spritePath = util.format(
'/%s/%s#xywh=%d,%d,%d,%d',
options.assetsDirectory,
sheetName + sheetExt,
sprite.positionX,
sprite.positionY,
sprite.width,
sprite.height
)
paths.push(spritePath)
}
renameSheetName(sheetFilename, sheetName + sheetExt)
})
}
function renameSheetName(src, dst) {
src = path.join(options.thumbnailsDirectory, src)
dst = path.join(options.thumbnailsDirectory, dst)
unlinkAndMove(src, dst, removeSpritesDirectory)
}
function removeSpritesDirectory(err) {
if (err) {
return self.emit('internalError', err)
}
fs.remove(spritesDirectory, removeSheetSpec)
}
function removeSheetSpec(err) {
if (err) {
return self.emit('internalError', err)
}
fs.remove(path.join(options.thumbnailsDirectory, sheetName + '.json'), writeInfo)
}
function writeInfo(err) {
if (err) {
return self.emit('internalError', err)
}
self._writeInfo(paths)
}
}
util.inherits(SpriteSheetWriter, Writer)
module.exports = SpriteSheetWriter
\ No newline at end of file
{
"name": "thumbot-webvtt",
"description": "Module for creating video thumbnails described by WebVTT file",
"version": "0.0.1",
"main": "lib/index.js",
"keywords": ["video", "thumbnail", "spritesheet", "sprite", "webvtt", "ffmpeg", "imagemagick"],
"dependencies": {
"fluent-ffmpeg": "^1.7.2",
"fs-extra": "^0.10.0",
"lodash": "^2.4.1",
"moment": "^2.7.0",
"node-sprite": "^0.1.2"
},
"author": "Alexander Makarenko <estliberitas@gmail.com>"
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment