@meframe/core 0.0.10 → 0.0.12
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/Meframe.d.ts.map +1 -1
- package/dist/Meframe.js +3 -0
- package/dist/Meframe.js.map +1 -1
- package/dist/cache/CacheManager.d.ts +12 -0
- package/dist/cache/CacheManager.d.ts.map +1 -1
- package/dist/cache/CacheManager.js +16 -4
- package/dist/cache/CacheManager.js.map +1 -1
- package/dist/controllers/PlaybackController.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.js +3 -0
- package/dist/controllers/PlaybackController.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/model/CompositionModel.d.ts +2 -1
- package/dist/model/CompositionModel.d.ts.map +1 -1
- package/dist/model/CompositionModel.js +80 -11
- package/dist/model/CompositionModel.js.map +1 -1
- package/dist/model/types.d.ts +30 -1
- package/dist/model/types.d.ts.map +1 -1
- package/dist/orchestrator/CompositionPlanner.d.ts.map +1 -1
- package/dist/orchestrator/CompositionPlanner.js +18 -2
- package/dist/orchestrator/CompositionPlanner.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +25 -6
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoClipSession.d.ts +2 -1
- package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoClipSession.js +16 -1
- package/dist/orchestrator/VideoClipSession.js.map +1 -1
- package/dist/orchestrator/types.d.ts +1 -0
- package/dist/orchestrator/types.d.ts.map +1 -1
- package/dist/stages/compose/LayerRenderer.d.ts +2 -0
- package/dist/stages/compose/LayerRenderer.d.ts.map +1 -1
- package/dist/stages/compose/instructions.d.ts +24 -1
- package/dist/stages/compose/instructions.d.ts.map +1 -1
- package/dist/stages/compose/types.d.ts +2 -0
- package/dist/stages/compose/types.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.d.ts +8 -0
- package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.js +74 -12
- package/dist/stages/load/ResourceLoader.js.map +1 -1
- package/dist/stages/load/types.d.ts +1 -0
- package/dist/stages/load/types.d.ts.map +1 -1
- package/dist/stages/mux/MP4Muxer.js +1 -1
- package/dist/stages/mux/MP4Muxer.js.map +1 -1
- package/dist/stages/mux/MuxManager.d.ts.map +1 -1
- package/dist/stages/mux/MuxManager.js +36 -5
- package/dist/stages/mux/MuxManager.js.map +1 -1
- package/dist/utils/animation-utils.d.ts +16 -0
- package/dist/utils/animation-utils.d.ts.map +1 -0
- package/dist/utils/image-utils.d.ts +5 -0
- package/dist/utils/image-utils.d.ts.map +1 -0
- package/dist/utils/image-utils.js +32 -0
- package/dist/utils/image-utils.js.map +1 -0
- package/dist/workers/stages/compose/video-compose.worker.js +263 -85
- package/dist/workers/stages/compose/video-compose.worker.js.map +1 -1
- package/package.json +1 -1
|
@@ -12,30 +12,6 @@ function frameDurationFromFps(fps) {
|
|
|
12
12
|
const duration = MICROSECONDS_PER_SECOND / normalized;
|
|
13
13
|
return Math.max(Math.round(duration), 1);
|
|
14
14
|
}
|
|
15
|
-
function frameIndexFromTimestamp(baseTimestampUs, timestampUs, fps, strategy = "nearest") {
|
|
16
|
-
const frameDurationUs = frameDurationFromFps(fps);
|
|
17
|
-
if (frameDurationUs <= 0) {
|
|
18
|
-
return 0;
|
|
19
|
-
}
|
|
20
|
-
const delta = timestampUs - baseTimestampUs;
|
|
21
|
-
const rawIndex = delta / frameDurationUs;
|
|
22
|
-
if (!Number.isFinite(rawIndex)) {
|
|
23
|
-
return 0;
|
|
24
|
-
}
|
|
25
|
-
switch (strategy) {
|
|
26
|
-
case "floor":
|
|
27
|
-
return Math.floor(rawIndex);
|
|
28
|
-
case "ceil":
|
|
29
|
-
return Math.ceil(rawIndex);
|
|
30
|
-
default:
|
|
31
|
-
return Math.round(rawIndex);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
function quantizeTimestampToFrame(timestampUs, baseTimestampUs, fps, strategy = "nearest") {
|
|
35
|
-
const frameDurationUs = frameDurationFromFps(fps);
|
|
36
|
-
const index = frameIndexFromTimestamp(baseTimestampUs, timestampUs, fps, strategy);
|
|
37
|
-
return baseTimestampUs + index * frameDurationUs;
|
|
38
|
-
}
|
|
39
15
|
class LayerRenderer {
|
|
40
16
|
ctx;
|
|
41
17
|
width;
|
|
@@ -63,7 +39,8 @@ class LayerRenderer {
|
|
|
63
39
|
this.ctx.globalCompositeOperation = layer.blendMode;
|
|
64
40
|
}
|
|
65
41
|
if (layer.transform) {
|
|
66
|
-
this.
|
|
42
|
+
const layerDimensions = this.getLayerDimensions(layer);
|
|
43
|
+
this.applyTransform(layer.transform, layerDimensions);
|
|
67
44
|
}
|
|
68
45
|
switch (layer.type) {
|
|
69
46
|
case "video":
|
|
@@ -83,9 +60,60 @@ class LayerRenderer {
|
|
|
83
60
|
this.ctx.restore();
|
|
84
61
|
}
|
|
85
62
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
63
|
+
parseDimension(value, canvasSize) {
|
|
64
|
+
if (value === void 0) return void 0;
|
|
65
|
+
if (typeof value === "number") return value;
|
|
66
|
+
const strValue = value;
|
|
67
|
+
if (strValue.includes("%")) {
|
|
68
|
+
const numValue = parseFloat(strValue);
|
|
69
|
+
return isNaN(numValue) ? void 0 : numValue / 100 * canvasSize;
|
|
70
|
+
}
|
|
71
|
+
const parsed = parseFloat(strValue);
|
|
72
|
+
return isNaN(parsed) ? void 0 : parsed;
|
|
73
|
+
}
|
|
74
|
+
getLayerDimensions(layer) {
|
|
75
|
+
if (layer.type === "image") {
|
|
76
|
+
const imageLayer = layer;
|
|
77
|
+
if (imageLayer.source) {
|
|
78
|
+
const imgWidth = imageLayer.source.width;
|
|
79
|
+
const imgHeight = imageLayer.source.height;
|
|
80
|
+
const isAttachment = !!imageLayer.attachmentId;
|
|
81
|
+
if (isAttachment) {
|
|
82
|
+
const targetWidthRaw = imageLayer.targetWidth;
|
|
83
|
+
const targetHeightRaw = imageLayer.targetHeight;
|
|
84
|
+
const targetWidth = this.parseDimension(targetWidthRaw, this.width);
|
|
85
|
+
const targetHeight = this.parseDimension(targetHeightRaw, this.height);
|
|
86
|
+
if (targetWidth && targetHeight) {
|
|
87
|
+
return { width: targetWidth, height: targetHeight };
|
|
88
|
+
} else if (targetWidth) {
|
|
89
|
+
return {
|
|
90
|
+
width: targetWidth,
|
|
91
|
+
height: imgHeight / imgWidth * targetWidth
|
|
92
|
+
};
|
|
93
|
+
} else if (targetHeight) {
|
|
94
|
+
return {
|
|
95
|
+
width: imgWidth / imgHeight * targetHeight,
|
|
96
|
+
height: targetHeight
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { width: imgWidth, height: imgHeight };
|
|
101
|
+
}
|
|
102
|
+
} else if (layer.type === "video") {
|
|
103
|
+
const videoLayer = layer;
|
|
104
|
+
const videoFrame = videoLayer.videoFrame;
|
|
105
|
+
return {
|
|
106
|
+
width: videoFrame.displayWidth || videoFrame.codedWidth,
|
|
107
|
+
height: videoFrame.displayHeight || videoFrame.codedHeight
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return { width: this.width, height: this.height };
|
|
111
|
+
}
|
|
112
|
+
applyTransform(transform, layerDimensions) {
|
|
113
|
+
const anchorX = transform.anchorX ?? 0.5;
|
|
114
|
+
const anchorY = transform.anchorY ?? 0.5;
|
|
115
|
+
const centerX = layerDimensions.width * anchorX;
|
|
116
|
+
const centerY = layerDimensions.height * anchorY;
|
|
89
117
|
this.ctx.translate(transform.x + centerX, transform.y + centerY);
|
|
90
118
|
if (transform.rotation) {
|
|
91
119
|
this.ctx.rotate(transform.rotation);
|
|
@@ -138,6 +166,33 @@ class LayerRenderer {
|
|
|
138
166
|
if (!source) {
|
|
139
167
|
return;
|
|
140
168
|
}
|
|
169
|
+
const isAttachment = !!layer.attachmentId;
|
|
170
|
+
const imgWidth = source.width;
|
|
171
|
+
const imgHeight = source.height;
|
|
172
|
+
let renderWidth;
|
|
173
|
+
let renderHeight;
|
|
174
|
+
if (isAttachment) {
|
|
175
|
+
const targetWidthRaw = layer.targetWidth;
|
|
176
|
+
const targetHeightRaw = layer.targetHeight;
|
|
177
|
+
const targetWidth = this.parseDimension(targetWidthRaw, this.width);
|
|
178
|
+
const targetHeight = this.parseDimension(targetHeightRaw, this.height);
|
|
179
|
+
if (targetWidth && targetHeight) {
|
|
180
|
+
renderWidth = targetWidth;
|
|
181
|
+
renderHeight = targetHeight;
|
|
182
|
+
} else if (targetWidth) {
|
|
183
|
+
renderWidth = targetWidth;
|
|
184
|
+
renderHeight = imgHeight / imgWidth * targetWidth;
|
|
185
|
+
} else if (targetHeight) {
|
|
186
|
+
renderHeight = targetHeight;
|
|
187
|
+
renderWidth = imgWidth / imgHeight * targetHeight;
|
|
188
|
+
} else {
|
|
189
|
+
renderWidth = imgWidth;
|
|
190
|
+
renderHeight = imgHeight;
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
renderWidth = this.width;
|
|
194
|
+
renderHeight = this.height;
|
|
195
|
+
}
|
|
141
196
|
if (crop) {
|
|
142
197
|
this.ctx.drawImage(
|
|
143
198
|
source,
|
|
@@ -147,11 +202,11 @@ class LayerRenderer {
|
|
|
147
202
|
crop.height,
|
|
148
203
|
0,
|
|
149
204
|
0,
|
|
150
|
-
|
|
151
|
-
|
|
205
|
+
renderWidth,
|
|
206
|
+
renderHeight
|
|
152
207
|
);
|
|
153
208
|
} else {
|
|
154
|
-
this.ctx.drawImage(source, 0, 0,
|
|
209
|
+
this.ctx.drawImage(source, 0, 0, renderWidth, renderHeight);
|
|
155
210
|
}
|
|
156
211
|
}
|
|
157
212
|
}
|
|
@@ -834,9 +889,87 @@ class VideoComposer {
|
|
|
834
889
|
this.filterProcessor.clearCache();
|
|
835
890
|
}
|
|
836
891
|
}
|
|
892
|
+
function interpolateKeyframes(keyframes, timeUs) {
|
|
893
|
+
const defaultTransform = {
|
|
894
|
+
x: 0,
|
|
895
|
+
y: 0,
|
|
896
|
+
scaleX: 1,
|
|
897
|
+
scaleY: 1,
|
|
898
|
+
rotation: 0,
|
|
899
|
+
anchorX: 0.5,
|
|
900
|
+
anchorY: 0.5
|
|
901
|
+
};
|
|
902
|
+
if (keyframes.length === 0) {
|
|
903
|
+
return { transform: defaultTransform };
|
|
904
|
+
}
|
|
905
|
+
const firstFrame = keyframes[0];
|
|
906
|
+
const lastFrame = keyframes[keyframes.length - 1];
|
|
907
|
+
if (timeUs <= firstFrame.time) {
|
|
908
|
+
return {
|
|
909
|
+
transform: { ...defaultTransform, ...firstFrame.transform },
|
|
910
|
+
opacity: firstFrame.opacity
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
if (timeUs >= lastFrame.time) {
|
|
914
|
+
return {
|
|
915
|
+
transform: { ...defaultTransform, ...lastFrame.transform },
|
|
916
|
+
opacity: lastFrame.opacity
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
let prevFrame = firstFrame;
|
|
920
|
+
let nextFrame = lastFrame;
|
|
921
|
+
for (let i = 0; i < keyframes.length - 1; i++) {
|
|
922
|
+
const currentFrame = keyframes[i];
|
|
923
|
+
const followingFrame = keyframes[i + 1];
|
|
924
|
+
if (timeUs >= currentFrame.time && timeUs < followingFrame.time) {
|
|
925
|
+
prevFrame = currentFrame;
|
|
926
|
+
nextFrame = followingFrame;
|
|
927
|
+
break;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
const duration = nextFrame.time - prevFrame.time;
|
|
931
|
+
const elapsed = timeUs - prevFrame.time;
|
|
932
|
+
const progress = elapsed / duration;
|
|
933
|
+
const easedProgress = applyEasing(progress, prevFrame.easing ?? "linear");
|
|
934
|
+
const prevTransform = prevFrame.transform ?? { x: 0, y: 0 };
|
|
935
|
+
const nextTransform = nextFrame.transform ?? { x: 0, y: 0 };
|
|
936
|
+
const transform = interpolateTransform(prevTransform, nextTransform, easedProgress);
|
|
937
|
+
const opacity = prevFrame.opacity !== void 0 && nextFrame.opacity !== void 0 ? lerp(prevFrame.opacity, nextFrame.opacity, easedProgress) : void 0;
|
|
938
|
+
return { transform, opacity };
|
|
939
|
+
}
|
|
940
|
+
function interpolateTransform(from, to, t) {
|
|
941
|
+
return {
|
|
942
|
+
x: lerp(from.x ?? 0, to.x ?? 0, t),
|
|
943
|
+
y: lerp(from.y ?? 0, to.y ?? 0, t),
|
|
944
|
+
scaleX: lerp(from.scaleX ?? 1, to.scaleX ?? 1, t),
|
|
945
|
+
scaleY: lerp(from.scaleY ?? 1, to.scaleY ?? 1, t),
|
|
946
|
+
rotation: lerp(from.rotation ?? 0, to.rotation ?? 0, t),
|
|
947
|
+
anchorX: lerp(from.anchorX ?? 0.5, to.anchorX ?? 0.5, t),
|
|
948
|
+
anchorY: lerp(from.anchorY ?? 0.5, to.anchorY ?? 0.5, t)
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
function lerp(a, b, t) {
|
|
952
|
+
return a + (b - a) * t;
|
|
953
|
+
}
|
|
954
|
+
function applyEasing(t, easing) {
|
|
955
|
+
switch (easing) {
|
|
956
|
+
case "linear":
|
|
957
|
+
return t;
|
|
958
|
+
case "ease-in":
|
|
959
|
+
return t * t * t;
|
|
960
|
+
case "ease-out": {
|
|
961
|
+
const oneMinusT = 1 - t;
|
|
962
|
+
return 1 - oneMinusT * oneMinusT * oneMinusT;
|
|
963
|
+
}
|
|
964
|
+
case "ease-in-out":
|
|
965
|
+
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
966
|
+
default:
|
|
967
|
+
return t;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
837
970
|
function resolveActiveLayers(layers, timestamp) {
|
|
838
971
|
return layers.filter((layer) => {
|
|
839
|
-
if (layer.
|
|
972
|
+
if (!layer.payload.attachmentId) {
|
|
840
973
|
return true;
|
|
841
974
|
}
|
|
842
975
|
if (layer.status !== "ready") {
|
|
@@ -847,7 +980,7 @@ function resolveActiveLayers(layers, timestamp) {
|
|
|
847
980
|
);
|
|
848
981
|
});
|
|
849
982
|
}
|
|
850
|
-
function materializeLayer(layer, frame) {
|
|
983
|
+
function materializeLayer(layer, frame, imageMap, globalTimeUs) {
|
|
851
984
|
const baseLayer = {
|
|
852
985
|
id: layer.layerId,
|
|
853
986
|
type: layer.type,
|
|
@@ -877,22 +1010,74 @@ function materializeLayer(layer, frame) {
|
|
|
877
1010
|
lineHeight: payload.lineHeight,
|
|
878
1011
|
textAlign: payload.align,
|
|
879
1012
|
verticalAlign: "bottom"
|
|
880
|
-
// Subtitles positioned at bottom
|
|
881
1013
|
};
|
|
882
1014
|
}
|
|
883
1015
|
if (layer.type === "image") {
|
|
884
1016
|
const payload = layer.payload;
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1017
|
+
const resourceId = payload.resourceId;
|
|
1018
|
+
const source = imageMap.get(resourceId) ?? null;
|
|
1019
|
+
const imageLayer = {
|
|
1020
|
+
...baseLayer,
|
|
1021
|
+
type: "image",
|
|
1022
|
+
source,
|
|
1023
|
+
attachmentId: payload.attachmentId
|
|
1024
|
+
};
|
|
1025
|
+
if (payload.targetWidth !== void 0) {
|
|
1026
|
+
imageLayer.targetWidth = payload.targetWidth;
|
|
1027
|
+
}
|
|
1028
|
+
if (payload.targetHeight !== void 0) {
|
|
1029
|
+
imageLayer.targetHeight = payload.targetHeight;
|
|
1030
|
+
}
|
|
1031
|
+
if (payload.animation && globalTimeUs !== void 0) {
|
|
1032
|
+
const animState = computeAnimationState(payload.animation, globalTimeUs);
|
|
1033
|
+
if (!animState.visible) {
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1036
|
+
imageLayer.transform = {
|
|
1037
|
+
x: animState.transform.x ?? 0,
|
|
1038
|
+
y: animState.transform.y ?? 0,
|
|
1039
|
+
scaleX: animState.transform.scaleX ?? 1,
|
|
1040
|
+
scaleY: animState.transform.scaleY ?? 1,
|
|
1041
|
+
rotation: animState.transform.rotation ?? 0,
|
|
1042
|
+
anchorX: animState.transform.anchorX ?? 0.5,
|
|
1043
|
+
anchorY: animState.transform.anchorY ?? 0.5
|
|
891
1044
|
};
|
|
1045
|
+
if (animState.opacity !== void 0) {
|
|
1046
|
+
imageLayer.opacity = animState.opacity;
|
|
1047
|
+
}
|
|
892
1048
|
}
|
|
1049
|
+
return imageLayer;
|
|
893
1050
|
}
|
|
894
1051
|
return baseLayer;
|
|
895
1052
|
}
|
|
1053
|
+
function computeAnimationState(animation, globalTimeUs) {
|
|
1054
|
+
const { position, keyframes, overlayClipStartUs } = animation;
|
|
1055
|
+
const relativeTimeUs = globalTimeUs - overlayClipStartUs;
|
|
1056
|
+
if (relativeTimeUs < 0 || relativeTimeUs > keyframes[keyframes.length - 1].time) {
|
|
1057
|
+
return {
|
|
1058
|
+
transform: { x: 0, y: 0, scaleX: 1, scaleY: 1, rotation: 0, anchorX: 0.5, anchorY: 0.5 },
|
|
1059
|
+
opacity: 0,
|
|
1060
|
+
visible: false
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
const animState = interpolateKeyframes(keyframes, relativeTimeUs);
|
|
1064
|
+
const rotationDeg = animState.transform?.rotation ?? 0;
|
|
1065
|
+
const rotationRad = rotationDeg * Math.PI / 180;
|
|
1066
|
+
const finalTransform = {
|
|
1067
|
+
x: position.x + (animState.transform?.x ?? 0),
|
|
1068
|
+
y: position.y + (animState.transform?.y ?? 0),
|
|
1069
|
+
scaleX: animState.transform?.scaleX ?? 1,
|
|
1070
|
+
scaleY: animState.transform?.scaleY ?? 1,
|
|
1071
|
+
rotation: rotationRad,
|
|
1072
|
+
anchorX: animState.transform?.anchorX ?? 0.5,
|
|
1073
|
+
anchorY: animState.transform?.anchorY ?? 0.5
|
|
1074
|
+
};
|
|
1075
|
+
return {
|
|
1076
|
+
transform: finalTransform,
|
|
1077
|
+
opacity: animState.opacity,
|
|
1078
|
+
visible: true
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
896
1081
|
class VideoComposeWorker {
|
|
897
1082
|
channel;
|
|
898
1083
|
composer = null;
|
|
@@ -902,7 +1087,7 @@ class VideoComposeWorker {
|
|
|
902
1087
|
upstreamPort = null;
|
|
903
1088
|
instructions = null;
|
|
904
1089
|
streamState = null;
|
|
905
|
-
|
|
1090
|
+
imageMap = /* @__PURE__ */ new Map();
|
|
906
1091
|
constructor() {
|
|
907
1092
|
this.channel = new WorkerChannel(self, {
|
|
908
1093
|
name: "VideoComposeWorker",
|
|
@@ -947,7 +1132,6 @@ class VideoComposeWorker {
|
|
|
947
1132
|
*/
|
|
948
1133
|
async handleConfigure(payload) {
|
|
949
1134
|
const { config, initial } = payload;
|
|
950
|
-
console.log("[VideoComposeWorker] handleConfigure", config, initial);
|
|
951
1135
|
const hasValidDimensions = config.width > 0 && config.height > 0;
|
|
952
1136
|
const hasValidFps = config.fps > 0;
|
|
953
1137
|
if (!hasValidDimensions || !hasValidFps) {
|
|
@@ -1067,8 +1251,8 @@ class VideoComposeWorker {
|
|
|
1067
1251
|
this.upstreamPort?.close();
|
|
1068
1252
|
this.downstreamPort = null;
|
|
1069
1253
|
this.upstreamPort = null;
|
|
1070
|
-
this.
|
|
1071
|
-
this.
|
|
1254
|
+
this.imageMap.forEach((bitmap) => bitmap.close());
|
|
1255
|
+
this.imageMap.clear();
|
|
1072
1256
|
this.instructions = null;
|
|
1073
1257
|
this.streamState = null;
|
|
1074
1258
|
this.channel.state = WorkerState.Disposed;
|
|
@@ -1091,16 +1275,21 @@ class VideoComposeWorker {
|
|
|
1091
1275
|
* only accepts ImageBitmap/OffscreenCanvas, not HTMLImageElement or Blob
|
|
1092
1276
|
*/
|
|
1093
1277
|
async handleReceiveImage(payload) {
|
|
1094
|
-
const { sessionId, imageBitmap } = payload;
|
|
1278
|
+
const { resourceId, sessionId, imageBitmap } = payload;
|
|
1095
1279
|
if (!this.sessionId) {
|
|
1096
1280
|
this.sessionId = sessionId;
|
|
1097
1281
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1282
|
+
const existing = this.imageMap.get(resourceId);
|
|
1283
|
+
if (existing) {
|
|
1284
|
+
existing.close();
|
|
1100
1285
|
}
|
|
1101
|
-
this.
|
|
1286
|
+
this.imageMap.set(resourceId, imageBitmap);
|
|
1102
1287
|
if (this.instructions) {
|
|
1103
|
-
|
|
1288
|
+
const mainLayer = this.instructions.layers.find((l) => !l.payload.attachmentId);
|
|
1289
|
+
const mainResourceId = mainLayer?.payload.resourceId;
|
|
1290
|
+
if (resourceId === mainResourceId) {
|
|
1291
|
+
await this.startImageFrameStream();
|
|
1292
|
+
}
|
|
1104
1293
|
}
|
|
1105
1294
|
return { success: true };
|
|
1106
1295
|
}
|
|
@@ -1111,8 +1300,8 @@ class VideoComposeWorker {
|
|
|
1111
1300
|
this.upstreamPort?.close();
|
|
1112
1301
|
this.downstreamPort = null;
|
|
1113
1302
|
this.upstreamPort = null;
|
|
1114
|
-
this.
|
|
1115
|
-
this.
|
|
1303
|
+
this.imageMap.forEach((bitmap) => bitmap.close());
|
|
1304
|
+
this.imageMap.clear();
|
|
1116
1305
|
return { success: true };
|
|
1117
1306
|
}
|
|
1118
1307
|
async startImageFrameStream() {
|
|
@@ -1123,6 +1312,14 @@ class VideoComposeWorker {
|
|
|
1123
1312
|
if (!timeline) {
|
|
1124
1313
|
return;
|
|
1125
1314
|
}
|
|
1315
|
+
const mainLayer = this.instructions.layers.find((l) => !l.payload.attachmentId);
|
|
1316
|
+
if (!mainLayer) return;
|
|
1317
|
+
const mainResourceId = mainLayer.payload.resourceId;
|
|
1318
|
+
const imageBitmap = this.imageMap.get(mainResourceId);
|
|
1319
|
+
if (!imageBitmap) {
|
|
1320
|
+
console.warn("[VideoComposeWorker] Main track ImageBitmap not found:", mainResourceId);
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1126
1323
|
const { composeStream, cacheStream, encodeStream } = this.composer.createStreams();
|
|
1127
1324
|
const { clipDurationUs, compositionFps } = timeline;
|
|
1128
1325
|
let currentTimeUs = 0;
|
|
@@ -1132,7 +1329,7 @@ class VideoComposeWorker {
|
|
|
1132
1329
|
controller.close();
|
|
1133
1330
|
return;
|
|
1134
1331
|
}
|
|
1135
|
-
const videoFrame = new VideoFrame(
|
|
1332
|
+
const videoFrame = new VideoFrame(imageBitmap, {
|
|
1136
1333
|
timestamp: currentTimeUs,
|
|
1137
1334
|
duration: frameDurationFromFps(compositionFps)
|
|
1138
1335
|
});
|
|
@@ -1164,21 +1361,24 @@ class VideoComposeWorker {
|
|
|
1164
1361
|
}
|
|
1165
1362
|
}
|
|
1166
1363
|
buildComposeRequest(instruction, frame) {
|
|
1167
|
-
const
|
|
1168
|
-
const clipStartUs = instruction.baseConfig.timeline?.clipStartUs ?? 0;
|
|
1364
|
+
const clipRelativeTime = this.computeTimelineTimestamp(frame, instruction.baseConfig);
|
|
1169
1365
|
const clipDurationUs = instruction.baseConfig.timeline?.clipDurationUs ?? Infinity;
|
|
1170
|
-
|
|
1171
|
-
if (normalizedTime < clipStartUs || normalizedTime >= clipEndUs) {
|
|
1366
|
+
if (clipRelativeTime < 0 || clipRelativeTime >= clipDurationUs) {
|
|
1172
1367
|
return null;
|
|
1173
1368
|
}
|
|
1174
|
-
const clipRelativeTime = normalizedTime - clipStartUs;
|
|
1175
1369
|
const activeLayers = resolveActiveLayers(instruction.layers, clipRelativeTime);
|
|
1176
1370
|
if (!activeLayers.length) {
|
|
1177
1371
|
return null;
|
|
1178
1372
|
}
|
|
1179
|
-
const
|
|
1373
|
+
const clipStartUs = instruction.baseConfig.timeline?.clipStartUs ?? 0;
|
|
1374
|
+
const globalTimeUs = clipStartUs + clipRelativeTime;
|
|
1375
|
+
const layers = activeLayers.map((layer) => materializeLayer(layer, frame, this.imageMap, globalTimeUs)).filter((layer) => layer !== null);
|
|
1376
|
+
if (!layers.length) {
|
|
1377
|
+
return null;
|
|
1378
|
+
}
|
|
1180
1379
|
return {
|
|
1181
|
-
timeUs:
|
|
1380
|
+
timeUs: clipRelativeTime,
|
|
1381
|
+
globalTimeUs,
|
|
1182
1382
|
layers,
|
|
1183
1383
|
transition: VideoComposeWorker.buildTransition(
|
|
1184
1384
|
instruction.transitions,
|
|
@@ -1208,7 +1408,6 @@ class VideoComposeWorker {
|
|
|
1208
1408
|
computeTimelineTimestamp(frame, config) {
|
|
1209
1409
|
if (!this.streamState) {
|
|
1210
1410
|
this.streamState = {
|
|
1211
|
-
baseTimestamp: null,
|
|
1212
1411
|
lastSourceTimestamp: null,
|
|
1213
1412
|
nextFrameIndex: 0
|
|
1214
1413
|
};
|
|
@@ -1219,42 +1418,21 @@ class VideoComposeWorker {
|
|
|
1219
1418
|
this.streamState.lastSourceTimestamp = frame.timestamp ?? null;
|
|
1220
1419
|
return ts;
|
|
1221
1420
|
}
|
|
1222
|
-
const {
|
|
1421
|
+
const { compositionFps } = timeline;
|
|
1223
1422
|
const sourceTimestamp = frame.timestamp ?? null;
|
|
1224
1423
|
if (sourceTimestamp !== null && this.streamState.lastSourceTimestamp !== null && sourceTimestamp < this.streamState.lastSourceTimestamp) {
|
|
1225
|
-
this.streamState.baseTimestamp = null;
|
|
1226
1424
|
this.streamState.nextFrameIndex = 0;
|
|
1227
1425
|
}
|
|
1228
|
-
if (this.streamState.baseTimestamp === null) {
|
|
1229
|
-
this.streamState.baseTimestamp = sourceTimestamp ?? 0;
|
|
1230
|
-
this.streamState.nextFrameIndex = 0;
|
|
1231
|
-
if (this.streamState.baseTimestamp > 1e3) {
|
|
1232
|
-
console.warn(
|
|
1233
|
-
`[VideoComposeWorker] First frame timestamp is ${this.streamState.baseTimestamp}us, expected ~0. Check MP4Demuxer normalization.`
|
|
1234
|
-
);
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
1426
|
const frameDuration = frameDurationFromFps(compositionFps);
|
|
1238
1427
|
let frameIndex = this.streamState.nextFrameIndex;
|
|
1239
1428
|
if (sourceTimestamp !== null) {
|
|
1240
|
-
const approxIndex =
|
|
1241
|
-
this.streamState.baseTimestamp,
|
|
1242
|
-
sourceTimestamp,
|
|
1243
|
-
compositionFps,
|
|
1244
|
-
"nearest"
|
|
1245
|
-
);
|
|
1429
|
+
const approxIndex = Math.round(sourceTimestamp / frameDuration);
|
|
1246
1430
|
frameIndex = Math.max(frameIndex, approxIndex);
|
|
1247
1431
|
}
|
|
1248
|
-
const
|
|
1249
|
-
const timelineTime = quantizeTimestampToFrame(
|
|
1250
|
-
rawTimeline,
|
|
1251
|
-
clipStartUs,
|
|
1252
|
-
compositionFps,
|
|
1253
|
-
"nearest"
|
|
1254
|
-
);
|
|
1432
|
+
const relativeTimeUs = frameIndex * frameDuration;
|
|
1255
1433
|
this.streamState.nextFrameIndex = frameIndex + 1;
|
|
1256
1434
|
this.streamState.lastSourceTimestamp = sourceTimestamp;
|
|
1257
|
-
return
|
|
1435
|
+
return relativeTimeUs;
|
|
1258
1436
|
}
|
|
1259
1437
|
}
|
|
1260
1438
|
const worker = new VideoComposeWorker();
|