@twick/2d 0.14.0 → 1.14.3
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/LICENSE +21 -21
- package/editor/editor/tsconfig.build.tsbuildinfo +1 -1
- package/lib/components/Audio.d.ts.map +1 -1
- package/lib/components/Audio.js +33 -3
- package/lib/components/CodeBlock.d.ts +1 -1
- package/lib/components/Img.js +23 -23
- package/lib/components/Line.js +31 -31
- package/lib/components/Media.d.ts +6 -0
- package/lib/components/Media.d.ts.map +1 -1
- package/lib/components/Media.js +277 -61
- package/lib/components/Node.d.ts +1 -1
- package/lib/components/Path.d.ts +1 -1
- package/lib/components/SVG.d.ts +1 -1
- package/lib/components/Shape.d.ts +1 -1
- package/lib/components/Spline.js +25 -25
- package/lib/components/Video.d.ts +0 -1
- package/lib/components/Video.d.ts.map +1 -1
- package/lib/components/Video.js +70 -65
- package/lib/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +4 -5
- package/src/editor/NodeInspectorConfig.tsx +76 -76
- package/src/editor/PreviewOverlayConfig.tsx +67 -67
- package/src/editor/Provider.tsx +93 -93
- package/src/editor/SceneGraphTabConfig.tsx +81 -81
- package/src/editor/icons/CircleIcon.tsx +7 -7
- package/src/editor/icons/CodeBlockIcon.tsx +8 -8
- package/src/editor/icons/CurveIcon.tsx +7 -7
- package/src/editor/icons/GridIcon.tsx +7 -7
- package/src/editor/icons/IconMap.ts +35 -35
- package/src/editor/icons/ImgIcon.tsx +8 -8
- package/src/editor/icons/LayoutIcon.tsx +9 -9
- package/src/editor/icons/LineIcon.tsx +7 -7
- package/src/editor/icons/NodeIcon.tsx +7 -7
- package/src/editor/icons/RayIcon.tsx +7 -7
- package/src/editor/icons/RectIcon.tsx +7 -7
- package/src/editor/icons/ShapeIcon.tsx +7 -7
- package/src/editor/icons/TxtIcon.tsx +8 -8
- package/src/editor/icons/VideoIcon.tsx +7 -7
- package/src/editor/icons/View2DIcon.tsx +10 -10
- package/src/editor/index.ts +17 -17
- package/src/editor/tree/DetachedRoot.tsx +23 -23
- package/src/editor/tree/NodeElement.tsx +74 -74
- package/src/editor/tree/TreeElement.tsx +72 -72
- package/src/editor/tree/TreeRoot.tsx +10 -10
- package/src/editor/tree/ViewRoot.tsx +20 -20
- package/src/editor/tree/index.module.scss +38 -38
- package/src/editor/tree/index.ts +3 -3
- package/src/editor/tsconfig.build.json +5 -5
- package/src/editor/tsconfig.json +12 -12
- package/src/editor/tsdoc.json +4 -4
- package/src/editor/vite-env.d.ts +1 -1
- package/src/lib/code/CodeCursor.ts +445 -445
- package/src/lib/code/CodeDiffer.ts +78 -78
- package/src/lib/code/CodeFragment.ts +97 -97
- package/src/lib/code/CodeHighlighter.ts +75 -75
- package/src/lib/code/CodeMetrics.ts +47 -47
- package/src/lib/code/CodeRange.test.ts +74 -74
- package/src/lib/code/CodeRange.ts +216 -216
- package/src/lib/code/CodeScope.ts +101 -101
- package/src/lib/code/CodeSelection.ts +24 -24
- package/src/lib/code/CodeSignal.ts +327 -327
- package/src/lib/code/CodeTokenizer.ts +54 -54
- package/src/lib/code/DefaultHighlightStyle.ts +98 -98
- package/src/lib/code/LezerHighlighter.ts +113 -113
- package/src/lib/code/diff.test.ts +311 -311
- package/src/lib/code/diff.ts +319 -319
- package/src/lib/code/extractRange.ts +126 -126
- package/src/lib/code/index.ts +13 -13
- package/src/lib/components/Audio.ts +168 -131
- package/src/lib/components/Bezier.ts +105 -105
- package/src/lib/components/Circle.ts +266 -266
- package/src/lib/components/Code.ts +526 -526
- package/src/lib/components/CodeBlock.ts +576 -576
- package/src/lib/components/CubicBezier.ts +112 -112
- package/src/lib/components/Curve.ts +455 -455
- package/src/lib/components/Grid.ts +135 -135
- package/src/lib/components/Icon.ts +96 -96
- package/src/lib/components/Img.ts +319 -319
- package/src/lib/components/Knot.ts +157 -157
- package/src/lib/components/Latex.ts +122 -122
- package/src/lib/components/Layout.ts +1092 -1092
- package/src/lib/components/Line.ts +429 -429
- package/src/lib/components/Media.ts +576 -346
- package/src/lib/components/Node.ts +1940 -1940
- package/src/lib/components/Path.ts +137 -137
- package/src/lib/components/Polygon.ts +171 -171
- package/src/lib/components/QuadBezier.ts +100 -100
- package/src/lib/components/Ray.ts +125 -125
- package/src/lib/components/Rect.ts +187 -187
- package/src/lib/components/Rive.ts +156 -156
- package/src/lib/components/SVG.ts +797 -797
- package/src/lib/components/Shape.ts +143 -143
- package/src/lib/components/Spline.ts +344 -344
- package/src/lib/components/Txt.test.tsx +81 -81
- package/src/lib/components/Txt.ts +203 -203
- package/src/lib/components/TxtLeaf.ts +205 -205
- package/src/lib/components/Video.ts +461 -462
- package/src/lib/components/View2D.ts +98 -98
- package/src/lib/components/__tests__/children.test.tsx +142 -142
- package/src/lib/components/__tests__/clone.test.tsx +126 -126
- package/src/lib/components/__tests__/generatorTest.ts +28 -28
- package/src/lib/components/__tests__/mockScene2D.ts +45 -45
- package/src/lib/components/__tests__/query.test.tsx +122 -122
- package/src/lib/components/__tests__/state.test.tsx +60 -60
- package/src/lib/components/index.ts +28 -28
- package/src/lib/components/types.ts +35 -35
- package/src/lib/curves/ArcSegment.ts +159 -159
- package/src/lib/curves/CircleSegment.ts +77 -77
- package/src/lib/curves/CubicBezierSegment.ts +78 -78
- package/src/lib/curves/CurveDrawingInfo.ts +11 -11
- package/src/lib/curves/CurvePoint.ts +15 -15
- package/src/lib/curves/CurveProfile.ts +7 -7
- package/src/lib/curves/KnotInfo.ts +10 -10
- package/src/lib/curves/LineSegment.ts +62 -62
- package/src/lib/curves/Polynomial.ts +355 -355
- package/src/lib/curves/Polynomial2D.ts +62 -62
- package/src/lib/curves/PolynomialSegment.ts +124 -124
- package/src/lib/curves/QuadBezierSegment.ts +64 -64
- package/src/lib/curves/Segment.ts +17 -17
- package/src/lib/curves/UniformPolynomialCurveSampler.ts +94 -94
- package/src/lib/curves/createCurveProfileLerp.ts +471 -471
- package/src/lib/curves/getBezierSplineProfile.ts +223 -223
- package/src/lib/curves/getCircleProfile.ts +86 -86
- package/src/lib/curves/getPathProfile.ts +178 -178
- package/src/lib/curves/getPointAtDistance.ts +21 -21
- package/src/lib/curves/getPolylineProfile.test.ts +21 -21
- package/src/lib/curves/getPolylineProfile.ts +89 -89
- package/src/lib/curves/getRectProfile.ts +139 -139
- package/src/lib/curves/index.ts +16 -16
- package/src/lib/decorators/canvasStyleSignal.ts +16 -16
- package/src/lib/decorators/colorSignal.ts +9 -9
- package/src/lib/decorators/compound.ts +72 -72
- package/src/lib/decorators/computed.ts +18 -18
- package/src/lib/decorators/defaultStyle.ts +18 -18
- package/src/lib/decorators/filtersSignal.ts +136 -136
- package/src/lib/decorators/index.ts +10 -10
- package/src/lib/decorators/initializers.ts +32 -32
- package/src/lib/decorators/nodeName.ts +13 -13
- package/src/lib/decorators/signal.test.ts +90 -90
- package/src/lib/decorators/signal.ts +345 -345
- package/src/lib/decorators/spacingSignal.ts +15 -15
- package/src/lib/decorators/vector2Signal.ts +30 -30
- package/src/lib/globals.d.ts +2 -2
- package/src/lib/index.ts +8 -8
- package/src/lib/jsx-dev-runtime.ts +2 -2
- package/src/lib/jsx-runtime.ts +46 -46
- package/src/lib/parse-svg-path.d.ts +14 -14
- package/src/lib/partials/Filter.ts +180 -180
- package/src/lib/partials/Gradient.ts +102 -102
- package/src/lib/partials/Pattern.ts +34 -34
- package/src/lib/partials/ShaderConfig.ts +117 -117
- package/src/lib/partials/index.ts +4 -4
- package/src/lib/partials/types.ts +58 -58
- package/src/lib/scenes/Scene2D.ts +242 -242
- package/src/lib/scenes/index.ts +3 -3
- package/src/lib/scenes/makeScene2D.ts +16 -16
- package/src/lib/scenes/useScene2D.ts +6 -6
- package/src/lib/tsconfig.build.json +5 -5
- package/src/lib/tsconfig.json +10 -10
- package/src/lib/tsdoc.json +4 -4
- package/src/lib/utils/CanvasUtils.ts +306 -306
- package/src/lib/utils/diff.test.ts +453 -453
- package/src/lib/utils/diff.ts +148 -148
- package/src/lib/utils/index.ts +2 -2
- package/src/lib/utils/is.ts +11 -11
- package/src/lib/utils/makeSignalExtensions.ts +30 -30
- package/src/lib/utils/video/declarations.d.ts +1 -1
- package/src/lib/utils/video/ffmpeg-client.ts +50 -50
- package/src/lib/utils/video/mp4-parser-manager.ts +72 -72
- package/src/lib/utils/video/parser/index.ts +1 -1
- package/src/lib/utils/video/parser/parser.ts +257 -257
- package/src/lib/utils/video/parser/sampler.ts +72 -72
- package/src/lib/utils/video/parser/segment.ts +302 -302
- package/src/lib/utils/video/parser/sink.ts +29 -29
- package/src/lib/utils/video/parser/utils.ts +31 -31
- package/src/tsconfig.base.json +19 -19
- package/src/tsconfig.build.json +8 -8
- package/src/tsconfig.json +5 -5
- package/tsconfig.project.json +7 -7
- package/lib/components/utils/waitUntil.d.ts +0 -7
- package/lib/components/utils/waitUntil.d.ts.map +0 -1
- package/lib/components/utils/waitUntil.js +0 -15
- package/lib/utils/waitUntil.d.ts +0 -7
- package/lib/utils/waitUntil.d.ts.map +0 -1
- package/lib/utils/waitUntil.js +0 -15
- package/src/lib/utils/waitUntil.ts +0 -18
|
@@ -1,346 +1,576 @@
|
|
|
1
|
-
import type {SignalValue, SimpleSignal} from '@twick/core';
|
|
2
|
-
import {
|
|
3
|
-
DependencyContext,
|
|
4
|
-
PlaybackState,
|
|
5
|
-
clamp,
|
|
6
|
-
isReactive,
|
|
7
|
-
useLogger,
|
|
8
|
-
useThread,
|
|
9
|
-
} from '@twick/core';
|
|
10
|
-
import {computed, initial, nodeName, signal} from '../decorators';
|
|
11
|
-
import type {RectProps} from './Rect';
|
|
12
|
-
import {Rect} from './Rect';
|
|
13
|
-
|
|
14
|
-
export interface MediaProps extends RectProps {
|
|
15
|
-
src?: SignalValue<string>;
|
|
16
|
-
loop?: SignalValue<boolean>;
|
|
17
|
-
playbackRate?: number;
|
|
18
|
-
volume?: number;
|
|
19
|
-
time?: SignalValue<number>;
|
|
20
|
-
play?: boolean;
|
|
21
|
-
awaitCanPlay?: SignalValue<boolean>;
|
|
22
|
-
allowVolumeAmplificationInPreview?: SignalValue<boolean>;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const reactivePlaybackRate = `
|
|
26
|
-
The \`playbackRate\` of a \`Video\` cannot be reactive.
|
|
27
|
-
|
|
28
|
-
Make sure to use a concrete value and not a function:
|
|
29
|
-
|
|
30
|
-
\`\`\`ts wrong
|
|
31
|
-
video.playbackRate(() => 7);
|
|
32
|
-
\`\`\`
|
|
33
|
-
|
|
34
|
-
\`\`\`ts correct
|
|
35
|
-
video.playbackRate(7);
|
|
36
|
-
\`\`\`
|
|
37
|
-
|
|
38
|
-
If you're using a signal, extract its value before passing it to the property:
|
|
39
|
-
|
|
40
|
-
\`\`\`ts wrong
|
|
41
|
-
video.playbackRate(mySignal);
|
|
42
|
-
\`\`\`
|
|
43
|
-
|
|
44
|
-
\`\`\`ts correct
|
|
45
|
-
video.playbackRate(mySignal());
|
|
46
|
-
\`\`\`
|
|
47
|
-
`;
|
|
48
|
-
|
|
49
|
-
@nodeName('Media')
|
|
50
|
-
export abstract class Media extends Rect {
|
|
51
|
-
@
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
@
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
@
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
@
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
@
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
1
|
+
import type {SignalValue, SimpleSignal} from '@twick/core';
|
|
2
|
+
import {
|
|
3
|
+
DependencyContext,
|
|
4
|
+
PlaybackState,
|
|
5
|
+
clamp,
|
|
6
|
+
isReactive,
|
|
7
|
+
useLogger,
|
|
8
|
+
useThread,
|
|
9
|
+
} from '@twick/core';
|
|
10
|
+
import {computed, initial, nodeName, signal} from '../decorators';
|
|
11
|
+
import type {RectProps} from './Rect';
|
|
12
|
+
import {Rect} from './Rect';
|
|
13
|
+
|
|
14
|
+
export interface MediaProps extends RectProps {
|
|
15
|
+
src?: SignalValue<string>;
|
|
16
|
+
loop?: SignalValue<boolean>;
|
|
17
|
+
playbackRate?: number;
|
|
18
|
+
volume?: number;
|
|
19
|
+
time?: SignalValue<number>;
|
|
20
|
+
play?: boolean;
|
|
21
|
+
awaitCanPlay?: SignalValue<boolean>;
|
|
22
|
+
allowVolumeAmplificationInPreview?: SignalValue<boolean>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const reactivePlaybackRate = `
|
|
26
|
+
The \`playbackRate\` of a \`Video\` cannot be reactive.
|
|
27
|
+
|
|
28
|
+
Make sure to use a concrete value and not a function:
|
|
29
|
+
|
|
30
|
+
\`\`\`ts wrong
|
|
31
|
+
video.playbackRate(() => 7);
|
|
32
|
+
\`\`\`
|
|
33
|
+
|
|
34
|
+
\`\`\`ts correct
|
|
35
|
+
video.playbackRate(7);
|
|
36
|
+
\`\`\`
|
|
37
|
+
|
|
38
|
+
If you're using a signal, extract its value before passing it to the property:
|
|
39
|
+
|
|
40
|
+
\`\`\`ts wrong
|
|
41
|
+
video.playbackRate(mySignal);
|
|
42
|
+
\`\`\`
|
|
43
|
+
|
|
44
|
+
\`\`\`ts correct
|
|
45
|
+
video.playbackRate(mySignal());
|
|
46
|
+
\`\`\`
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
@nodeName('Media')
|
|
50
|
+
export abstract class Media extends Rect {
|
|
51
|
+
@initial('')
|
|
52
|
+
@signal()
|
|
53
|
+
public declare readonly src: SimpleSignal<string, this>;
|
|
54
|
+
|
|
55
|
+
@initial(false)
|
|
56
|
+
@signal()
|
|
57
|
+
public declare readonly loop: SimpleSignal<boolean, this>;
|
|
58
|
+
|
|
59
|
+
@initial(1)
|
|
60
|
+
@signal()
|
|
61
|
+
public declare readonly playbackRate: SimpleSignal<number, this>;
|
|
62
|
+
|
|
63
|
+
@initial(0)
|
|
64
|
+
@signal()
|
|
65
|
+
protected declare readonly time: SimpleSignal<number, this>;
|
|
66
|
+
|
|
67
|
+
@initial(false)
|
|
68
|
+
@signal()
|
|
69
|
+
protected declare readonly playing: SimpleSignal<boolean, this>;
|
|
70
|
+
|
|
71
|
+
@initial(true)
|
|
72
|
+
@signal()
|
|
73
|
+
protected declare readonly awaitCanPlay: SimpleSignal<boolean, this>;
|
|
74
|
+
|
|
75
|
+
@initial(false)
|
|
76
|
+
@signal()
|
|
77
|
+
protected declare readonly allowVolumeAmplificationInPreview: SimpleSignal<
|
|
78
|
+
boolean,
|
|
79
|
+
this
|
|
80
|
+
>;
|
|
81
|
+
|
|
82
|
+
protected declare volume: number;
|
|
83
|
+
|
|
84
|
+
protected static readonly amplificationPool: Record<
|
|
85
|
+
string,
|
|
86
|
+
{
|
|
87
|
+
audioContext: AudioContext;
|
|
88
|
+
sourceNode: MediaElementAudioSourceNode;
|
|
89
|
+
gainNode: GainNode;
|
|
90
|
+
}
|
|
91
|
+
> = {};
|
|
92
|
+
protected lastTime = -1;
|
|
93
|
+
private isSchedulingPlay = false;
|
|
94
|
+
|
|
95
|
+
public constructor(props: MediaProps) {
|
|
96
|
+
super(props);
|
|
97
|
+
|
|
98
|
+
if (!this.awaitCanPlay()) {
|
|
99
|
+
this.scheduleSeek(this.time());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (props.play) {
|
|
103
|
+
this.play();
|
|
104
|
+
}
|
|
105
|
+
this.volume = props.volume ?? 1;
|
|
106
|
+
// Only set volume immediately if media is ready
|
|
107
|
+
if (!this.awaitCanPlay()) {
|
|
108
|
+
this.setVolume(this.volume);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public isPlaying(): boolean {
|
|
113
|
+
return this.playing();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public getCurrentTime(): number {
|
|
117
|
+
return this.clampTime(this.time());
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
public getDuration(): number {
|
|
121
|
+
try {
|
|
122
|
+
const mElement = this.mediaElement();
|
|
123
|
+
const isVideo = (mElement instanceof HTMLVideoElement);
|
|
124
|
+
const isAudio = (mElement instanceof HTMLAudioElement);
|
|
125
|
+
return (this.isIOS() && (isVideo || isAudio)) ? 2 /** dummy duration for iOS */ : mElement.duration;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// If media element is not ready yet, return a default duration
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public getVolume(): number {
|
|
133
|
+
return this.volume;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public getUrl(): string {
|
|
137
|
+
try {
|
|
138
|
+
return this.mediaElement().src;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
// If media element is not ready yet, return the src signal value
|
|
141
|
+
return this.src();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public override dispose() {
|
|
146
|
+
// Set playing state to false without trying to access media element
|
|
147
|
+
this.playing(false);
|
|
148
|
+
this.time.save();
|
|
149
|
+
this.remove();
|
|
150
|
+
super.dispose();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@computed()
|
|
154
|
+
public override completion(): number {
|
|
155
|
+
return this.clampTime(this.time()) / this.getDuration();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
protected abstract mediaElement(): HTMLMediaElement;
|
|
159
|
+
|
|
160
|
+
protected abstract seekedMedia(): HTMLMediaElement;
|
|
161
|
+
|
|
162
|
+
protected abstract fastSeekedMedia(): HTMLMediaElement;
|
|
163
|
+
|
|
164
|
+
protected abstract override draw(
|
|
165
|
+
context: CanvasRenderingContext2D,
|
|
166
|
+
): Promise<void>;
|
|
167
|
+
|
|
168
|
+
protected setCurrentTime(value: number) {
|
|
169
|
+
try {
|
|
170
|
+
const media = this.mediaElement();
|
|
171
|
+
if (media.readyState < 2) return;
|
|
172
|
+
|
|
173
|
+
media.currentTime = value;
|
|
174
|
+
this.lastTime = value;
|
|
175
|
+
if (media.seeking) {
|
|
176
|
+
DependencyContext.collectPromise(
|
|
177
|
+
new Promise<void>(resolve => {
|
|
178
|
+
const listener = () => {
|
|
179
|
+
resolve();
|
|
180
|
+
media.removeEventListener('seeked', listener);
|
|
181
|
+
};
|
|
182
|
+
media.addEventListener('seeked', listener);
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
// If media element is not ready yet, just update the lastTime
|
|
188
|
+
this.lastTime = value;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
public setVolume(volume: number) {
|
|
193
|
+
if (volume < 0) {
|
|
194
|
+
console.warn(
|
|
195
|
+
`volumes cannot be negative - the value will be clamped to 0.`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Store the volume value
|
|
200
|
+
this.volume = volume;
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const media = this.mediaElement();
|
|
204
|
+
media.volume = Math.min(Math.max(volume, 0), 1);
|
|
205
|
+
|
|
206
|
+
if (volume > 1) {
|
|
207
|
+
if (this.allowVolumeAmplificationInPreview()) {
|
|
208
|
+
this.amplify(media, volume);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
console.warn(
|
|
212
|
+
`you have set the volume of node ${this.key} to ${volume} - your video will be exported with the correct volume, but the browser does not support volumes higher than 1 by default. To enable volume amplification in the preview, set the "allowVolumeAmplificationInPreview" of your <Video/> or <Audio/> tag to true. Note that amplification for previews will not work if you use autoplay within the player due to browser autoplay policies: https://developer.chrome.com/blog/autoplay/#webaudio.`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
} catch (error) {
|
|
216
|
+
// If media element is not ready yet, just store the volume
|
|
217
|
+
// It will be applied when the media becomes available via collectAsyncResources
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@computed()
|
|
222
|
+
protected amplify(node: HTMLMediaElement, volume: number) {
|
|
223
|
+
const key = `${this.src()}/${this.key}`;
|
|
224
|
+
|
|
225
|
+
if (Media.amplificationPool[key]) {
|
|
226
|
+
Media.amplificationPool[key].gainNode.gain.value = volume;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const audioContext = new AudioContext();
|
|
231
|
+
const sourceNode = audioContext.createMediaElementSource(node);
|
|
232
|
+
const gainNode = audioContext.createGain();
|
|
233
|
+
|
|
234
|
+
gainNode.gain.value = volume;
|
|
235
|
+
sourceNode.connect(gainNode);
|
|
236
|
+
gainNode.connect(audioContext.destination);
|
|
237
|
+
|
|
238
|
+
Media.amplificationPool[key] = {audioContext, sourceNode, gainNode};
|
|
239
|
+
|
|
240
|
+
if (typeof window === 'undefined' || audioContext.state !== 'suspended') {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Start audio context after user interation, neccessary due to browser autoplay policies
|
|
245
|
+
const handleInteraction = () => {
|
|
246
|
+
Media.amplificationPool[key].audioContext.resume();
|
|
247
|
+
window.removeEventListener('click', handleInteraction);
|
|
248
|
+
};
|
|
249
|
+
window.addEventListener('click', handleInteraction);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
protected setPlaybackRate(playbackRate: number) {
|
|
253
|
+
let value: number;
|
|
254
|
+
if (isReactive(playbackRate)) {
|
|
255
|
+
value = playbackRate();
|
|
256
|
+
useLogger().warn({
|
|
257
|
+
message: 'Invalid value set as the playback rate',
|
|
258
|
+
remarks: reactivePlaybackRate,
|
|
259
|
+
inspect: this.key,
|
|
260
|
+
stack: new Error().stack,
|
|
261
|
+
});
|
|
262
|
+
} else {
|
|
263
|
+
value = playbackRate;
|
|
264
|
+
}
|
|
265
|
+
this.playbackRate.context.setter(value);
|
|
266
|
+
|
|
267
|
+
if (this.playing()) {
|
|
268
|
+
if (value === 0) {
|
|
269
|
+
this.pause();
|
|
270
|
+
} else {
|
|
271
|
+
const time = useThread().time;
|
|
272
|
+
const start = time();
|
|
273
|
+
const offset = this.time();
|
|
274
|
+
this.time(() => this.clampTime(offset + (time() - start) * value));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
protected scheduleSeek(time: number) {
|
|
280
|
+
// Defer the media element access to avoid immediate async property access
|
|
281
|
+
setTimeout(() => {
|
|
282
|
+
try {
|
|
283
|
+
const media = this.mediaElement();
|
|
284
|
+
|
|
285
|
+
// Use the existing waitForCanPlay method which handles readiness properly
|
|
286
|
+
this.waitForCanPlay(media, () => {
|
|
287
|
+
// Wait until the media is ready to seek again as
|
|
288
|
+
// setting the time before the video doesn't work reliably.
|
|
289
|
+
media.currentTime = time;
|
|
290
|
+
});
|
|
291
|
+
} catch (error) {
|
|
292
|
+
// If media element is not ready yet, retry after a longer delay
|
|
293
|
+
setTimeout(() => this.scheduleSeek(time), 50);
|
|
294
|
+
}
|
|
295
|
+
}, 0);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Waits for the canplay event to be fired before calling onCanPlay.
|
|
300
|
+
*
|
|
301
|
+
* If the media is already ready to play, onCanPlay is called immediately.
|
|
302
|
+
* @param onCanPlay - The function to call when the media is ready to play.
|
|
303
|
+
* @returns
|
|
304
|
+
*/
|
|
305
|
+
protected waitForCanPlay(media: HTMLMediaElement, onCanPlay: () => void) {
|
|
306
|
+
// Be more strict - require readyState >= 3 (HAVE_FUTURE_DATA) for better reliability
|
|
307
|
+
if (media.readyState >= 3) {
|
|
308
|
+
onCanPlay();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const onCanPlayWrapper = () => {
|
|
313
|
+
onCanPlay();
|
|
314
|
+
media.removeEventListener('canplay', onCanPlayWrapper);
|
|
315
|
+
media.removeEventListener('canplaythrough', onCanPlayWrapper);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const onError = () => {
|
|
319
|
+
const reason = this.getErrorReason(media.error?.code);
|
|
320
|
+
const srcValue = this.src();
|
|
321
|
+
|
|
322
|
+
console.log(`ERROR: Error loading video: src="${srcValue}", ${reason}`);
|
|
323
|
+
console.log(`Media element src: "${media.src}"`);
|
|
324
|
+
media.removeEventListener('error', onError);
|
|
325
|
+
media.removeEventListener('canplay', onCanPlayWrapper);
|
|
326
|
+
media.removeEventListener('canplaythrough', onCanPlayWrapper);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// Listen for both canplay and canplaythrough events
|
|
330
|
+
media.addEventListener('canplay', onCanPlayWrapper);
|
|
331
|
+
media.addEventListener('canplaythrough', onCanPlayWrapper);
|
|
332
|
+
media.addEventListener('error', onError);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Returns true if we should wait for the media to be ready to play.
|
|
337
|
+
*/
|
|
338
|
+
protected waitForCanPlayNecessary(media: HTMLMediaElement): boolean {
|
|
339
|
+
if (media.readyState >= 2) {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
this.awaitCanPlay() ||
|
|
345
|
+
this.view().playbackState() === PlaybackState.Rendering
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
public play() {
|
|
350
|
+
console.log('=== Media.play() called ===');
|
|
351
|
+
// Set the playing state first
|
|
352
|
+
this.playing(true);
|
|
353
|
+
|
|
354
|
+
// Schedule the actual play operation for when media is ready
|
|
355
|
+
this.schedulePlay();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
protected schedulePlay() {
|
|
359
|
+
// Prevent recursive calls
|
|
360
|
+
if (this.isSchedulingPlay) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
this.isSchedulingPlay = true;
|
|
365
|
+
|
|
366
|
+
// Check if thread context is available before accessing it
|
|
367
|
+
let timeFunction: (() => number) | null = null;
|
|
368
|
+
try {
|
|
369
|
+
const time = useThread().time;
|
|
370
|
+
timeFunction = time;
|
|
371
|
+
} catch (error) {
|
|
372
|
+
// Reset flag and use simple play without thread time
|
|
373
|
+
this.isSchedulingPlay = false;
|
|
374
|
+
this.simplePlay();
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// We need to wait for the media to be ready before we can play it
|
|
379
|
+
// Use a setTimeout to defer the operation and avoid immediate async property access
|
|
380
|
+
setTimeout(() => {
|
|
381
|
+
// Check if we're still supposed to be playing (avoid race conditions)
|
|
382
|
+
const isPlaying = this.playing();
|
|
383
|
+
if (!isPlaying) {
|
|
384
|
+
this.isSchedulingPlay = false;
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Add another timeout to further defer media element access
|
|
389
|
+
setTimeout(() => {
|
|
390
|
+
try {
|
|
391
|
+
const media = this.mediaElement();
|
|
392
|
+
|
|
393
|
+
// Always use waitForCanPlay to ensure media is ready
|
|
394
|
+
this.waitForCanPlay(media, () => {
|
|
395
|
+
// Double-check we're still playing before calling actuallyPlay
|
|
396
|
+
if (this.playing() && timeFunction) {
|
|
397
|
+
this.actuallyPlay(media, timeFunction);
|
|
398
|
+
}
|
|
399
|
+
// Reset the flag when done
|
|
400
|
+
this.isSchedulingPlay = false;
|
|
401
|
+
});
|
|
402
|
+
} catch (error) {
|
|
403
|
+
// Reset flag before retry
|
|
404
|
+
this.isSchedulingPlay = false;
|
|
405
|
+
// If media is not ready yet, retry after a longer delay
|
|
406
|
+
setTimeout(() => this.schedulePlay(), 100);
|
|
407
|
+
}
|
|
408
|
+
}, 10);
|
|
409
|
+
}, 0);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private simplePlay() {
|
|
413
|
+
setTimeout(() => {
|
|
414
|
+
try {
|
|
415
|
+
const media = this.mediaElement();
|
|
416
|
+
|
|
417
|
+
// Guard against undefined src
|
|
418
|
+
if (!media.src || media.src.includes('undefined')) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (media.paused && this.playing()) {
|
|
423
|
+
media.playbackRate = this.playbackRate();
|
|
424
|
+
const playPromise = media.play();
|
|
425
|
+
if (playPromise !== undefined) {
|
|
426
|
+
playPromise.then(() => {
|
|
427
|
+
console.log('Simple play started successfully');
|
|
428
|
+
}).catch(error => {
|
|
429
|
+
if (error.name !== 'AbortError') {
|
|
430
|
+
console.warn('Error in simple play:', error);
|
|
431
|
+
}
|
|
432
|
+
this.playing(false);
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
} catch (error) {
|
|
437
|
+
// Stop retries for errors
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
}, 10);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private actuallyPlay(media: HTMLMediaElement, timeFunction: () => number) {
|
|
444
|
+
console.log('=== actuallyPlay called ===');
|
|
445
|
+
console.log('Media element:', media);
|
|
446
|
+
console.log('Media src:', media.src);
|
|
447
|
+
console.log('Media paused:', media.paused);
|
|
448
|
+
console.log('Media readyState:', media.readyState);
|
|
449
|
+
|
|
450
|
+
// Make sure we're still supposed to be playing
|
|
451
|
+
if (!this.playing()) {
|
|
452
|
+
console.log('Playing state is false, aborting actuallyPlay');
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Set playback rate on media element
|
|
457
|
+
media.playbackRate = this.playbackRate();
|
|
458
|
+
|
|
459
|
+
// Ensure the media is ready to play
|
|
460
|
+
if (media.paused) {
|
|
461
|
+
console.log('Media is paused, calling play()');
|
|
462
|
+
// Start playing the media element
|
|
463
|
+
const playPromise = media.play();
|
|
464
|
+
if (playPromise !== undefined) {
|
|
465
|
+
playPromise.then(() => {
|
|
466
|
+
console.log('Media play() promise resolved - should be playing now');
|
|
467
|
+
console.log('Post-play media paused:', media.paused);
|
|
468
|
+
console.log('Post-play media currentTime:', media.currentTime);
|
|
469
|
+
}).catch(error => {
|
|
470
|
+
// Don't warn about AbortError - it's normal when play() is interrupted by pause()
|
|
471
|
+
if (error.name !== 'AbortError') {
|
|
472
|
+
console.warn('Error playing media:', error);
|
|
473
|
+
}
|
|
474
|
+
this.playing(false);
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
} else {
|
|
478
|
+
console.log('Media is already playing');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Set up time synchronization
|
|
482
|
+
const start = timeFunction();
|
|
483
|
+
const offset = media.currentTime;
|
|
484
|
+
|
|
485
|
+
// Update time signal
|
|
486
|
+
this.time(() => {
|
|
487
|
+
const newTime = this.clampTime(offset + (timeFunction() - start) * this.playbackRate());
|
|
488
|
+
return newTime;
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
public pause() {
|
|
493
|
+
// Set the playing state first
|
|
494
|
+
this.playing(false);
|
|
495
|
+
this.time.save();
|
|
496
|
+
|
|
497
|
+
// Try to pause the media element if it's available
|
|
498
|
+
// Use setTimeout to defer access and avoid async property issues
|
|
499
|
+
setTimeout(() => {
|
|
500
|
+
try {
|
|
501
|
+
const media = this.mediaElement();
|
|
502
|
+
media.pause();
|
|
503
|
+
} catch (error) {
|
|
504
|
+
// If media element is not ready yet, just update the state
|
|
505
|
+
// The media won't be playing anyway if it's not ready
|
|
506
|
+
}
|
|
507
|
+
}, 0);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
public clampTime(time: number): number {
|
|
511
|
+
const duration = this.getDuration();
|
|
512
|
+
if (this.loop()) {
|
|
513
|
+
time %= duration;
|
|
514
|
+
}
|
|
515
|
+
return clamp(0, duration, time);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
protected override collectAsyncResources() {
|
|
519
|
+
super.collectAsyncResources();
|
|
520
|
+
this.seekedMedia();
|
|
521
|
+
// Ensure volume is set when media becomes available
|
|
522
|
+
this.setVolume(this.volume);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
protected autoPlayBasedOnTwick() {
|
|
526
|
+
// Auto-start/stop playback based on Twick's playback state
|
|
527
|
+
const playbackState = this.view().playbackState();
|
|
528
|
+
// console.log('autoPlayBasedOnTwick called:', {
|
|
529
|
+
// playbackState,
|
|
530
|
+
// currentlyPlaying: this.playing(),
|
|
531
|
+
// shouldAutoPlay: (playbackState === PlaybackState.Playing || playbackState === PlaybackState.Presenting) && !this.playing(),
|
|
532
|
+
// shouldAutoPause: playbackState === PlaybackState.Paused && this.playing()
|
|
533
|
+
// });
|
|
534
|
+
|
|
535
|
+
if ((playbackState === PlaybackState.Playing || playbackState === PlaybackState.Presenting) && !this.playing()) {
|
|
536
|
+
console.log('Auto-starting media playback via play() method');
|
|
537
|
+
this.play(); // Call the full play() method instead of just setting playing(true)
|
|
538
|
+
} else if (playbackState === PlaybackState.Paused && this.playing()) {
|
|
539
|
+
console.log('Auto-pausing media playback via pause() method');
|
|
540
|
+
this.pause(); // Call the full pause() method
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
protected getErrorReason(errCode?: number) {
|
|
545
|
+
let reason;
|
|
546
|
+
switch (errCode) {
|
|
547
|
+
case 1:
|
|
548
|
+
reason = 'MEDIA_ERR_ABORTED';
|
|
549
|
+
break;
|
|
550
|
+
case 2:
|
|
551
|
+
reason = 'MEDIA_ERR_NETWORK. This might be a 404 error.';
|
|
552
|
+
break;
|
|
553
|
+
case 3:
|
|
554
|
+
reason =
|
|
555
|
+
'MEDIA_ERR_DECODE. This might be an issue with your video file.';
|
|
556
|
+
break;
|
|
557
|
+
case 4:
|
|
558
|
+
reason =
|
|
559
|
+
'MEDIA_ERR_SRC_NOT_SUPPORTED. If you are sure that the path to the video is correct, this might be a CORS error.';
|
|
560
|
+
break;
|
|
561
|
+
default:
|
|
562
|
+
reason = 'UNKNOWN';
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return reason;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Helper method to check if running on iOS
|
|
569
|
+
protected isIOS(): boolean {
|
|
570
|
+
if (typeof navigator === 'undefined') return false;
|
|
571
|
+
const isIos = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
|
572
|
+
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
|
573
|
+
|
|
574
|
+
return isIos;
|
|
575
|
+
}
|
|
576
|
+
}
|