artifact_converter.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. 'use strict';
  2. const exec = require('child_process');
  3. const gm = require('gm');
  4. const async = require('async');
  5. const fs = require('fs');
  6. const Models = require('../models/db');
  7. const uploader = require('../helpers/uploader');
  8. const path = require('path');
  9. const os = require('os');
  10. const db = require('../models/db');
  11. const Sequelize = require('sequelize');
  12. const Op = Sequelize.Op;
  13. const mime = require('mime-types');
  14. const fileType = require('file-type');
  15. const readChunk = require('read-chunk');
  16. const convertableImageTypes = [
  17. "image/png",
  18. "image/jpeg",
  19. "application/pdf",
  20. "image/jpg",
  21. "image/gif",
  22. "image/tiff",
  23. "image/vnd.adobe.photoshop"];
  24. const convertableVideoTypes = [
  25. "video/quicktime",
  26. "video/3gpp",
  27. "video/mpeg",
  28. "video/mp4",
  29. "video/ogg"];
  30. const convertableAudioTypes = [
  31. "application/ogg",
  32. "audio/amr",
  33. "audio/3ga",
  34. "audio/wave",
  35. "audio/3gpp",
  36. "audio/x-wav",
  37. "audio/aiff",
  38. "audio/x-aiff",
  39. "audio/ogg",
  40. "audio/mp4",
  41. "audio/x-m4a",
  42. "audio/mpeg",
  43. "audio/mp3",
  44. "audio/x-hx-aac-adts",
  45. "audio/aac"];
  46. function getDuration(localFilePath, callback){
  47. exec.execFile("ffprobe", ["-show_format", "-of", "json", localFilePath], function(error, stdout, stderr) {
  48. var test = JSON.parse(stdout);
  49. callback(parseFloat(test.format.duration));
  50. });
  51. }
  52. function createWaveform(fileName, localFilePath, callback){
  53. var filePathImage = localFilePath + "-" + (new Date().getTime()) + ".png";
  54. getDuration(localFilePath, function(duration){
  55. var totalTime = duration || 1.0;
  56. var pixelsPerSecond = 256.0;
  57. do {
  58. var targetWidth = parseInt(pixelsPerSecond*totalTime, 10);
  59. if (targetWidth>2048) pixelsPerSecond/=2.0;
  60. } while (targetWidth>2048 && pixelsPerSecond>1);
  61. exec.execFile("audiowaveform",
  62. [
  63. "-w",
  64. ""+targetWidth,
  65. "--pixels-per-second",
  66. ""+parseInt(pixelsPerSecond),
  67. "--background-color", "ffffff00",
  68. "--border-color", "ffffff",
  69. "--waveform-color", "3498db",
  70. "--no-axis-labels",
  71. "-i", localFilePath, "-o", filePathImage
  72. ],
  73. {}, function(error, stdout, stderr) {
  74. if(!error) {
  75. callback(null, filePathImage);
  76. } else {
  77. console.log("error:", stdout, stderr);
  78. callback(error, null);
  79. }
  80. });
  81. });
  82. }
  83. function convertVideo(fileName, filePath, codec, callback, progressCallback) {
  84. var ext = path.extname(fileName);
  85. var presetMime = mime.lookup(fileName);
  86. var newExt = codec == "mp4" ? "mp4" : "ogv";
  87. var convertedPath = filePath + "." + newExt;
  88. console.log("converting", filePath, "to", convertedPath);
  89. var convertArgs = (codec == "mp4") ? [
  90. "-i", filePath,
  91. "-threads", "4",
  92. "-vf", "scale=1280:trunc(ow/a/2)*2", // scale to width of 1280, truncating height to an even value
  93. "-b:v", "2000k",
  94. "-acodec", "libvo_aacenc",
  95. "-b:a", "96k",
  96. "-vcodec", "libx264",
  97. "-y", convertedPath ]
  98. : [
  99. "-i", filePath,
  100. "-threads", "4",
  101. "-vf", "scale=1280:trunc(ow/a/2)*2", // scale to width of 1280, truncating height to an even value
  102. "-b:v", "2000k",
  103. "-acodec", "libvorbis",
  104. "-b:a", "96k",
  105. "-vcodec", "libtheora",
  106. "-y", convertedPath];
  107. var ff = exec.spawn('ffmpeg', convertArgs, {
  108. stdio: [
  109. 'pipe', // use parents stdin for child
  110. 'pipe', // pipe child's stdout to parent
  111. 'pipe'
  112. ]
  113. });
  114. ff.stdout.on('data', function (data) {
  115. console.log('[ffmpeg-video] stdout: ' + data);
  116. });
  117. ff.stderr.on('data', function (data) {
  118. console.log('[ffmpeg-video] stderr: ' + data);
  119. if (progressCallback) {
  120. progressCallback(data);
  121. }
  122. });
  123. ff.on('close', function (code) {
  124. console.log('[ffmpeg-video] child process exited with code ' + code);
  125. if (!code) {
  126. console.log("converted", filePath, "to", convertedPath);
  127. callback(null, convertedPath);
  128. } else {
  129. callback(code, null);
  130. }
  131. });
  132. }
  133. function convertAudio(fileName, filePath, codec, callback) {
  134. var ext = path.extname(fileName);
  135. var presetMime = mime.lookup(fileName);
  136. var newExt = codec == "mp3" ? "mp3" : "ogg";
  137. var convertedPath = filePath + "." + newExt;
  138. console.log("converting audio", filePath, "to", convertedPath);
  139. var convertArgs = (ext == ".aac") ? [ "-i", filePath, "-y", convertedPath ]
  140. : [ "-i", filePath,
  141. "-b:a", "128k",
  142. "-y", convertedPath];
  143. exec.execFile("ffmpeg", convertArgs , {}, function(error, stdout, stderr) {
  144. if(!error){
  145. console.log("converted", filePath, "to", convertedPath);
  146. callback(null, convertedPath);
  147. }else{
  148. console.log(error,stdout, stderr);
  149. callback(error, null);
  150. }
  151. });
  152. }
  153. function createThumbnailForVideo(fileName, filePath, callback) {
  154. var filePathImage = filePath + ".jpg";
  155. exec.execFile("ffmpeg", ["-y", "-i", filePath, "-ss", "00:00:01.00", "-vcodec", "mjpeg", "-vframes", "1", "-f", "image2", filePathImage], {}, function(error, stdout, stderr){
  156. if(!error){
  157. callback(null, filePathImage);
  158. }else{
  159. console.log("error:", stdout, stderr);
  160. callback(error, null);
  161. }
  162. });
  163. }
  164. function getMime(fileName, filePath, callback) {
  165. var ext = path.extname(fileName);
  166. var presetMime = mime.lookup(fileName);
  167. if (presetMime) {
  168. callback(null, presetMime);
  169. } else {
  170. const buffer = readChunk.sync(filePath, 0, 4100);
  171. var mimeType = fileType(buffer);
  172. callback(null, mimeType);
  173. }
  174. }
  175. function resizeAndUpload(a, size, max, fileName, localFilePath, callback) {
  176. if (max>320 || size.width > max || size.height > max) {
  177. var resizedFileName = max + "_"+fileName;
  178. var s3Key = "s"+ a.space_id.toString() + "/a" + a._id.toString() + "/" + resizedFileName;
  179. var localResizedFilePath = os.tmpdir()+"/"+resizedFileName;
  180. gm(localFilePath).resize(max, max).autoOrient().write(localResizedFilePath, function (err) {
  181. if(!err) {
  182. uploader.uploadFile(s3Key, "image/jpeg", localResizedFilePath, function(err, url) {
  183. if (err) callback(err);
  184. else{
  185. fs.unlink(localResizedFilePath, function (err) {
  186. if (err) {
  187. console.error(err);
  188. callback(null, url);
  189. }
  190. else callback(null, url);
  191. });
  192. }
  193. });
  194. } else {
  195. console.error(err);
  196. callback(err);
  197. }
  198. });
  199. } else {
  200. callback(null, "");
  201. }
  202. }
  203. var resizeAndUploadImage = function(a, mimeType, size, fileName, fileNameOrg, imageFilePath, originalFilePath, payloadCallback) {
  204. async.parallel({
  205. small: function(callback){
  206. resizeAndUpload(a, size, 320, fileName, imageFilePath, callback);
  207. },
  208. medium: function(callback){
  209. resizeAndUpload(a, size, 800, fileName, imageFilePath, callback);
  210. },
  211. big: function(callback){
  212. resizeAndUpload(a, size, 1920, fileName, imageFilePath, callback);
  213. },
  214. original: function(callback){
  215. var s3Key = "s"+ a.space_id.toString() + "/a" + a._id + "/" + fileNameOrg;
  216. uploader.uploadFile(s3Key, mimeType, originalFilePath, function(err, url){
  217. callback(null, url);
  218. });
  219. }
  220. }, function(err, results) {
  221. a.state = "idle";
  222. a.mime = mimeType;
  223. var stats = fs.statSync(originalFilePath);
  224. a.payload_size = stats["size"];
  225. a.payload_thumbnail_web_uri = results.small;
  226. a.payload_thumbnail_medium_uri = results.medium;
  227. a.payload_thumbnail_big_uri = results.big;
  228. a.payload_uri = results.original;
  229. var factor = 320/size.width;
  230. a.w = Math.round(size.width*factor);
  231. a.h = Math.round(size.height*factor);
  232. a.updated_at = new Date();
  233. db.packArtifact(a);
  234. a.save().then(function() {
  235. fs.unlink(originalFilePath, function (err) {
  236. if (err){
  237. console.error(err);
  238. payloadCallback(err, null);
  239. } else {
  240. console.log('successfully deleted ' + originalFilePath);
  241. payloadCallback(null, a);
  242. }
  243. });
  244. });
  245. });
  246. };
  247. module.exports = {
  248. convert: function(a, fileName, localFilePath, payloadCallback, progressCallback) {
  249. getMime(fileName, localFilePath, function(err, mimeType){
  250. console.log("[convert] fn: "+fileName+" local: "+localFilePath+" mimeType:", mimeType);
  251. if (!err) {
  252. if (convertableImageTypes.indexOf(mimeType) > -1) {
  253. gm(localFilePath).size(function (err, size) {
  254. console.log("[convert] gm:", err, size);
  255. if (!err) {
  256. if(mimeType == "application/pdf") {
  257. var firstImagePath = localFilePath + ".jpeg";
  258. exec.execFile("gs", ["-sDEVICE=jpeg","-dNOPAUSE", "-dJPEGQ=80", "-dBATCH", "-dFirstPage=1", "-dLastPage=1", "-sOutputFile=" + firstImagePath, "-r90", "-f", localFilePath], {}, function(error, stdout, stderr) {
  259. if(error === null) {
  260. resizeAndUploadImage(a, mimeType, size, fileName + ".jpeg", fileName, firstImagePath, localFilePath, function(err, a) {
  261. fs.unlink(firstImagePath, function (err) {
  262. payloadCallback(err, a);
  263. });
  264. });
  265. } else {
  266. payloadCallback(error, null);
  267. }
  268. });
  269. } else if(mimeType == "image/gif") {
  270. //gifs are buggy after convertion, so we should not convert them
  271. var s3Key = "s"+ a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName;
  272. uploader.uploadFile(s3Key, "image/gif", localFilePath, function(err, url) {
  273. if (err) payloadCallback(err);
  274. else {
  275. console.log(localFilePath);
  276. var stats = fs.statSync(localFilePath);
  277. a.state = "idle";
  278. a.mime = mimeType;
  279. a.payload_size = stats["size"];
  280. a.payload_thumbnail_web_uri = url;
  281. a.payload_thumbnail_medium_uri = url;
  282. a.payload_thumbnail_big_uri = url;
  283. a.payload_uri = url;
  284. var factor = 320/size.width;
  285. a.w = Math.round(size.width*factor);
  286. a.h = Math.round(size.height*factor);
  287. a.updated_at = new Date();
  288. db.packArtifact(a);
  289. a.save().then(function() {
  290. fs.unlink(localFilePath, function (err) {
  291. if (err){
  292. console.error(err);
  293. payloadCallback(err, null);
  294. } else {
  295. console.log('successfully deleted ' + localFilePath);
  296. payloadCallback(null, a);
  297. }
  298. });
  299. });
  300. }
  301. });
  302. } else {
  303. resizeAndUploadImage(a, mimeType, size, fileName, fileName, localFilePath, localFilePath, payloadCallback);
  304. }
  305. } else payloadCallback(err);
  306. });
  307. } else if (convertableVideoTypes.indexOf(mimeType) > -1) {
  308. async.parallel({
  309. thumbnail: function(callback) {
  310. createThumbnailForVideo(fileName, localFilePath, function(err, created){
  311. console.log("thumbnail created: ", err, created);
  312. if (err) callback(err);
  313. else {
  314. var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + ".jpg" ;
  315. uploader.uploadFile(keyName, "image/jpeg", created, function(err, url){
  316. if (err) callback(err);
  317. else callback(null, url);
  318. });
  319. }
  320. });
  321. },
  322. ogg: function(callback) {
  323. if (mimeType == "video/ogg") {
  324. callback(null, "org");
  325. } else {
  326. convertVideo(fileName, localFilePath, "ogg", function(err, file) {
  327. if(err) callback(err);
  328. else {
  329. var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + ".ogv" ;
  330. uploader.uploadFile(keyName, "video/ogg", file, function(err, url){
  331. if (err) callback(err);
  332. else callback(null, url);
  333. });
  334. }
  335. }, progressCallback);
  336. }
  337. },
  338. mp4: function(callback) {
  339. if (mimeType == "video/mp4") {
  340. callback(null, "org");
  341. } else {
  342. convertVideo(fileName, localFilePath, "mp4", function(err, file) {
  343. if (err) callback(err);
  344. else {
  345. var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + ".mp4";
  346. uploader.uploadFile(keyName, "video/mp4" ,file, function(err, url) {
  347. if (err) callback(err);
  348. else callback(null, url);
  349. });
  350. }
  351. }, progressCallback);
  352. }
  353. },
  354. original: function(callback){
  355. uploader.uploadFile(fileName, mimeType, localFilePath, function(err, url){
  356. callback(null, url);
  357. });
  358. }
  359. }, function(err, results) {
  360. console.log(err, results);
  361. if (err) payloadCallback(err, a);
  362. else {
  363. a.state = "idle";
  364. a.mime = mimeType;
  365. var stats = fs.statSync(localFilePath);
  366. a.payload_size = stats["size"];
  367. a.payload_thumbnail_web_uri = results.thumbnail;
  368. a.payload_thumbnail_medium_uri = results.thumbnail;
  369. a.payload_thumbnail_big_uri = results.thumbnail;
  370. a.payload_uri = results.original;
  371. if (mimeType == "video/mp4") {
  372. a.payload_alternatives = [
  373. {
  374. mime: "video/ogg",
  375. payload_uri: results.ogg
  376. }
  377. ];
  378. } else {
  379. a.payload_alternatives = [
  380. {
  381. mime: "video/mp4",
  382. payload_uri: results.mp4
  383. }
  384. ];
  385. }
  386. db.packArtifact(a);
  387. a.updated_at = new Date();
  388. a.save().then(function() {
  389. fs.unlink(localFilePath, function (err) {
  390. if (err) {
  391. console.error(err);
  392. payloadCallback(err, null);
  393. } else {
  394. console.log('successfully deleted ' + localFilePath);
  395. payloadCallback(null, a);
  396. }
  397. });
  398. });
  399. }
  400. });
  401. } else if (convertableAudioTypes.indexOf(mimeType) > -1) {
  402. async.parallel({
  403. ogg: function(callback) {
  404. convertAudio(fileName, localFilePath, "ogg", function(err, file) {
  405. if(err) callback(err);
  406. else {
  407. var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + ".ogg" ;
  408. uploader.uploadFile(keyName, "audio/ogg", file, function(err, url){
  409. if (err) callback(err);
  410. else callback(null, url);
  411. });
  412. }
  413. });
  414. },
  415. mp3_waveform: function(callback) {
  416. convertAudio(fileName, localFilePath, "mp3", function(err, file) {
  417. if(err) callback(err);
  418. else {
  419. createWaveform(fileName, file, function(err, filePath){
  420. var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + "-" + (new Date().getTime()) + ".png";
  421. uploader.uploadFile(keyName, "image/png", filePath, function(err, pngUrl){
  422. var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + ".mp3" ;
  423. uploader.uploadFile(keyName, "audio/mp3", file, function(err, mp3Url){
  424. if (err) callback(err);
  425. else callback(null, {waveform: pngUrl, mp3: mp3Url});
  426. });
  427. });
  428. });
  429. }
  430. });
  431. },
  432. original: function(callback) {
  433. var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName;
  434. uploader.uploadFile(keyName, mimeType, localFilePath, function(err, url){
  435. callback(null, url);
  436. });
  437. }
  438. }, function(err, results) {
  439. console.log(err, results);
  440. if (err) payloadCallback(err, a);
  441. else {
  442. a.state = "idle";
  443. a.mime = mimeType;
  444. var stats = fs.statSync(localFilePath);
  445. a.payload_size = stats["size"];
  446. a.payload_thumbnail_web_uri = results.mp3_waveform.waveform;
  447. a.payload_thumbnail_medium_uri = results.mp3_waveform.waveform;
  448. a.payload_thumbnail_big_uri = results.mp3_waveform.waveform;
  449. a.payload_uri = results.original;
  450. a.payload_alternatives = [
  451. {payload_uri:results.ogg, mime:"audio/ogg"},
  452. {payload_uri:results.mp3_waveform.mp3, mime:"audio/mpeg"}
  453. ];
  454. a.updated_at = new Date();
  455. db.packArtifact(a);
  456. a.save().then(function(){
  457. fs.unlink(localFilePath, function (err) {
  458. if (err){
  459. console.error(err);
  460. payloadCallback(err, null);
  461. } else {
  462. console.log('successfully deleted ' + localFilePath);
  463. payloadCallback(null, a);
  464. }
  465. });
  466. });
  467. }
  468. });
  469. } else {
  470. console.log("mimeType not matched for conversion, storing file");
  471. var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName;
  472. uploader.uploadFile(keyName, mimeType, localFilePath, function(err, url) {
  473. a.state = "idle";
  474. a.mime = mimeType;
  475. var stats = fs.statSync(localFilePath);
  476. a.payload_size = stats["size"];
  477. a.payload_uri = url;
  478. a.updated_at = new Date();
  479. a.save().then(function() {
  480. fs.unlink(localFilePath, function (err) {
  481. payloadCallback(null, a);
  482. });
  483. });
  484. });
  485. }
  486. } else {
  487. //there was an error getting mime
  488. payloadCallback(err);
  489. }
  490. });
  491. }
  492. };