polotno-node 2.15.7 → 2.15.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/instance.js +78 -7
  2. package/package.json +1 -1
package/instance.js CHANGED
@@ -2,6 +2,7 @@ const path = require('path');
2
2
  const fs = require('fs');
3
3
  const os = require('os');
4
4
  const crypto = require('crypto');
5
+ const { execSync } = require('child_process');
5
6
  const { pathToFileURL } = require('url');
6
7
  const { Readable } = require('stream');
7
8
  const { pipeline } = require('stream/promises');
@@ -603,8 +604,35 @@ module.exports.createInstance = async ({
603
604
  return blob;
604
605
  };
605
606
 
606
- const jsonToVideoDataURL = async (json, attrs) => {
607
- const chunks = [];
607
+ const isFfmpegAvailable = () => {
608
+ try {
609
+ execSync('ffmpeg -version', { stdio: 'ignore' });
610
+ return true;
611
+ } catch {
612
+ return false;
613
+ }
614
+ };
615
+
616
+ const getAudioCodec = (filePath) => {
617
+ try {
618
+ const result = execSync(
619
+ `ffprobe -v error -select_streams a:0 -show_entries stream=codec_name -of csv=p=0 "${filePath}"`,
620
+ { encoding: 'utf8' }
621
+ );
622
+ return result.trim();
623
+ } catch {
624
+ return null;
625
+ }
626
+ };
627
+
628
+ const convertAudioToAac = (inputPath, outputPath) => {
629
+ execSync(
630
+ `ffmpeg -y -i "${inputPath}" -c:v copy -c:a aac "${outputPath}"`,
631
+ { stdio: 'ignore' }
632
+ );
633
+ };
634
+
635
+ const jsonToVideoFile = async (json, outputPath, attrs = {}) => {
608
636
  const shouldDownloadMedia =
609
637
  !hasCustomClientUrl && !(attrs && attrs.skipDownloads);
610
638
 
@@ -612,6 +640,8 @@ module.exports.createInstance = async ({
612
640
  ? await prepareLocalMediaForVideoExport(json)
613
641
  : { json, cleanup: async () => {} };
614
642
 
643
+ const writeStream = fs.createWriteStream(outputPath);
644
+
615
645
  try {
616
646
  const mimeType = await run(
617
647
  async (json, attrs) => {
@@ -708,7 +738,7 @@ module.exports.createInstance = async ({
708
738
  for (let i = 0; i < chunk.length; i++) {
709
739
  binary += String.fromCharCode(chunk[i]);
710
740
  }
711
- await window.__polotnoSendChunk(btoa(binary));
741
+ await window.__polotnoWriteChunk(btoa(binary));
712
742
  }
713
743
 
714
744
  return videoBlob.type;
@@ -717,19 +747,59 @@ module.exports.createInstance = async ({
717
747
  {
718
748
  ...attrs,
719
749
  exposeFunctions: {
720
- __polotnoSendChunk: (chunk) => chunks.push(chunk),
750
+ __polotnoWriteChunk: (base64Chunk) => {
751
+ const buffer = Buffer.from(base64Chunk, 'base64');
752
+ writeStream.write(buffer);
753
+ },
721
754
  },
722
755
  }
723
756
  );
724
- return `data:${mimeType || 'video/mp4'};base64,${chunks.join('')}`;
757
+
758
+ writeStream.end();
759
+ await new Promise((resolve) => writeStream.on('finish', resolve));
760
+
761
+ // Convert audio to AAC if needed
762
+ if (isFfmpegAvailable()) {
763
+ const codec = getAudioCodec(outputPath);
764
+ if (codec && codec !== 'aac') {
765
+ const tempPath = outputPath + '.tmp.mp4';
766
+ convertAudioToAac(outputPath, tempPath);
767
+ await fs.promises.rename(tempPath, outputPath);
768
+ }
769
+ }
770
+
771
+ return mimeType;
725
772
  } finally {
726
773
  await prepared.cleanup();
727
774
  }
728
775
  };
729
776
 
777
+ const jsonToVideoDataURL = async (json, attrs) => {
778
+ const tempPath = path.join(
779
+ os.tmpdir(),
780
+ `polotno-video-${Date.now()}-${Math.random().toString(36).slice(2)}.mp4`
781
+ );
782
+ try {
783
+ const mimeType = await jsonToVideoFile(json, tempPath, attrs);
784
+ const buffer = await fs.promises.readFile(tempPath);
785
+ return `data:${mimeType || 'video/mp4'};base64,${buffer.toString('base64')}`;
786
+ } finally {
787
+ await fs.promises.rm(tempPath, { force: true });
788
+ }
789
+ };
790
+
730
791
  const jsonToVideoBase64 = async (json, attrs) => {
731
- const url = await jsonToVideoDataURL(json, attrs);
732
- return url.split('base64,')[1];
792
+ const tempPath = path.join(
793
+ os.tmpdir(),
794
+ `polotno-video-${Date.now()}-${Math.random().toString(36).slice(2)}.mp4`
795
+ );
796
+ try {
797
+ await jsonToVideoFile(json, tempPath, attrs);
798
+ const buffer = await fs.promises.readFile(tempPath);
799
+ return buffer.toString('base64');
800
+ } finally {
801
+ await fs.promises.rm(tempPath, { force: true });
802
+ }
733
803
  };
734
804
 
735
805
  const instance = {
@@ -749,6 +819,7 @@ module.exports.createInstance = async ({
749
819
  jsonToGIFBase64,
750
820
  jsonToVideoDataURL,
751
821
  jsonToVideoBase64,
822
+ jsonToVideoFile,
752
823
  createPage: async () =>
753
824
  await module.exports.createPage(browser, visitPage, requestInterceptor),
754
825
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polotno-node",
3
- "version": "2.15.7",
3
+ "version": "2.15.8",
4
4
  "description": "Polotno workflow from NodeJS",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",