shiveshnavin commited on
Commit
08ff55b
·
1 Parent(s): 611cc41

Rene frame and render vieo working

Browse files
package-lock.json CHANGED
@@ -43,6 +43,7 @@
43
  "npm": "^9.8.1",
44
  "path": "^0.12.7",
45
  "pdfkit": "^0.15.0",
 
46
  "puppeteer": "^21.3.6",
47
  "react": "^18.0.0",
48
  "react-dom": "^18.0.0",
@@ -14557,6 +14558,36 @@
14557
  "node": ">= 6"
14558
  }
14559
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14560
  "node_modules/pluralize": {
14561
  "version": "8.0.0",
14562
  "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
@@ -28072,6 +28103,20 @@
28072
  "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
28073
  "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="
28074
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28075
  "pluralize": {
28076
  "version": "8.0.0",
28077
  "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
 
43
  "npm": "^9.8.1",
44
  "path": "^0.12.7",
45
  "pdfkit": "^0.15.0",
46
+ "playwright": "^1.60.0",
47
  "puppeteer": "^21.3.6",
48
  "react": "^18.0.0",
49
  "react-dom": "^18.0.0",
 
14558
  "node": ">= 6"
14559
  }
14560
  },
14561
+ "node_modules/playwright": {
14562
+ "version": "1.60.0",
14563
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
14564
+ "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
14565
+ "license": "Apache-2.0",
14566
+ "dependencies": {
14567
+ "playwright-core": "1.60.0"
14568
+ },
14569
+ "bin": {
14570
+ "playwright": "cli.js"
14571
+ },
14572
+ "engines": {
14573
+ "node": ">=18"
14574
+ },
14575
+ "optionalDependencies": {
14576
+ "fsevents": "2.3.2"
14577
+ }
14578
+ },
14579
+ "node_modules/playwright-core": {
14580
+ "version": "1.60.0",
14581
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
14582
+ "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
14583
+ "license": "Apache-2.0",
14584
+ "bin": {
14585
+ "playwright-core": "cli.js"
14586
+ },
14587
+ "engines": {
14588
+ "node": ">=18"
14589
+ }
14590
+ },
14591
  "node_modules/pluralize": {
14592
  "version": "8.0.0",
14593
  "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
 
28103
  "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
28104
  "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="
28105
  },
28106
+ "playwright": {
28107
+ "version": "1.60.0",
28108
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
28109
+ "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
28110
+ "requires": {
28111
+ "fsevents": "2.3.2",
28112
+ "playwright-core": "1.60.0"
28113
+ }
28114
+ },
28115
+ "playwright-core": {
28116
+ "version": "1.60.0",
28117
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
28118
+ "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA=="
28119
+ },
28120
  "pluralize": {
28121
  "version": "8.0.0",
28122
  "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
package.json CHANGED
@@ -83,6 +83,7 @@
83
  "npm": "^9.8.1",
84
  "path": "^0.12.7",
85
  "pdfkit": "^0.15.0",
 
86
  "puppeteer": "^21.3.6",
87
  "react": "^18.0.0",
88
  "react-dom": "^18.0.0",
@@ -107,4 +108,4 @@
107
  "devDependencies": {
108
  "@types/lodash": "^4.14.199"
109
  }
110
- }
 
83
  "npm": "^9.8.1",
84
  "path": "^0.12.7",
85
  "pdfkit": "^0.15.0",
86
+ "playwright": "^1.60.0",
87
  "puppeteer": "^21.3.6",
88
  "react": "^18.0.0",
89
  "react-dom": "^18.0.0",
 
108
  "devDependencies": {
109
  "@types/lodash": "^4.14.199"
110
  }
111
+ }
server-plugins/apply.js CHANGED
@@ -4,7 +4,7 @@ import { SplitRenderPlugin } from './split-render.js';
4
  import { FramesPlugin } from './frames.js';
5
  import { CaptionPlugin } from './generate-captions.js';
6
  import { CropPlugin } from './crop.js';
7
- import { FlattenPathsPlugin } from './flatten-paths.js';
8
  import { SpeedPlugin } from './speed.js';
