gifski.ts 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. import { spawn, spawnSync } from 'child_process';
  2. import { closeSync, existsSync, readFileSync, statSync, unlinkSync, writeSync, PathLike } from 'fs';
  3. import * as temp from 'temp';
  4. import { getLogger } from './loggers';
  5. const logger = getLogger('gifski');
  6. const sizeLimit = 10 * 2 ** 20;
  7. const roundToEven = (n: number) => Math.ceil(n / 2) * 2;
  8. const safeStatSync = (path: PathLike) => !existsSync(path) ? null : statSync(path);
  9. const isEmptyNullable = (path: PathLike) => !existsSync(path) ? null : statSync(path).size === 0;
  10. export default async function (data: ArrayBuffer, targetWidth?: number) {
  11. const outputFilePath = temp.path({suffix: '.gif'});
  12. temp.track();
  13. try {
  14. const inputFile = temp.openSync();
  15. writeSync(inputFile.fd, Buffer.from(data));
  16. closeSync(inputFile.fd);
  17. spawnSync('ffmpeg', [
  18. '-i',
  19. inputFile.path,
  20. '-c:a', 'copy',
  21. '-vn',
  22. inputFile.path + '.mka',
  23. ]);
  24. switch (isEmptyNullable(inputFile.path + '.mka')) {
  25. case true: unlinkSync(inputFile.path + '.mka'); break;
  26. case false: logger.info(`extracted audio to ${inputFile.path + '.mka'}`);
  27. }
  28. logger.info(`saved video file to ${inputFile.path}, starting gif conversion...`);
  29. const args = [
  30. inputFile.path,
  31. '-o',
  32. outputFilePath,
  33. '--fps',
  34. '12.5',
  35. '--quiet',
  36. '--quality',
  37. '90',
  38. ];
  39. if (typeof(targetWidth) === 'number') {
  40. args.push('--width', roundToEven(targetWidth).toString());
  41. }
  42. logger.info(` gifski ${args.join(' ')}`);
  43. const gifskiSpawn = spawn('gifski', args);
  44. const gifskiResult = new Promise<ArrayBufferLike>((resolve, reject) => {
  45. const sizeChecker = setInterval(() => {
  46. if (safeStatSync(outputFilePath)?.size > sizeLimit) gifskiSpawn.kill();
  47. }, 5000);
  48. gifskiSpawn.on('exit', () => {
  49. clearInterval(sizeChecker);
  50. if (!existsSync(outputFilePath)) reject('no file was created on exit');
  51. logger.info('gif conversion succeeded, remuxing to mkv...');
  52. spawnSync('ffmpeg', [
  53. '-i',
  54. outputFilePath,
  55. ...existsSync(inputFile.path + '.mka') ? ['-i', inputFile.path + '.mka'] : [],
  56. '-c', 'copy',
  57. outputFilePath + '.mkv',
  58. ]);
  59. if (isEmptyNullable(outputFilePath + '.mkv')) reject('remux to mkv failed');
  60. logger.info(`mkv remuxing succeeded, file path: ${outputFilePath}.mkv`);
  61. resolve(readFileSync(outputFilePath + '.mkv').buffer);
  62. });
  63. });
  64. const stderr = [];
  65. gifskiSpawn.stderr.on('data', errdata => {
  66. if (!gifskiSpawn.killed) gifskiSpawn.kill();
  67. stderr.concat(errdata);
  68. });
  69. gifskiSpawn.stderr.on('end', () => {
  70. if (stderr.length !== 0) throw Error(Buffer.concat(stderr).toString());
  71. });
  72. return await gifskiResult;
  73. } catch (error) {
  74. logger.error('error converting video to gif' + error ? `message: ${error}` : '');
  75. throw Error('error converting video to gif');
  76. } finally {
  77. temp.cleanup();
  78. }
  79. }