@gannochenko/staticstripes 0.0.22 → 0.0.24

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 (56) hide show
  1. package/Makefile +8 -0
  2. package/dist/app-builder.d.ts +18 -0
  3. package/dist/app-builder.d.ts.map +1 -0
  4. package/dist/app-builder.js +94 -0
  5. package/dist/app-builder.js.map +1 -0
  6. package/dist/cli/commands/filters.d.ts +3 -0
  7. package/dist/cli/commands/filters.d.ts.map +1 -0
  8. package/dist/cli/commands/filters.js +21 -0
  9. package/dist/cli/commands/filters.js.map +1 -0
  10. package/dist/cli/commands/generate.d.ts.map +1 -1
  11. package/dist/cli/commands/generate.js +6 -1
  12. package/dist/cli/commands/generate.js.map +1 -1
  13. package/dist/cli/instagram/instagram-upload-strategy.d.ts +5 -0
  14. package/dist/cli/instagram/instagram-upload-strategy.d.ts.map +1 -1
  15. package/dist/cli/instagram/instagram-upload-strategy.js +46 -3
  16. package/dist/cli/instagram/instagram-upload-strategy.js.map +1 -1
  17. package/dist/cli.js +2 -0
  18. package/dist/cli.js.map +1 -1
  19. package/dist/ffmpeg.d.ts +32 -0
  20. package/dist/ffmpeg.d.ts.map +1 -1
  21. package/dist/ffmpeg.js +118 -0
  22. package/dist/ffmpeg.js.map +1 -1
  23. package/dist/html-project-parser.d.ts +36 -1
  24. package/dist/html-project-parser.d.ts.map +1 -1
  25. package/dist/html-project-parser.js +332 -15
  26. package/dist/html-project-parser.js.map +1 -1
  27. package/dist/project.d.ts +4 -1
  28. package/dist/project.d.ts.map +1 -1
  29. package/dist/project.js +50 -1
  30. package/dist/project.js.map +1 -1
  31. package/dist/sample-sequences.d.ts.map +1 -1
  32. package/dist/sample-sequences.js +293 -0
  33. package/dist/sample-sequences.js.map +1 -1
  34. package/dist/sequence.d.ts +4 -1
  35. package/dist/sequence.d.ts.map +1 -1
  36. package/dist/sequence.js +71 -21
  37. package/dist/sequence.js.map +1 -1
  38. package/dist/stream.d.ts +17 -0
  39. package/dist/stream.d.ts.map +1 -1
  40. package/dist/stream.js +28 -0
  41. package/dist/stream.js.map +1 -1
  42. package/dist/type.d.ts +29 -2
  43. package/dist/type.d.ts.map +1 -1
  44. package/package.json +1 -1
  45. package/src/app-builder.ts +113 -0
  46. package/src/cli/commands/filters.ts +21 -0
  47. package/src/cli/commands/generate.ts +10 -1
  48. package/src/cli/instagram/instagram-upload-strategy.ts +61 -1
  49. package/src/cli.ts +2 -0
  50. package/src/ffmpeg.ts +161 -0
  51. package/src/html-project-parser.ts +410 -28
  52. package/src/project.ts +62 -0
  53. package/src/sample-sequences.ts +300 -0
  54. package/src/sequence.ts +78 -22
  55. package/src/stream.ts +50 -0
  56. package/src/type.ts +31 -2
@@ -36,8 +36,19 @@ export const getSampleSequences = (
36
36
  objectFitContainAmbientBrightness: -0.1,
37
37
  objectFitContainAmbientSaturation: 0.7,
38
38
  objectFitContainPillarboxColor: '#000000',
39
+ objectFitKenBurns: 'zoom-in' as const,
40
+ objectFitKenBurnsZoom: 30,
41
+ objectFitKenBurnsEffectDuration: 0,
42
+ objectFitKenBurnsEasing: 'linear' as const,
43
+ objectFitKenBurnsFocalX: 50,
44
+ objectFitKenBurnsFocalY: 50,
45
+ objectFitKenBurnsPanStartX: 0,
46
+ objectFitKenBurnsPanStartY: 0,
47
+ objectFitKenBurnsPanEndX: 100,
48
+ objectFitKenBurnsPanEndY: 100,
39
49
  chromakey: false,
40
50
  chromakeyBlend: 0.1,
51
+ sound: 'on' as const,
41
52
  chromakeySimilarity: 0.1,
42
53
  chromakeyColor: '#000000',
43
54
  },