9
  import { OverlayPlugin } from './overlay.js';
10
  import { HookPlugin } from './hook.js';
 
4
  import { FramesPlugin } from './frames.js';
5
  import { CaptionPlugin } from './generate-captions.js';
6
  import { CropPlugin } from './crop.js';
7
+ import { FlattenPathsPlugin } from './normalize-flatten-paths.js';
8
  import { SpeedPlugin } from './speed.js';
9
  import { OverlayPlugin } from './overlay.js';
10
  import { HookPlugin } from './hook.js';
server-plugins/{flatten-paths.js → normalize-flatten-paths.js} RENAMED
@@ -32,6 +32,12 @@ export class FlattenPathsPlugin extends Plugin {
32
  path.basename(b.mediaAbsPath.path)
33
  );
34
  }
 
 
 
 
 
 
35
  });
36
  }
37
 
 
32
  path.basename(b.mediaAbsPath.path)
33
  );
34
  }
35
+ if (b.bubbleHtml && b.bubbleHtml.path) {
36
+ b.bubbleHtml.path = path.join(
37
+ 'public',
38
+ path.basename(b.bubbleHtml.path)
39
+ );
40
+ }
41
  });
42
  }
43
 
utils/HtmlRender.js ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { chromium } from 'playwright';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+
5
+ export class HtmlRender {
6
+ constructor() {
7
+ this.browser = null;
8
+ this.page = null;
9
+ }
10
+
11
+ async init() {
12
+ if (!this.browser) {
13
+ this.browser = await chromium.launch();
14
+ this.page = await this.browser.newPage();
15
+ }
16
+ }
17
+
18
+ async renderFrame(html, htmlFile, outFilePath, options = {}) {
19
+ await this.init();
20
+
21
+ if (html) {
22
+ await this.page.setContent(html, { waitUntil: 'networkidle' });
23
+ } else if (htmlFile) {
24
+ await this.page.goto(`file://${htmlFile}`, { waitUntil: 'networkidle' });
25
+ } else {
26
+ throw new Error('Either html or htmlFile must be provided.');
27
+ }
28
+
29
+ await this.page.screenshot({ path: outFilePath, ...options });
30
+ }
31
+
32
+ async renderVideo(html, htmlFile, outFilePath, durationSec, options = {}) {
33
+ if (!this.browser) {
34
+ this.browser = await chromium.launch();
35
+ }
36
+ const context = await this.browser.newContext({
37
+ recordVideo: {
38
+ dir: path.dirname(outFilePath),
39
+ size: options.viewport || { width: 1080, height: 1920 }
40
+ }
41
+ });
42
+ const page = await context.newPage();
43
+ if (options.viewport) await page.setViewportSize(options.viewport);
44
+
45
+ if (html) {
46
+ await page.setContent(html, { waitUntil: 'networkidle' });
47
+ } else if (htmlFile) {
48
+ await page.goto(`file://${htmlFile}`, { waitUntil: 'networkidle' });
49
+ }
50
+
51
+ await page.waitForTimeout(durationSec * 1000);
52
+ await page.close();
53
+ await context.close();
54
+
55
+ const videoPath = await page.video().path();
56
+ if (fs.existsSync(videoPath)) {
57
+ if (fs.existsSync(outFilePath)) fs.unlinkSync(outFilePath);
58
+ fs.renameSync(videoPath, outFilePath);
59
+ }
60
+ }
61
+
62
+ async close() {
63
+ if (this.browser) {
64
+ await this.browser.close();
65
+ this.browser = null;
66
+ this.page = null;
67
+ }
68
+ }
69
+ }
utils/bubble/Bubble.js CHANGED
@@ -56,6 +56,7 @@ const resolveFontFile = (fontName, fontWeight) => {
56
 
57
 
58
  import { BaseBubbleTemplates } from './bubble-templates.js';
 
59
 
60
 
61
  class BubbleMaker {
@@ -225,10 +226,15 @@ class BubbleMaker {
225
  const meta = await FFMpegUtils.getMediaMetadata(silentVideo);
226
  const vw = meta.video.width;
227
  const vh = meta.video.height;
228
- bubbleArr.map(b => Object.assign(b, this._applyTemplate(b, vw, vh), b));
229
- const expandedBubbles = bubbleArr
 
 
 
 
 
 
230
  const hasText = expandedBubbles.some(b => b.bubbleText && b.bubbleText.text);
231
- const hasHtml = expandedBubbles.some(b => b.htmlText && b.htmlText.text);
232
 
233
 
234
  // If there are text bubbles we must go sequential (ASS subtitle filter can't be batched easily)
@@ -245,10 +251,6 @@ class BubbleMaker {
245
  return;
246
  }
247
 
248
- if (hasHtml) {
249
- this._renderHtmlBatch(silentVideo, expandedBubbles, outputPath, onLog, meta)
250
- }
251
-
252
  const hasMedia = expandedBubbles.some(b => {
253
  const mf = b.mediaAbsPaths || b.mediaAbsPath || b.mediaAbs;
254
  return !!mf;
@@ -315,9 +317,73 @@ class BubbleMaker {
315
  }
316
 
317
  async _renderHtmlBatch(silentVideo, bubbles, outputPath, onLog, meta) {
318
- // First render the bubble html to a png file
319
- // the PNG fill MUST be of the same dimenstion as the silentVideo (check the meta)
320
- // then simply add it to b.mediaAbsPaths so it gets picked up
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  }
322
 
323
  /**
 
56
 
57
 
58
  import { BaseBubbleTemplates } from './bubble-templates.js';
59
+ import { HtmlRender } from '../HtmlRender.js';
60
 
61
 
62
  class BubbleMaker {
 
226
  const meta = await FFMpegUtils.getMediaMetadata(silentVideo);
227
  const vw = meta.video.width;
228
  const vh = meta.video.height;
229
+ bubbleArr.forEach(b => Object.assign(b, this._applyTemplate(b, vw, vh)));
230
+ const expandedBubbles = bubbleArr;
231
+ const hasHtml = expandedBubbles.some(b => b.bubbleHtml && b.bubbleHtml.text);
232
+
233
+ if (hasHtml) {
234
+ await this._renderHtmlBatch(silentVideo, expandedBubbles, outputPath, onLog, meta);
235
+ }
236
+
237
  const hasText = expandedBubbles.some(b => b.bubbleText && b.bubbleText.text);
 
238
 
239
 
240
  // If there are text bubbles we must go sequential (ASS subtitle filter can't be batched easily)
 
251
  return;
252
  }
253
 
 
 
 
 
254
  const hasMedia = expandedBubbles.some(b => {
255
  const mf = b.mediaAbsPaths || b.mediaAbsPath || b.mediaAbs;
256
  return !!mf;
 
317
  }
318
 
319
  async _renderHtmlBatch(silentVideo, bubbles, outputPath, onLog, meta) {
320
+ const htmlBubbles = bubbles.filter(b => b.htmlFile || (b.bubbleHtml && b.bubbleHtml.text));
321
+ if (htmlBubbles.length === 0) return;
322
+
323
+ const htmlRenderer = new HtmlRender();
324
+ await htmlRenderer.init();
325
+
326
+ try {
327
+ const vw = meta.video.width;
328
+ const vh = meta.video.height;
329
+ await htmlRenderer.page.setViewportSize({ width: vw, height: vh });
330
+
331
+ for (const b of htmlBubbles) {
332
+ const isVideo = typeof b.durationSec === 'number' && b.durationSec > 0;
333
+ const outPath = tempPath('html_bubble', isVideo ? 'mp4' : 'png');
334
+
335
+ let finalHtml = null;
336
+ let finalFile = null;
337
+
338
+ if (b.htmlFile) {
339
+ finalFile = path.isAbsolute(b.htmlFile) ? b.htmlFile : path.join(process.cwd(), b.htmlFile);
340
+ } else {
341
+ const html = b.bubbleHtml.text;
342
+ // Wrap HTML to ensure transparent background and full viewport coverage
343
+ finalHtml = `
344
+ <!DOCTYPE html>
345
+ <html>
346
+ <head>
347
+ <style>
348
+ body, html {
349
+ margin: 0;
350
+ padding: 0;
351
+ background: transparent;
352
+ overflow: hidden;
353
+ width: 100%;
354
+ height: 100%;
355
+ }
356
+ </style>
357
+ </head>
358
+ <body>
359
+ ${html}
360
+ </body>
361
+ </html>
362
+ `;
363
+ }
364
+
365
+ if (isVideo) {
366
+ onLog && onLog(`Rendering HTML bubble to video ${outPath} for ${b.durationSec}s`);
367
+ await htmlRenderer.renderVideo(finalHtml, finalFile, outPath, b.durationSec, { viewport: { width: vw, height: vh } });
368
+ } else {
369
+ onLog && onLog(`Rendering HTML bubble to image ${outPath}`);
370
+ await htmlRenderer.renderFrame(finalHtml, finalFile, outPath, { omitBackground: true });
371
+ }
372
+
373
+ // Add the rendered asset
374
+ b.mediaAbsPaths = [outPath];
375
+
376
+ // Ensure it covers full screen since it was rendered at video dimensions
377
+ if (!b.bubbleExtra) b.bubbleExtra = {};
378
+ b.bubbleExtra.size = 'full';
379
+ b.bubbleExtra.positionX = 'left';
380
+ b.bubbleExtra.positionY = 'top';
381
+ b.bubbleExtra.paddingX = 0;
382
+ b.bubbleExtra.paddingY = 0;
383
+ }
384
+ } finally {
385
+ await htmlRenderer.close();
386
+ }
387
  }
388
 
389
  /**
utils/bubble/bubble-templates.js CHANGED
@@ -1,4 +1,13 @@
1
  // Base bubble templates (defaults). Templates prefill everything except bubbleText.text for text templates.
 
 
 
 
 
 
 
 
 
2
  export const BaseBubbleTemplates = {
3
  // Simple text box: black text on white rounded box at top-center
4
  'simple-top-center': {
@@ -9,7 +18,7 @@ export const BaseBubbleTemplates = {
9
  audioEffectFile: 'click',
10
  audioEffectDurationSec: 0.2,
11
  animExtra: {
12
- template: 'slide_down',
13
  durationSec: 0.1
14
  },
15
  bubbleText: {
@@ -33,7 +42,7 @@ export const BaseBubbleTemplates = {
33
  audioEffectDurationSec: 0.2,
34
  // default animation for the overlay itself. callers can override
35
  animExtra: {
36
- template: 'slide_down',
37
  durationSec: 0.1
38
  },
39
  bubbleExtra: {
@@ -51,7 +60,7 @@ export const BaseBubbleTemplates = {
51
  audioEffectDurationSec: 0.2,
52
  // default animation for the overlay itself. callers can override
53
  animExtra: {
54
- template: 'slide_down',
55
  durationSec: 0.1
56
  },
57
  bubbleExtra: {
 
1
  // Base bubble templates (defaults). Templates prefill everything except bubbleText.text for text templates.
2
+ /**
3
+ * | 'none'
4
+ | 'popup'
5
+ | "fade"
6
+ | "slide_left"
7
+ | "slide_right"
8
+ | "slide_up"
9
+ | "slide_down"
10
+ */
11
  export const BaseBubbleTemplates = {
12
  // Simple text box: black text on white rounded box at top-center
13
  'simple-top-center': {
 
18
  audioEffectFile: 'click',
19
  audioEffectDurationSec: 0.2,
20
  animExtra: {
21
+ template: 'fade',
22
  durationSec: 0.1
23
  },
24
  bubbleText: {
 
42
  audioEffectDurationSec: 0.2,
43
  // default animation for the overlay itself. callers can override
44
  animExtra: {
45
+ template: 'fade',
46
  durationSec: 0.1
47
  },
48
  bubbleExtra: {
 
60
  audioEffectDurationSec: 0.2,
61
  // default animation for the overlay itself. callers can override
62
  animExtra: {
63
+ template: 'fade',
64
  durationSec: 0.1
65
  },
66
  bubbleExtra: {