@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.
- package/.prettierrc +8 -0
- package/Makefile +69 -0
- package/dist/asset-manager.d.ts +16 -0
- package/dist/asset-manager.d.ts.map +1 -0
- package/dist/asset-manager.js +50 -0
- package/dist/asset-manager.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +257 -0
- package/dist/cli.js.map +1 -0
- package/dist/container-renderer.d.ts +21 -0
- package/dist/container-renderer.d.ts.map +1 -0
- package/dist/container-renderer.js +149 -0
- package/dist/container-renderer.js.map +1 -0
- package/dist/expression-parser.d.ts +63 -0
- package/dist/expression-parser.d.ts.map +1 -0
- package/dist/expression-parser.js +145 -0
- package/dist/expression-parser.js.map +1 -0
- package/dist/ffmpeg.d.ts +375 -0
- package/dist/ffmpeg.d.ts.map +1 -0
- package/dist/ffmpeg.js +997 -0
- package/dist/ffmpeg.js.map +1 -0
- package/dist/ffprobe.d.ts +2 -0
- package/dist/ffprobe.d.ts.map +1 -0
- package/dist/ffprobe.js +31 -0
- package/dist/ffprobe.js.map +1 -0
- package/dist/html-parser.d.ts +56 -0
- package/dist/html-parser.d.ts.map +1 -0
- package/dist/html-parser.js +208 -0
- package/dist/html-parser.js.map +1 -0
- package/dist/html-project-parser.d.ts +169 -0
- package/dist/html-project-parser.d.ts.map +1 -0
- package/dist/html-project-parser.js +954 -0
- package/dist/html-project-parser.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/label-generator.d.ts +35 -0
- package/dist/label-generator.d.ts.map +1 -0
- package/dist/label-generator.js +69 -0
- package/dist/label-generator.js.map +1 -0
- package/dist/project.d.ts +29 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +137 -0
- package/dist/project.js.map +1 -0
- package/dist/sample-sequences.d.ts +5 -0
- package/dist/sample-sequences.d.ts.map +1 -0
- package/dist/sample-sequences.js +199 -0
- package/dist/sample-sequences.js.map +1 -0
- package/dist/sample-streams.d.ts +2 -0
- package/dist/sample-streams.d.ts.map +1 -0
- package/dist/sample-streams.js +109 -0
- package/dist/sample-streams.js.map +1 -0
- package/dist/sequence.d.ts +21 -0
- package/dist/sequence.d.ts.map +1 -0
- package/dist/sequence.js +269 -0
- package/dist/sequence.js.map +1 -0
- package/dist/stream.d.ts +135 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/stream.js +779 -0
- package/dist/stream.js.map +1 -0
- package/dist/type.d.ts +73 -0
- package/dist/type.d.ts.map +1 -0
- package/dist/type.js +3 -0
- package/dist/type.js.map +1 -0
- package/eslint.config.js +44 -0
- package/package.json +50 -0
- package/src/asset-manager.ts +55 -0
- package/src/cli.ts +306 -0
- package/src/container-renderer.ts +190 -0
- package/src/expression-parser.test.ts +459 -0
- package/src/expression-parser.ts +199 -0
- package/src/ffmpeg.ts +1403 -0
- package/src/ffprobe.ts +29 -0
- package/src/html-parser.ts +221 -0
- package/src/html-project-parser.ts +1195 -0
- package/src/index.ts +9 -0
- package/src/label-generator.ts +74 -0
- package/src/project.ts +180 -0
- package/src/sample-sequences.ts +225 -0
- package/src/sample-streams.ts +142 -0
- package/src/sequence.ts +330 -0
- package/src/stream.ts +1012 -0
- package/src/type.ts +81 -0
- package/tsconfig.json +24 -0
package/src/sequence.ts
ADDED
|
@@ -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
|
+
}
|