@@ -59,8 +70,19 @@ export const getSampleSequences = (
59
70
  objectFitContainAmbientBrightness: -0.1,
60
71
  objectFitContainAmbientSaturation: 0.7,
61
72
  objectFitContainPillarboxColor: '#000000',
73
+ objectFitKenBurns: 'zoom-in' as const,
74
+ objectFitKenBurnsZoom: 30,
75
+ objectFitKenBurnsEffectDuration: 0,
76
+ objectFitKenBurnsEasing: 'linear' as const,
77
+ objectFitKenBurnsFocalX: 50,
78
+ objectFitKenBurnsFocalY: 50,
79
+ objectFitKenBurnsPanStartX: 0,
80
+ objectFitKenBurnsPanStartY: 0,
81
+ objectFitKenBurnsPanEndX: 100,
82
+ objectFitKenBurnsPanEndY: 100,
62
83
  chromakey: false,
63
84
  chromakeyBlend: 0.1,
85
+ sound: 'on' as const,
64
86
  chromakeySimilarity: 0.1,
65
87
  chromakeyColor: '#000000',
66
88
  },
@@ -82,10 +104,21 @@ export const getSampleSequences = (
82
104
  objectFitContainAmbientBrightness: -0.1,
83
105
  objectFitContainAmbientSaturation: 0.7,
84
106
  objectFitContainPillarboxColor: '#000000',
107
+ objectFitKenBurns: 'zoom-in' as const,
108
+ objectFitKenBurnsZoom: 30,
109
+ objectFitKenBurnsEffectDuration: 0,
110
+ objectFitKenBurnsEasing: 'linear' as const,
111
+ objectFitKenBurnsFocalX: 50,
112
+ objectFitKenBurnsFocalY: 50,
113
+ objectFitKenBurnsPanStartX: 0,
114
+ objectFitKenBurnsPanStartY: 0,
115
+ objectFitKenBurnsPanEndX: 100,
116
+ objectFitKenBurnsPanEndY: 100,
85
117
  chromakey: true,
86
118
  chromakeyBlend: 0.1,
87
119
  chromakeySimilarity: 0.1,
88
120
  chromakeyColor: '#000000',
121
+ sound: 'on' as const,
89
122
  },
