@gannochenko/staticstripes 0.0.1

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 (86) hide show
  1. package/.prettierrc +8 -0
  2. package/Makefile +69 -0
  3. package/dist/asset-manager.d.ts +16 -0
  4. package/dist/asset-manager.d.ts.map +1 -0
  5. package/dist/asset-manager.js +50 -0
  6. package/dist/asset-manager.js.map +1 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +257 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/container-renderer.d.ts +21 -0
  12. package/dist/container-renderer.d.ts.map +1 -0
  13. package/dist/container-renderer.js +149 -0
  14. package/dist/container-renderer.js.map +1 -0
  15. package/dist/expression-parser.d.ts +63 -0
  16. package/dist/expression-parser.d.ts.map +1 -0
  17. package/dist/expression-parser.js +145 -0
  18. package/dist/expression-parser.js.map +1 -0
  19. package/dist/ffmpeg.d.ts +375 -0
  20. package/dist/ffmpeg.d.ts.map +1 -0
  21. package/dist/ffmpeg.js +997 -0
  22. package/dist/ffmpeg.js.map +1 -0
  23. package/dist/ffprobe.d.ts +2 -0
  24. package/dist/ffprobe.d.ts.map +1 -0
  25. package/dist/ffprobe.js +31 -0
  26. package/dist/ffprobe.js.map +1 -0
  27. package/dist/html-parser.d.ts +56 -0
  28. package/dist/html-parser.d.ts.map +1 -0
  29. package/dist/html-parser.js +208 -0
  30. package/dist/html-parser.js.map +1 -0
  31. package/dist/html-project-parser.d.ts +169 -0
  32. package/dist/html-project-parser.d.ts.map +1 -0
  33. package/dist/html-project-parser.js +954 -0
  34. package/dist/html-project-parser.js.map +1 -0
  35. package/dist/index.d.ts +6 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +18 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/label-generator.d.ts +35 -0
  40. package/dist/label-generator.d.ts.map +1 -0
  41. package/dist/label-generator.js +69 -0
  42. package/dist/label-generator.js.map +1 -0
  43. package/dist/project.d.ts +29 -0
  44. package/dist/project.d.ts.map +1 -0
  45. package/dist/project.js +137 -0
  46. package/dist/project.js.map +1 -0
  47. package/dist/sample-sequences.d.ts +5 -0
  48. package/dist/sample-sequences.d.ts.map +1 -0
  49. package/dist/sample-sequences.js +199 -0
  50. package/dist/sample-sequences.js.map +1 -0
  51. package/dist/sample-streams.d.ts +2 -0
  52. package/dist/sample-streams.d.ts.map +1 -0
  53. package/dist/sample-streams.js +109 -0
  54. package/dist/sample-streams.js.map +1 -0
  55. package/dist/sequence.d.ts +21 -0
  56. package/dist/sequence.d.ts.map +1 -0
  57. package/dist/sequence.js +269 -0
  58. package/dist/sequence.js.map +1 -0
  59. package/dist/stream.d.ts +135 -0
  60. package/dist/stream.d.ts.map +1 -0
  61. package/dist/stream.js +779 -0
  62. package/dist/stream.js.map +1 -0
  63. package/dist/type.d.ts +73 -0
  64. package/dist/type.d.ts.map +1 -0
  65. package/dist/type.js +3 -0
  66. package/dist/type.js.map +1 -0
  67. package/eslint.config.js +44 -0
  68. package/package.json +50 -0
  69. package/src/asset-manager.ts +55 -0
  70. package/src/cli.ts +306 -0
  71. package/src/container-renderer.ts +190 -0
  72. package/src/expression-parser.test.ts +459 -0
  73. package/src/expression-parser.ts +199 -0
  74. package/src/ffmpeg.ts +1403 -0
  75. package/src/ffprobe.ts +29 -0
  76. package/src/html-parser.ts +221 -0
  77. package/src/html-project-parser.ts +1195 -0
  78. package/src/index.ts +9 -0
  79. package/src/label-generator.ts +74 -0
  80. package/src/project.ts +180 -0
  81. package/src/sample-sequences.ts +225 -0
  82. package/src/sample-streams.ts +142 -0
  83. package/src/sequence.ts +330 -0
  84. package/src/stream.ts +1012 -0
  85. package/src/type.ts +81 -0
  86. package/tsconfig.json +24 -0
