polotno-node 2.15.7 → 2.15.9

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.
package/dist/index.html CHANGED
@@ -8,7 +8,7 @@
8
8
  }
9
9
  </style>
10
10
  <meta name="viewport" content="width=device-width, initial-scale=1" />
11
- <script type="module" crossorigin src="./assets/index-A296L9Ae.js"></script>
11
+ <script type="module" crossorigin src="./assets/index-DAfiosIl.js"></script>
12
12
  </head>
13
13
  <body>
14
14
  <div id="root"></div>
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,34 @@ 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(`ffmpeg -y -i "${inputPath}" -c:v copy -c:a aac "${outputPath}"`, {
630
+ stdio: 'ignore',
631
+ });
632
+ };
633
+
634
+ const jsonToVideoFile = async (json, outputPath, attrs = {}) => {
608
635
  const shouldDownloadMedia =
609
636
  !hasCustomClientUrl && !(attrs && attrs.skipDownloads);
610
637
 
@@ -612,6 +639,8 @@ module.exports.createInstance = async ({
612
639
  ? await prepareLocalMediaForVideoExport(json)
613
640
  : { json, cleanup: async () => {} };
614
641
 
642
+ const writeStream = fs.createWriteStream(outputPath);
643
+
615
644
  try {
616
645
  const mimeType = await run(
617
646
  async (json, attrs) => {
@@ -639,13 +668,13 @@ module.exports.createInstance = async ({
639
668
  // to ensure text fits properly in animations. User preferences are ignored for now.
640
669
  window.config.setTextOverflow('resize');
641
670
 
642
- if (!window.loadVideoExportModule) {
671
+ if (!window.__polotnoLoadVideoExport) {
643
672
  throw new Error(
644
- 'Video export module loader is not defined in the client. Expected window.loadVideoExportModule().'
673
+ 'Video export module loader is not defined in the client. Expected window.__polotnoLoadVideoExport().'
645
674
  );
646
675
  }
647
676
 
648
- const { storeToVideo } = await window.loadVideoExportModule();
677
+ const { storeToVideo } = await window.__polotnoLoadVideoExport();
649
678
 
650
679
  // Create abort controller to cancel video render on error
651
680
  const abortController = new AbortController();
@@ -708,7 +737,7 @@ module.exports.createInstance = async ({
708
737
  for (let i = 0; i < chunk.length; i++) {
709
738
  binary += String.fromCharCode(chunk[i]);
710
739
  }
711
- await window.__polotnoSendChunk(btoa(binary));
740
+ await window.__polotnoWriteChunk(btoa(binary));
712
741
  }
713
742
 
714
743
  return videoBlob.type;
@@ -717,19 +746,61 @@ module.exports.createInstance = async ({
717
746
  {
718
747
  ...attrs,
719
748
  exposeFunctions: {
720
- __polotnoSendChunk: (chunk) => chunks.push(chunk),
749
+ __polotnoWriteChunk: (base64Chunk) => {
750
+ const buffer = Buffer.from(base64Chunk, 'base64');
751
+ writeStream.write(buffer);
752
+ },
721
753
  },
722
754
  }
723
755
  );
724
- return `data:${mimeType || 'video/mp4'};base64,${chunks.join('')}`;
756
+
757
+ writeStream.end();
758
+ await new Promise((resolve) => writeStream.on('finish', resolve));
759
+
760
+ // Convert audio to AAC if needed
761
+ if (isFfmpegAvailable()) {
762
+ const codec = getAudioCodec(outputPath);
763
+ if (codec && codec !== 'aac') {
764
+ const tempPath = outputPath + '.tmp.mp4';
765
+ convertAudioToAac(outputPath, tempPath);
766
+ await fs.promises.rename(tempPath, outputPath);
767
+ }
768
+ }
769
+
770
+ return mimeType;
725
771
  } finally {
726
772
  await prepared.cleanup();
727
773
  }
728
774
  };
729
775
 
776
+ const jsonToVideoDataURL = async (json, attrs) => {
777
+ const tempPath = path.join(
778
+ os.tmpdir(),
779
+ `polotno-video-${Date.now()}-${Math.random().toString(36).slice(2)}.mp4`
780
+ );
781
+ try {
782
+ const mimeType = await jsonToVideoFile(json, tempPath, attrs);
783
+ const buffer = await fs.promises.readFile(tempPath);
784
+ return `data:${mimeType || 'video/mp4'};base64,${buffer.toString(
785
+ 'base64'
786
+ )}`;
787
+ } finally {
788
+ await fs.promises.rm(tempPath, { force: true });
789
+ }
790
+ };
791
+
730
792
  const jsonToVideoBase64 = async (json, attrs) => {
731
- const url = await jsonToVideoDataURL(json, attrs);
732
- return url.split('base64,')[1];
793
+ const tempPath = path.join(
794
+ os.tmpdir(),
795
+ `polotno-video-${Date.now()}-${Math.random().toString(36).slice(2)}.mp4`
796
+ );
797
+ try {
798
+ await jsonToVideoFile(json, tempPath, attrs);
799
+ const buffer = await fs.promises.readFile(tempPath);
800
+ return buffer.toString('base64');
801
+ } finally {
802
+ await fs.promises.rm(tempPath, { force: true });
803
+ }
733
804
  };
734
805
 
735
806
  const instance = {
@@ -749,6 +820,7 @@ module.exports.createInstance = async ({
749
820
  jsonToGIFBase64,
750
821
  jsonToVideoDataURL,
751
822
  jsonToVideoBase64,
823
+ jsonToVideoFile,
752
824
  createPage: async () =>
753
825
  await module.exports.createPage(browser, visitPage, requestInterceptor),
754
826
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polotno-node",
3
- "version": "2.15.7",
3
+ "version": "2.15.9",
4
4
  "description": "Polotno workflow from NodeJS",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -23,26 +23,26 @@
23
23
  "browserslist": "> 0.5%, last 2 versions, not dead",
24
24
  "devDependencies": {
25
25
  "@polotno/video-export": "^0.0.11",
26
- "axios": "^1.13.2",
27
- "dotenv": "^17.2.3",
26
+ "axios": "^1.13.4",
27
+ "dotenv": "^17.2.4",
28
28
  "fluent-ffmpeg": "^2.1.3",
29
- "jspdf": "^4.0.0",
29
+ "jspdf": "^4.1.0",
30
30
  "patch-package": "^8.0.1",
31
31
  "pixelmatch": "^7.1.0",
32
32
  "pngjs": "^7.0.0",
33
- "polotno": "^2.35.2",
33
+ "polotno": "^2.36.0",
34
34
  "react": "^18.3.1",
35
35
  "react-dom": "^18.3.1",
36
36
  "vite": "^7.3.1",
37
- "vitest": "^4.0.17",
37
+ "vitest": "^4.0.18",
38
38
  "web-audio-api": "^0.2.2"
39
39
  },
40
40
  "author": "Anton Lavrenov",
41
41
  "license": "MIT",
42
42
  "dependencies": {
43
43
  "@sparticuz/chromium": "^143.0.4",
44
- "puppeteer": "24.36.0",
45
- "puppeteer-core": "24.36.0",
44
+ "puppeteer": "24.37.2",
45
+ "puppeteer-core": "24.37.2",
46
46
  "tmp": "^0.2.5"
47
47
  },
48
48
  "bugs": {