90
123
  {
91
124
  id: 'f_04',
@@ -105,8 +138,19 @@ export const getSampleSequences = (
105
138
  objectFitContainAmbientBrightness: -0.1,
106
139
  objectFitContainAmbientSaturation: 0.7,
107
140
  objectFitContainPillarboxColor: '#000000',
141
+ objectFitKenBurns: 'zoom-in' as const,
142
+ objectFitKenBurnsZoom: 30,
143
+ objectFitKenBurnsEffectDuration: 0,
144
+ objectFitKenBurnsEasing: 'linear' as const,
145
+ objectFitKenBurnsFocalX: 50,
146
+ objectFitKenBurnsFocalY: 50,
147
+ objectFitKenBurnsPanStartX: 0,
148
+ objectFitKenBurnsPanStartY: 0,
149
+ objectFitKenBurnsPanEndX: 100,
150
+ objectFitKenBurnsPanEndY: 100,
108
151
  chromakey: false,
109
152
  chromakeyBlend: 0.1,
153
+ sound: 'on' as const,
110
154
  chromakeySimilarity: 0.1,
111
155
  chromakeyColor: '#000000',
112
156
  },
@@ -128,8 +172,19 @@ export const getSampleSequences = (
128
172
  objectFitContainAmbientBrightness: -0.1,
129
173
  objectFitContainAmbientSaturation: 0.7,
130
174
  objectFitContainPillarboxColor: '#000000',
175
+ objectFitKenBurns: 'zoom-in' as const,
176
+ objectFitKenBurnsZoom: 30,
177
+ objectFitKenBurnsEffectDuration: 0,
178
+ objectFitKenBurnsEasing: 'linear' as const,
179
+ objectFitKenBurnsFocalX: 50,
180
+ objectFitKenBurnsFocalY: 50,
181
+ objectFitKenBurnsPanStartX: 0,
182
+ objectFitKenBurnsPanStartY: 0,
183
+ objectFitKenBurnsPanEndX: 100,
184
+ objectFitKenBurnsPanEndY: 100,
131
185
  chromakey: false,
132
186
  chromakeyBlend: 0.1,
187
+ sound: 'on' as const,
133
188
  chromakeySimilarity: 0.1,
134
189
  chromakeyColor: '#000000',
135
190
  },
@@ -163,8 +218,19 @@ export const getSampleSequences = (
163
218
  objectFitContainAmbientBrightness: -0.1,
164
219
  objectFitContainAmbientSaturation: 0.7,
165
220
  objectFitContainPillarboxColor: '#000000',
221
+ objectFitKenBurns: 'zoom-in' as const,
222
+ objectFitKenBurnsZoom: 30,
223
+ objectFitKenBurnsEffectDuration: 0,
224
+ objectFitKenBurnsEasing: 'linear' as const,
225
+ objectFitKenBurnsFocalX: 50,
226
+ objectFitKenBurnsFocalY: 50,
227
+ objectFitKenBurnsPanStartX: 0,
228
+ objectFitKenBurnsPanStartY: 0,
229
+ objectFitKenBurnsPanEndX: 100,
230
+ objectFitKenBurnsPanEndY: 100,
166
231
  chromakey: false,
167
232
  chromakeyBlend: 0.1,
233
+ sound: 'on' as const,
168
234
  chromakeySimilarity: 0.1,
169
235
  chromakeyColor: '#000000',
170
236
  },
@@ -198,8 +264,19 @@ export const getSampleSequences = (
198
264
  objectFitContainAmbientBrightness: -0.1,
199
265
  objectFitContainAmbientSaturation: 0.7,
200
266
  objectFitContainPillarboxColor: '#000000',
267
+ objectFitKenBurns: 'zoom-in' as const,
268
+ objectFitKenBurnsZoom: 30,
269
+ objectFitKenBurnsEffectDuration: 0,
270
+ objectFitKenBurnsEasing: 'linear' as const,
271
+ objectFitKenBurnsFocalX: 50,
272
+ objectFitKenBurnsFocalY: 50,
273
+ objectFitKenBurnsPanStartX: 0,
274
+ objectFitKenBurnsPanStartY: 0,
275
+ objectFitKenBurnsPanEndX: 100,
276
+ objectFitKenBurnsPanEndY: 100,
201
277
  chromakey: false,
202
278
  chromakeyBlend: 0.1,
279
+ sound: 'on' as const,
203
280
  chromakeySimilarity: 0.1,
204
281
  chromakeyColor: '#000000',
205
282
  },
@@ -211,6 +288,229 @@ export const getSampleSequences = (
211
288
  );
212
289
  seq3.build();
213
290
 
291
+ // Ken Burns effect test sequence - using intro_image for all effect types
292
+ const seq4 = new Sequence(
293
+ buf,
294
+ {
295
+ fragments: [
296
+ // zoom-in effect with custom focal point
297
+ {
298
+ id: 'kb_zoom_in',
299
+ enabled: true,
300
+ assetName: 'intro_image',
301
+ duration: 3000,
302
+ trimLeft: 0,
303
+ overlayLeft: 0,
304
+ overlayZIndex: 1,
305
+ transitionIn: '',
306
+ transitionInDuration: 0,
307
+ transitionOut: '',
308
+ transitionOutDuration: 0,
309
+ objectFit: 'ken-burns',
310
+ objectFitContain: 'ambient',
311
+ objectFitContainAmbientBlurStrength: 25,
312
+ objectFitContainAmbientBrightness: -0.1,
313
+ objectFitContainAmbientSaturation: 0.7,
314
+ objectFitContainPillarboxColor: '#000000',
315
+ objectFitKenBurns: 'zoom-in' as const,
316
+ objectFitKenBurnsZoom: 50,
317
+ objectFitKenBurnsEffectDuration: 1000,
318
+ objectFitKenBurnsEasing: 'ease-in-out' as const,
319
+ objectFitKenBurnsFocalX: 30,
320
+ objectFitKenBurnsFocalY: 40,
321
+ objectFitKenBurnsPanStartX: 0,
322
+ objectFitKenBurnsPanStartY: 0,
323
+ objectFitKenBurnsPanEndX: 100,
324
+ objectFitKenBurnsPanEndY: 100,
325
+ chromakey: false,
326
+ chromakeyBlend: 0.1,
327
+ sound: 'on' as const,
328
+ chromakeySimilarity: 0.1,
329
+ chromakeyColor: '#000000',
330
+ },
331
+ // zoom-out effect with center focal point
332
+ {
333
+ id: 'kb_zoom_out',
334
+ enabled: true,
335
+ assetName: 'intro_image',
336
+ duration: 3000,
337
+ trimLeft: 0,
338
+ overlayLeft: 0,
339
+ overlayZIndex: 1,
340
+ transitionIn: '',
341
+ transitionInDuration: 0,
342
+ transitionOut: '',
343
+ transitionOutDuration: 0,
344
+ objectFit: 'ken-burns',
345
+ objectFitContain: 'ambient',
346
+ objectFitContainAmbientBlurStrength: 25,
347
+ objectFitContainAmbientBrightness: -0.1,
348
+ objectFitContainAmbientSaturation: 0.7,
349
+ objectFitContainPillarboxColor: '#000000',
350
+ objectFitKenBurns: 'zoom-out' as const,
351
+ objectFitKenBurnsZoom: 40,
352
+ objectFitKenBurnsEffectDuration: 2000,
353
+ objectFitKenBurnsEasing: 'ease-in' as const,
354
+ objectFitKenBurnsFocalX: 50,
355
+ objectFitKenBurnsFocalY: 50,
356
+ objectFitKenBurnsPanStartX: 0,
357
+ objectFitKenBurnsPanStartY: 0,
358
+ objectFitKenBurnsPanEndX: 100,
359
+ objectFitKenBurnsPanEndY: 100,
360
+ chromakey: false,
361
+ chromakeyBlend: 0.1,
362
+ sound: 'on' as const,
363
+ chromakeySimilarity: 0.1,
364
+ chromakeyColor: '#000000',
365
+ },
366
+ // pan-left effect
367
+ {
368
+ id: 'kb_pan_left',
369
+ enabled: true,
370
+ assetName: 'intro_image',
371
+ duration: 3000,
372
+ trimLeft: 0,
373
+ overlayLeft: 0,
374
+ overlayZIndex: 1,
375
+ transitionIn: '',
376
+ transitionInDuration: 0,
377
+ transitionOut: '',
378
+ transitionOutDuration: 0,
379
+ objectFit: 'ken-burns',
380
+ objectFitContain: 'ambient',
381
+ objectFitContainAmbientBlurStrength: 25,
382
+ objectFitContainAmbientBrightness: -0.1,
383
+ objectFitContainAmbientSaturation: 0.7,
384
+ objectFitContainPillarboxColor: '#000000',
385
+ objectFitKenBurns: 'pan-left' as const,
386
+ objectFitKenBurnsZoom: 30,
387
+ objectFitKenBurnsEffectDuration: 1500,
388
+ objectFitKenBurnsEasing: 'linear' as const,
389
+ objectFitKenBurnsFocalX: 50,
390
+ objectFitKenBurnsFocalY: 50,
391
+ objectFitKenBurnsPanStartX: 0,
392
+ objectFitKenBurnsPanStartY: 0,
393
+ objectFitKenBurnsPanEndX: 100,
394
+ objectFitKenBurnsPanEndY: 100,
395
+ chromakey: false,
396
+ chromakeyBlend: 0.1,
397
+ sound: 'on' as const,
398
+ chromakeySimilarity: 0.1,
399
+ chromakeyColor: '#000000',
400
+ },
401
+ // pan-right effect
402
+ {
403
+ id: 'kb_pan_right',
404
+ enabled: true,
405
+ assetName: 'intro_image',
406
+ duration: 3000,
407
+ trimLeft: 0,
408
+ overlayLeft: 0,
409
+ overlayZIndex: 1,
410
+ transitionIn: '',
411
+ transitionInDuration: 0,
412
+ transitionOut: '',
413
+ transitionOutDuration: 0,
414
+ objectFit: 'ken-burns',
415
+ objectFitContain: 'ambient',
416
+ objectFitContainAmbientBlurStrength: 25,
417
+ objectFitContainAmbientBrightness: -0.1,
418
+ objectFitContainAmbientSaturation: 0.7,
419
+ objectFitContainPillarboxColor: '#000000',
420
+ objectFitKenBurns: 'pan-right' as const,
421
+ objectFitKenBurnsZoom: 50,
422
+ objectFitKenBurnsEffectDuration: 1500,
423
+ objectFitKenBurnsEasing: 'ease-out' as const,
424
+ objectFitKenBurnsFocalX: 50,
425
+ objectFitKenBurnsFocalY: 50,
426
+ objectFitKenBurnsPanStartX: 0,
427
+ objectFitKenBurnsPanStartY: 0,
428
+ objectFitKenBurnsPanEndX: 100,
429
+ objectFitKenBurnsPanEndY: 100,
430
+ chromakey: false,
431
+ chromakeyBlend: 0.1,
432
+ sound: 'on' as const,
433
+ chromakeySimilarity: 0.1,
434
+ chromakeyColor: '#000000',
435
+ },
436
+ // pan-top effect
437
+ {
438
+ id: 'kb_pan_top',
439
+ enabled: true,
440
+ assetName: 'intro_image',
441
+ duration: 3000,
442
+ trimLeft: 0,
443
+ overlayLeft: 0,
444
+ overlayZIndex: 1,
445
+ transitionIn: '',
446
+ transitionInDuration: 0,
447
+ transitionOut: '',
448
+ transitionOutDuration: 0,
449
+ objectFit: 'ken-burns',
450
+ objectFitContain: 'ambient',
451
+ objectFitContainAmbientBlurStrength: 25,
452
+ objectFitContainAmbientBrightness: -0.1,
453
+ objectFitContainAmbientSaturation: 0.7,
454
+ objectFitContainPillarboxColor: '#000000',
455
+ objectFitKenBurns: 'pan-top' as const,
456
+ objectFitKenBurnsZoom: 30,
457
+ objectFitKenBurnsEffectDuration: 1500,
458
+ objectFitKenBurnsEasing: 'ease-in' as const,
459
+ objectFitKenBurnsFocalX: 50,
460
+ objectFitKenBurnsFocalY: 50,
461
+ objectFitKenBurnsPanStartX: 0,
462
+ objectFitKenBurnsPanStartY: 0,
463
+ objectFitKenBurnsPanEndX: 100,
464
+ objectFitKenBurnsPanEndY: 100,
465
+ chromakey: false,
466
+ chromakeyBlend: 0.1,
467
+ sound: 'on' as const,
468
+ chromakeySimilarity: 0.1,
469
+ chromakeyColor: '#000000',
470
+ },
471
+ // pan-bottom effect
472
+ {
473
+ id: 'kb_pan_bottom',
474
+ enabled: true,
475
+ assetName: 'intro_image',
476
+ duration: 3000,
477
+ trimLeft: 0,
478
+ overlayLeft: 0,
479
+ overlayZIndex: 1,
480
+ transitionIn: '',
481
+ transitionInDuration: 0,
482
+ transitionOut: '',
483
+ transitionOutDuration: 0,
484
+ objectFit: 'ken-burns',
485
+ objectFitContain: 'ambient',
486
+ objectFitContainAmbientBlurStrength: 25,
487
+ objectFitContainAmbientBrightness: -0.1,
488
+ objectFitContainAmbientSaturation: 0.7,
489
+ objectFitContainPillarboxColor: '#000000',
490
+ objectFitKenBurns: 'pan-bottom' as const,
491
+ objectFitKenBurnsZoom: 40,
492
+ objectFitKenBurnsEffectDuration: 1500,
493
+ objectFitKenBurnsEasing: 'ease-in-out' as const,
494
+ objectFitKenBurnsFocalX: 50,
495
+ objectFitKenBurnsFocalY: 50,
496
+ objectFitKenBurnsPanStartX: 0,
497
+ objectFitKenBurnsPanStartY: 0,
498
+ objectFitKenBurnsPanEndX: 100,
499
+ objectFitKenBurnsPanEndY: 100,
500
+ chromakey: false,
501
+ chromakeyBlend: 0.1,
502
+ sound: 'on' as const,
503
+ chromakeySimilarity: 0.1,
504
+ chromakeyColor: '#000000',
505
+ },
506
+ ],
507
+ },
508
+ output,
509
+ project.getAssetManager(),
510
+ expressionContext,
511
+ );
512
+ seq4.build();
513
+
214
514
  seq1.overlayWith(seq2);
215
515
  seq1.overlayWith(seq3);
216
516
 
package/src/sequence.ts CHANGED
@@ -16,13 +16,14 @@ import {
16
16
  Stream,
17
17
  VisualFilter,
18
18
  } from './stream';
19
- import { Output, SequenceDefinition } from './type';
19
+ import { Output, SequenceDefinition, FragmentDebugInfo } from './type';
20
20
 
21
21
  export class Sequence {
22
22
  private time: number = 0; // time is absolute
23
23
 
24
24
  private videoStream!: Stream;
25
25
  private audioStream!: Stream;
26
+ private debugInfo: FragmentDebugInfo[] = []; // Collect debug info during build
26
27
 
27
28
  constructor(
28
29
  private buf: FilterBuffer,
@@ -44,6 +45,11 @@ export class Sequence {
44
45
  this.expressionContext,
45
46
  );
46
47
 
48
+ const calculatedDuration = calculateFinalValue(
49
+ fragment.duration,
50
+ this.expressionContext,
51
+ );
52
+
47
53
  if (fragment.id === 'outro_message') {
48
54
  debugger;
49
55
  }
@@ -51,7 +57,7 @@ export class Sequence {
51
57
  const timeContext: TimeData = {
52
58
  start: 0,
53
59
  end: 0,
54
- duration: fragment.duration,
60
+ duration: calculatedDuration,
55
61
  };
56
62
 
57
63
  const asset = this.assetManager.getAssetByName(fragment.assetName);
@@ -69,7 +75,7 @@ export class Sequence {
69
75
  } else {
70
76
  // Create blank transparent video stream for audio-only assets
71
77
  currentVideoStream = makeBlankStream(
72
- fragment.duration,
78
+ calculatedDuration,
73
79
  this.output.resolution.width,
74
80
  this.output.resolution.height,
75
81
  this.output.fps,
@@ -78,40 +84,50 @@ export class Sequence {
78
84
  }
79
85
 
80
86
  // Create audio stream: use actual audio if available, otherwise create silent stream
87
+ // If fragment has -sound: off, always use silence
81
88
  let currentAudioStream: Stream;
82
- if (asset.hasAudio) {
89
+ if (fragment.sound === 'off') {
90
+ // Force silent audio when -sound: off
91
+ currentAudioStream = makeSilentStream(calculatedDuration, this.buf);
92
+ } else if (asset.hasAudio) {
83
93
  currentAudioStream = makeStream(
84
94
  this.assetManager.getAudioInputLabelByAssetName(fragment.assetName),
85
95
  this.buf,
86
96
  );
87
97
  } else {
88
98
  // Create silent audio stream matching the video duration
89
- currentAudioStream = makeSilentStream(fragment.duration, this.buf);
99
+ currentAudioStream = makeSilentStream(calculatedDuration, this.buf);
90
100
  }
91
101
 
92
102
  // duration and clipping adjustment
93
- if (fragment.trimLeft != 0 || fragment.duration < asset.duration) {
103
+ if (fragment.trimLeft != 0 || calculatedDuration < asset.duration) {
94
104
  // console.log('fragment.trimLeft=' + fragment.trimLeft);
95
- // console.log('fragment.duration=' + fragment.duration);
105
+ // console.log('fragment.duration=' + calculatedDuration);
96
106
  // console.log('asset.duration=' + asset.duration);
97
107
 
98
108
  // Only trim video if it came from an actual source
99
109
  if (asset.hasVideo) {
100
110
  currentVideoStream.trim(
101
111
  fragment.trimLeft,
102
- fragment.trimLeft + fragment.duration,
112
+ fragment.trimLeft + calculatedDuration,
103
113
  );
104
114
  }
105
115
 
106
- // Only trim audio if it came from an actual source
107
- if (asset.hasAudio) {
116
+ // Only trim audio if it came from an actual source AND sound is not off
117
+ if (asset.hasAudio && fragment.sound !== 'off') {
108
118
  currentAudioStream.trim(
109
119
  fragment.trimLeft,
110
- fragment.trimLeft + fragment.duration,
120
+ fragment.trimLeft + calculatedDuration,
111
121
  );
112
122
  }
113
123
  }
114
124
 
125
+ // Convert deprecated JPEG pixel format (yuvj420p) to standard yuv420p early
126
+ // This prevents swscaler warnings from appearing in all subsequent filters
127
+ if (asset.hasVideo && asset.type === 'image') {
128
+ currentVideoStream.convertPixelFormat('yuv420p');
129
+ }
130
+
115
131
  // Apply visual filter early for static images (before padding/cloning)
116
132
  // This is more efficient as ffmpeg processes the filter once, then clones the filtered frame
117
133
  if (
@@ -124,12 +140,14 @@ export class Sequence {
124
140
 
125
141
  if (
126
142
  asset.duration === 0 &&
127
- fragment.duration > 0 &&
128
- asset.type === 'image'
143
+ calculatedDuration > 0 &&
144
+ asset.type === 'image' &&
145
+ fragment.objectFit !== 'ken-burns'
129
146
  ) {
130
147
  // special case for images - extend static image to desired duration
148
+ // Skip tpad for Ken Burns - zoompan will generate the frames
131
149
  currentVideoStream.tPad({
132
- start: fragment.duration,
150
+ start: calculatedDuration,
133
151
  startMode: 'clone',
134
152
  });
135
153
  }
@@ -140,7 +158,25 @@ export class Sequence {
140
158
  currentVideoStream.fps(this.output.fps);
141
159
 
142
160
  // fitting the video stream into the output frame
143
- if (fragment.objectFit === 'cover') {
161
+ if (fragment.objectFit === 'ken-burns') {
162
+ // Ken Burns effect (zoom/pan)
163
+ currentVideoStream.kenBurns({
164
+ effect: fragment.objectFitKenBurns,
165
+ zoom: fragment.objectFitKenBurnsZoom,
166
+ effectDuration: fragment.objectFitKenBurnsEffectDuration,
167
+ fragmentDuration: calculatedDuration,
168
+ easing: fragment.objectFitKenBurnsEasing,
169
+ width: this.output.resolution.width,
170
+ height: this.output.resolution.height,
171
+ fps: this.output.fps,
172
+ focalX: fragment.objectFitKenBurnsFocalX,
173
+ focalY: fragment.objectFitKenBurnsFocalY,
174
+ panStartX: fragment.objectFitKenBurnsPanStartX,
175
+ panStartY: fragment.objectFitKenBurnsPanStartY,
176
+ panEndX: fragment.objectFitKenBurnsPanEndX,
177
+ panEndY: fragment.objectFitKenBurnsPanEndY,
178
+ });
179
+ } else if (fragment.objectFit === 'cover') {
144
180
  currentVideoStream.fitOutputCover(this.output.resolution);
145
181
  } else {
146
182
  const options: ObjectFitContainOptions = {};
@@ -202,7 +238,7 @@ export class Sequence {
202
238
  fades: [
203
239
  {
204
240
  type: 'out',
205
- startTime: fragment.duration - fragment.transitionOutDuration,
241
+ startTime: calculatedDuration - fragment.transitionOutDuration,
206
242
  duration: fragment.transitionOutDuration,
207
243
  },
208
244
  ],
@@ -211,7 +247,7 @@ export class Sequence {
211
247
  fades: [
212
248
  {
213
249
  type: 'out',
214
- startTime: fragment.duration - fragment.transitionOutDuration,
250
+ startTime: calculatedDuration - fragment.transitionOutDuration,
215
251
  duration: fragment.transitionOutDuration,
216
252
  },
217
253
  ],
@@ -239,7 +275,7 @@ export class Sequence {
239
275
 
240
276
  // console.log('this.time=' + this.time);
241
277
  // console.log('streamDuration=' + this.time);
242
- // console.log('otherStreamDuration=' + fragment.duration);
278
+ // console.log('otherStreamDuration=' + calculatedDuration);
243
279
  // console.log('otherStreamOffsetLeft=' + otherStreamOffsetLeft);
244
280
 
245
281
  // use overlay
@@ -247,14 +283,14 @@ export class Sequence {
247
283
  flipLayers: fragment.overlayZIndex < 0,
248
284
  offset: {
249
285
  streamDuration: this.time,
250
- otherStreamDuration: fragment.duration,
286
+ otherStreamDuration: calculatedDuration,
251
287
  otherStreamOffsetLeft: otherStreamOffsetLeft,
252
288
  },
253
289
  });
254
290
  this.audioStream.overlayStream(currentAudioStream, {
255
291
  offset: {
256
292
  streamDuration: this.time,
257
- otherStreamDuration: fragment.duration,
293
+ otherStreamDuration: calculatedDuration,
258
294
  otherStreamOffsetLeft: otherStreamOffsetLeft,
259
295
  },
260
296
  });
@@ -291,13 +327,25 @@ export class Sequence {
291
327
  }
292
328
 
293
329
  timeContext.start = this.time + calculatedOverlayLeft;
294
- timeContext.end = this.time + fragment.duration + calculatedOverlayLeft;
295
- this.time += fragment.duration + calculatedOverlayLeft;
330
+ timeContext.end = this.time + calculatedDuration + calculatedOverlayLeft;
331
+ this.time += calculatedDuration + calculatedOverlayLeft;
296
332
 
297
333
  this.expressionContext.fragments.set(fragment.id, {
298
334
  time: timeContext,
299
335
  });
300
336
 
337
+ // Collect debug info
338
+ this.debugInfo.push({
339
+ id: fragment.id,
340
+ assetName: fragment.assetName,
341
+ startTime: timeContext.start,
342
+ endTime: timeContext.end,
343
+ duration: calculatedDuration,
344
+ trimLeft: fragment.trimLeft,
345
+ overlayLeft: calculatedOverlayLeft,
346
+ enabled: fragment.enabled,
347
+ });
348
+
301
349
  // console.log('new time=' + this.time);
302
350
 
303
351
  firstOne = false;
@@ -327,4 +375,12 @@ export class Sequence {
327
375
  public getAudioStream(): Stream {
328
376
  return this.audioStream;
329
377
  }
378
+
379
+ public getDebugInfo(): FragmentDebugInfo[] {
380
+ return this.debugInfo;
381
+ }
382
+
383
+ public getTotalDuration(): number {
384
+ return this.time;
385
+ }
330
386
  }
package/src/stream.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  Millisecond,
5
5
  makeNull,
6
6
  makeFps,
7
+ makeFormat,
7
8
  makeTranspose,
8
9
  makeTrim,
9
10
  makeTPad,
@@ -17,6 +18,7 @@ import {
17
18
  makeOverlay,
18
19
  makeEq,
19
20
  makeChromakey,
21
+ makeKenBurns,
20
22
  makeConcat,
21
23
  makeFade,
22
24
  makeAmix,
@@ -311,6 +313,45 @@ export class Stream {
311
313
  return this;
312
314
  }
313
315
 
316
+ public kenBurns(parameters: {
317
+ effect: 'zoom-in' | 'zoom-out' | 'pan-left' | 'pan-right' | 'pan-top' | 'pan-bottom';
318
+ zoom: number;
319
+ effectDuration: number;
320
+ fragmentDuration: number;
321
+ easing: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out';
322
+ width: number;
323
+ height: number;
324
+ fps: number;
325
+ focalX?: number;
326
+ focalY?: number;
327
+ panStartX?: number;
328
+ panStartY?: number;
329
+ panEndX?: number;
330
+ panEndY?: number;
331
+ }): Stream {
332
+ // Apply Ken Burns effect
333
+ const kenBurnsRes = makeKenBurns([this.looseEnd], {
334
+ effect: parameters.effect,
335
+ zoom: parameters.zoom,
336
+ effectDuration: parameters.effectDuration,
337
+ fragmentDuration: parameters.fragmentDuration,
338
+ easing: parameters.easing,
339
+ width: parameters.width,
340
+ height: parameters.height,
341
+ fps: parameters.fps,
342
+ focalX: parameters.focalX,
343
+ focalY: parameters.focalY,
344
+ panStartX: parameters.panStartX,
345
+ panStartY: parameters.panStartY,
346
+ panEndX: parameters.panEndX,
347
+ panEndY: parameters.panEndY,
348
+ });
349
+ this.looseEnd = kenBurnsRes.outputs[0];
350
+ this.buf.append(kenBurnsRes);
351
+
352
+ return this;
353
+ }
354
+
314
355
  public fps(value: number): Stream {
315
356
  const res = makeFps([this.looseEnd], value);
316
357
  this.looseEnd = res.outputs[0];
@@ -320,6 +361,15 @@ export class Stream {
320
361
  return this;
321
362
  }
322
363
 
364
+ public convertPixelFormat(format: string): Stream {
365
+ const res = makeFormat([this.looseEnd], format);
366
+ this.looseEnd = res.outputs[0];
367
+
368
+ this.buf.append(res);
369
+
370
+ return this;
371
+ }
372
+
323
373
  public blur(strength: number): Stream {
324
374
  const res = makeGblur([this.looseEnd], {
325
375
  sigma: strength,