@@ -0,0 +1,330 @@
1
+ import { AssetManager } from './asset-manager';
2
+ import {
3
+ calculateFinalValue,
4
+ ExpressionContext,
5
+ TimeData,
6
+ } from './expression-parser';
7
+
8
+ import {
9
+ AMBIENT,
10
+ FilterBuffer,
11
+ makeStream,
12
+ makeSilentStream,
13
+ makeBlankStream,
14
+ ObjectFitContainOptions,
15
+ PILLARBOX,
16
+ Stream,
17
+ VisualFilter,
18
+ } from './stream';
19
+ import { Output, SequenceDefinition } from './type';
20
+
21
+ export class Sequence {
22
+ private time: number = 0; // time is absolute
23
+
24
+ private videoStream!: Stream;
25
+ private audioStream!: Stream;
26
+
27
+ constructor(
28
+ private buf: FilterBuffer,
29
+ private definition: SequenceDefinition,
30
+ private output: Output,
31
+ private assetManager: AssetManager,
32
+ private expressionContext: ExpressionContext,
33
+ ) {}
34
+
35
+ build() {
36
+ let firstOne = true;
37
+ this.definition.fragments.forEach((fragment) => {
38
+ if (!fragment.enabled) {
39
+ return;
40
+ }
41
+
42
+ const calculatedOverlayLeft = calculateFinalValue(
43
+ fragment.overlayLeft,
44
+ this.expressionContext,
45
+ );
46
+
47
+ if (fragment.id === 'outro_message') {
48
+ debugger;
49
+ }
50
+
51
+ const timeContext: TimeData = {
52
+ start: 0,
53
+ end: 0,
54
+ duration: fragment.duration,
55
+ };
56
+
57
+ const asset = this.assetManager.getAssetByName(fragment.assetName);
58
+ if (!asset) {
59
+ return;
60
+ }
61
+
62
+ // Create video stream: use actual video if available, otherwise create blank stream
63
+ let currentVideoStream: Stream;
64
+ if (asset.hasVideo) {
65
+ currentVideoStream = makeStream(
66
+ this.assetManager.getVideoInputLabelByAssetName(fragment.assetName),
67
+ this.buf,
68
+ );
69
+ } else {
70
+ // Create blank transparent video stream for audio-only assets
71
+ currentVideoStream = makeBlankStream(
72
+ fragment.duration,
73
+ this.output.resolution.width,
74
+ this.output.resolution.height,
75
+ this.output.fps,
76
+ this.buf,
77
+ );
78
+ }
79
+
80
+ // Create audio stream: use actual audio if available, otherwise create silent stream
81
+ let currentAudioStream: Stream;
82
+ if (asset.hasAudio) {
83
+ currentAudioStream = makeStream(
84
+ this.assetManager.getAudioInputLabelByAssetName(fragment.assetName),
85
+ this.buf,
86
+ );
87
+ } else {
88
+ // Create silent audio stream matching the video duration
89
+ currentAudioStream = makeSilentStream(fragment.duration, this.buf);
90
+ }
91
+
92
+ // duration and clipping adjustment
93
+ if (fragment.trimLeft != 0 || fragment.duration < asset.duration) {
94
+ // console.log('fragment.trimLeft=' + fragment.trimLeft);
95
+ // console.log('fragment.duration=' + fragment.duration);
96
+ // console.log('asset.duration=' + asset.duration);
97
+
98
+ // Only trim video if it came from an actual source
99
+ if (asset.hasVideo) {
100
+ currentVideoStream.trim(
101
+ fragment.trimLeft,
102
+ fragment.trimLeft + fragment.duration,
103
+ );
104
+ }
105
+
106
+ // Only trim audio if it came from an actual source
107
+ if (asset.hasAudio) {
108
+ currentAudioStream.trim(
109
+ fragment.trimLeft,
110
+ fragment.trimLeft + fragment.duration,
111
+ );
112
+ }
113
+ }
114
+
115
+ // Apply visual filter early for static images (before padding/cloning)
116
+ // This is more efficient as ffmpeg processes the filter once, then clones the filtered frame
117
+ if (
118
+ asset.hasVideo &&
119
+ asset.type === 'image' &&
120
+ fragment.visualFilter
121
+ ) {
122
+ currentVideoStream.filter(fragment.visualFilter as VisualFilter);
123
+ }
124
+
125
+ if (
126
+ asset.duration === 0 &&
127
+ fragment.duration > 0 &&
128
+ asset.type === 'image'
129
+ ) {
130
+ // special case for images - extend static image to desired duration
131
+ currentVideoStream.tPad({
132
+ start: fragment.duration,
133
+ startMode: 'clone',
134
+ });
135
+ }
136
+
137
+ // stream normalization (only for actual video, not synthetic blank video)
138
+ if (asset.hasVideo) {
139
+ // fps reduction
140
+ currentVideoStream.fps(this.output.fps);
141
+
142
+ // fitting the video stream into the output frame
143
+ if (fragment.objectFit === 'cover') {
144
+ currentVideoStream.fitOutputCover(this.output.resolution);
145
+ } else {
146
+ const options: ObjectFitContainOptions = {};
147
+ if (fragment.objectFitContain === AMBIENT) {
148
+ options.ambient = {
149
+ blurStrength: fragment.objectFitContainAmbientBlurStrength,
150
+ brightness: fragment.objectFitContainAmbientBrightness,
151
+ saturation: fragment.objectFitContainAmbientSaturation,
152
+ };
153
+ } else if (fragment.objectFitContain === PILLARBOX) {
154
+ options.pillarbox = {
155
+ color: fragment.objectFitContainPillarboxColor,
156
+ };
157
+ }
158
+ currentVideoStream.fitOutputContain(this.output.resolution, options);
159
+ }
160
+ }
161
+
162
+ // adding effects if needed (only for actual video, not synthetic blank video)
163
+ if (asset.hasVideo) {
164
+ // chromakey
165
+ if (fragment.chromakey) {
166
+ currentVideoStream.chromakey({
167
+ blend: fragment.chromakeyBlend,
168
+ similarity: fragment.chromakeySimilarity,
169
+ color: fragment.chromakeyColor,
170
+ });
171
+ }
172
+
173
+ // visual filter (for video assets - images are filtered earlier before padding)
174
+ if (fragment.visualFilter && asset.type !== 'image') {
175
+ currentVideoStream.filter(fragment.visualFilter as VisualFilter);
176
+ }
177
+ }
178
+
179
+ // transitions
180
+ if (fragment.transitionIn === 'fade-in') {
181
+ currentVideoStream.fade({
182
+ fades: [
183
+ {
184
+ type: 'in',
185
+ startTime: 0,
186
+ duration: fragment.transitionInDuration,
187
+ },
188
+ ],
189
+ });
190
+ currentAudioStream.fade({
191
+ fades: [
192
+ {
193
+ type: 'in',
194
+ startTime: 0,
195
+ duration: fragment.transitionInDuration,
196
+ },
197
+ ],
198
+ });
199
+ }
200
+ if (fragment.transitionOut === 'fade-out') {
201
+ currentVideoStream.fade({
202
+ fades: [
203
+ {
204
+ type: 'out',
205
+ startTime: fragment.duration - fragment.transitionOutDuration,
206
+ duration: fragment.transitionOutDuration,
207
+ },
208
+ ],
209
+ });
210
+ currentAudioStream.fade({
211
+ fades: [
212
+ {
213
+ type: 'out',
214
+ startTime: fragment.duration - fragment.transitionOutDuration,
215
+ duration: fragment.transitionOutDuration,
216
+ },
217
+ ],
218
+ });
219
+ }
220
+
221
+ // console.log(
222
+ // 'id=' +
223
+ // fragment.id +
224
+ // ' overlay=' +
225
+ // calculatedOverlayLeft +
226
+ // ' duration=' +
227
+ // fragment.duration,
228
+ // );
229
+
230
+ // merging to the main streams
231
+ if (!firstOne) {
232
+ // attach current streams to the main ones, depending on the stated overlap
233
+ if (calculatedOverlayLeft === 0) {
234
+ // just concat with the previous one, faster
235
+ this.videoStream.concatStream(currentVideoStream);
236
+ this.audioStream.concatStream(currentAudioStream);
237
+ } else {
238
+ const otherStreamOffsetLeft = this.time + calculatedOverlayLeft;
239
+
240
+ // console.log('this.time=' + this.time);
241
+ // console.log('streamDuration=' + this.time);
242
+ // console.log('otherStreamDuration=' + fragment.duration);
243
+ // console.log('otherStreamOffsetLeft=' + otherStreamOffsetLeft);
244
+
245
+ // use overlay
246
+ this.videoStream.overlayStream(currentVideoStream, {
247
+ flipLayers: fragment.overlayZIndex < 0,
248
+ offset: {
249
+ streamDuration: this.time,
250
+ otherStreamDuration: fragment.duration,
251
+ otherStreamOffsetLeft: otherStreamOffsetLeft,
252
+ },
253
+ });
254
+ this.audioStream.overlayStream(currentAudioStream, {
255
+ offset: {
256
+ streamDuration: this.time,
257
+ otherStreamDuration: fragment.duration,
258
+ otherStreamOffsetLeft: otherStreamOffsetLeft,
259
+ },
260
+ });
261
+ }
262
+ } else {
263
+ // here an overlay can only be positive
264
+ if (calculatedOverlayLeft > 0) {
265
+ // padding video with a transparent fragment
266
+ currentVideoStream.tPad({
267
+ start: calculatedOverlayLeft,
268
+ startMode: 'add',
269
+ color: '#00000000',
270
+ });
271
+ // padding audio with a slient fragment
272
+ currentAudioStream.tPad({
273
+ start: calculatedOverlayLeft,
274
+ });
275
+ } else if (calculatedOverlayLeft < 0) {
276
+ throw new Error(
277
+ 'overlay cannot be negative for the first fragment in a sequence (fragment id = ' +
278
+ fragment.id +
279
+ ')',
280
+ );
281
+ }
282
+
283
+ // if (fragment.id === 'end_music') {
284
+ // console.log(
285
+ // this.expressionContext.fragments.get('ending_screen')!.time,
286
+ // );
287
+ // }
288
+
289
+ this.videoStream = currentVideoStream;
290
+ this.audioStream = currentAudioStream;
291
+ }
292
+
293
+ timeContext.start = this.time + calculatedOverlayLeft;
294
+ timeContext.end = this.time + fragment.duration + calculatedOverlayLeft;
295
+ this.time += fragment.duration + calculatedOverlayLeft;
296
+
297
+ this.expressionContext.fragments.set(fragment.id, {
298
+ time: timeContext,
299
+ });
300
+
301
+ // console.log('new time=' + this.time);
302
+
303
+ firstOne = false;
304
+ });
305
+ }
306
+
307
+ isEmpty() {
308
+ return !this.definition.fragments.some((fragment) => {
309
+ if (!fragment.enabled) {
310
+ return false;
311
+ }
312
+ // Check if fragment has a valid asset
313
+ const asset = this.assetManager.getAssetByName(fragment.assetName);
314
+ return !!asset;
315
+ });
316
+ }
317
+
318
+ overlayWith(sequence: Sequence) {
319
+ this.videoStream.overlayStream(sequence.getVideoStream(), {});
320
+ this.audioStream.overlayStream(sequence.getAudioStream(), {});
321
+ }
322
+
323
+ public getVideoStream(): Stream {
324
+ return this.videoStream;
325
+ }
326
+
327
+ public getAudioStream(): Stream {
328
+ return this.audioStream;
329
+ }
330
+ }