@twick/2d 0.12.0 → 0.13.0
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/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/Media.d.ts +6 -1
- package/lib/components/Media.d.ts.map +1 -1
- package/lib/components/Media.js +245 -55
- package/lib/components/Video.d.ts.map +1 -1
- package/lib/components/Video.js +71 -12
- package/lib/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +6 -7
- package/src/lib/components/Audio.ts +39 -2
- package/src/lib/components/Media.ts +261 -64
- package/src/lib/components/Video.ts +77 -13
- package/LICENSE +0 -21
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twick/2d",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "A 2D renderer for twick",
|
|
5
5
|
"author": "twick",
|
|
6
6
|
"homepage": "https://re.video/",
|
|
7
|
-
"bugs": "https://github.com/
|
|
7
|
+
"bugs": "https://github.com/ncounterspecialist/twick-base/issues",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"main": "lib/index.js",
|
|
10
10
|
"types": "./lib/index.d.ts",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"sideEffects": false,
|
|
20
20
|
"repository": {
|
|
21
21
|
"type": "git",
|
|
22
|
-
"url": "git+https://github.com/
|
|
22
|
+
"url": "git+https://github.com/ncounterspecialist/twick-base.git"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"lib",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@preact/signals": "^1.2.1",
|
|
32
32
|
"@rollup/plugin-node-resolve": "^15.2.4",
|
|
33
33
|
"@rollup/plugin-typescript": "^12.1.0",
|
|
34
|
-
"@twick/ui": "^0.
|
|
34
|
+
"@twick/ui": "^0.13.0",
|
|
35
35
|
"clsx": "^2.0.0",
|
|
36
36
|
"jsdom": "^22.1.0",
|
|
37
37
|
"preact": "^10.19.2",
|
|
@@ -42,12 +42,11 @@
|
|
|
42
42
|
"@lezer/common": "^1.2.1",
|
|
43
43
|
"@lezer/highlight": "^1.2.0",
|
|
44
44
|
"@rive-app/canvas-advanced": "2.7.3",
|
|
45
|
-
"@twick/core": "^0.
|
|
45
|
+
"@twick/core": "^0.13.0",
|
|
46
46
|
"code-fns": "^0.8.2",
|
|
47
47
|
"hls.js": "^1.5.11",
|
|
48
48
|
"mathjax-full": "^3.2.2",
|
|
49
49
|
"mp4box": "^0.5.2",
|
|
50
50
|
"parse-svg-path": "^0.1.2"
|
|
51
|
-
}
|
|
52
|
-
"gitHead": "59f38f4e7d3a9a30943bbad830dc0201eaa57ce7"
|
|
51
|
+
}
|
|
53
52
|
}
|
|
@@ -26,13 +26,47 @@ export class Audio extends Media {
|
|
|
26
26
|
@computed()
|
|
27
27
|
protected audio(): HTMLAudioElement {
|
|
28
28
|
const src = this.src();
|
|
29
|
-
|
|
29
|
+
|
|
30
|
+
// Use a temporary key for undefined src to avoid conflicts
|
|
31
|
+
const key = `${this.key}/${src || 'pending'}`;
|
|
32
|
+
|
|
30
33
|
let audio = Audio.pool[key];
|
|
31
34
|
if (!audio) {
|
|
32
35
|
audio = document.createElement('audio');
|
|
33
36
|
audio.crossOrigin = 'anonymous';
|
|
34
|
-
|
|
37
|
+
|
|
38
|
+
// Only set src if it's valid, otherwise leave it empty
|
|
39
|
+
if (src && src !== 'undefined') {
|
|
40
|
+
audio.src = src;
|
|
41
|
+
}
|
|
42
|
+
|
|
35
43
|
Audio.pool[key] = audio;
|
|
44
|
+
} else if (src && src !== 'undefined' && audio.src !== src) {
|
|
45
|
+
// Update existing audio element if src has changed and is now valid
|
|
46
|
+
audio.src = src;
|
|
47
|
+
|
|
48
|
+
// Move audio to correct pool key
|
|
49
|
+
delete Audio.pool[key];
|
|
50
|
+
const newKey = `${this.key}/${src}`;
|
|
51
|
+
Audio.pool[newKey] = audio;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// If src is still undefined, wait for it to become available
|
|
55
|
+
if (!src || src === 'undefined') {
|
|
56
|
+
DependencyContext.collectPromise(
|
|
57
|
+
new Promise<void>(resolve => {
|
|
58
|
+
// Check periodically for valid src
|
|
59
|
+
const checkSrc = () => {
|
|
60
|
+
const currentSrc = this.src();
|
|
61
|
+
if (currentSrc && currentSrc !== 'undefined') {
|
|
62
|
+
resolve();
|
|
63
|
+
} else {
|
|
64
|
+
setTimeout(checkSrc, 10);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
checkSrc();
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
36
70
|
}
|
|
37
71
|
|
|
38
72
|
const weNeedToWait = this.waitForCanPlayNecessary(audio);
|
|
@@ -116,6 +150,9 @@ export class Audio extends Media {
|
|
|
116
150
|
}
|
|
117
151
|
|
|
118
152
|
protected override async draw(context: CanvasRenderingContext2D) {
|
|
153
|
+
// Auto-start playback if Revideo is playing but media isn't
|
|
154
|
+
this.autoPlayBasedOnRevideo();
|
|
155
|
+
|
|
119
156
|
const playbackState = this.view().playbackState();
|
|
120
157
|
|
|
121
158
|
playbackState === PlaybackState.Playing ||
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
DependencyContext,
|
|
4
4
|
PlaybackState,
|
|
5
5
|
clamp,
|
|
6
|
-
isReactive,
|
|
6
|
+
isReactive,
|
|
7
7
|
useLogger,
|
|
8
8
|
useThread,
|
|
9
9
|
} from '@twick/core';
|
|
@@ -48,6 +48,7 @@ video.playbackRate(mySignal());
|
|
|
48
48
|
|
|
49
49
|
@nodeName('Media')
|
|
50
50
|
export abstract class Media extends Rect {
|
|
51
|
+
@initial('')
|
|
51
52
|
@signal()
|
|
52
53
|
public declare readonly src: SimpleSignal<string, this>;
|
|
53
54
|
|
|
@@ -89,9 +90,11 @@ export abstract class Media extends Rect {
|
|
|
89
90
|
}
|
|
90
91
|
> = {};
|
|
91
92
|
protected lastTime = -1;
|
|
93
|
+
private isSchedulingPlay = false;
|
|
92
94
|
|
|
93
95
|
public constructor(props: MediaProps) {
|
|
94
96
|
super(props);
|
|
97
|
+
|
|
95
98
|
if (!this.awaitCanPlay()) {
|
|
96
99
|
this.scheduleSeek(this.time());
|
|
97
100
|
}
|
|
@@ -100,9 +103,12 @@ export abstract class Media extends Rect {
|
|
|
100
103
|
this.play();
|
|
101
104
|
}
|
|
102
105
|
this.volume = props.volume ?? 1;
|
|
103
|
-
|
|
106
|
+
// Only set volume immediately if media is ready
|
|
107
|
+
if (!this.awaitCanPlay()) {
|
|
108
|
+
this.setVolume(this.volume);
|
|
109
|
+
}
|
|
104
110
|
}
|
|
105
|
-
|
|
111
|
+
|
|
106
112
|
public isPlaying(): boolean {
|
|
107
113
|
return this.playing();
|
|
108
114
|
}
|
|
@@ -112,10 +118,15 @@ export abstract class Media extends Rect {
|
|
|
112
118
|
}
|
|
113
119
|
|
|
114
120
|
public getDuration(): number {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
+
}
|
|
119
130
|
}
|
|
120
131
|
|
|
121
132
|
public getVolume(): number {
|
|
@@ -123,11 +134,18 @@ export abstract class Media extends Rect {
|
|
|
123
134
|
}
|
|
124
135
|
|
|
125
136
|
public getUrl(): string {
|
|
126
|
-
|
|
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
|
+
}
|
|
127
143
|
}
|
|
128
144
|
|
|
129
145
|
public override dispose() {
|
|
130
|
-
|
|
146
|
+
// Set playing state to false without trying to access media element
|
|
147
|
+
this.playing(false);
|
|
148
|
+
this.time.save();
|
|
131
149
|
this.remove();
|
|
132
150
|
super.dispose();
|
|
133
151
|
}
|
|
@@ -148,21 +166,26 @@ export abstract class Media extends Rect {
|
|
|
148
166
|
): Promise<void>;
|
|
149
167
|
|
|
150
168
|
protected setCurrentTime(value: number) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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;
|
|
166
189
|
}
|
|
167
190
|
}
|
|
168
191
|
|
|
@@ -172,17 +195,26 @@ export abstract class Media extends Rect {
|
|
|
172
195
|
`volumes cannot be negative - the value will be clamped to 0.`,
|
|
173
196
|
);
|
|
174
197
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
+
);
|
|
182
214
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
186
218
|
}
|
|
187
219
|
}
|
|
188
220
|
|
|
@@ -245,12 +277,22 @@ export abstract class Media extends Rect {
|
|
|
245
277
|
}
|
|
246
278
|
|
|
247
279
|
protected scheduleSeek(time: number) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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);
|
|
254
296
|
}
|
|
255
297
|
|
|
256
298
|
/**
|
|
@@ -261,23 +303,32 @@ export abstract class Media extends Rect {
|
|
|
261
303
|
* @returns
|
|
262
304
|
*/
|
|
263
305
|
protected waitForCanPlay(media: HTMLMediaElement, onCanPlay: () => void) {
|
|
264
|
-
|
|
306
|
+
// Be more strict - require readyState >= 3 (HAVE_FUTURE_DATA) for better reliability
|
|
307
|
+
if (media.readyState >= 3) {
|
|
265
308
|
onCanPlay();
|
|
266
309
|
return;
|
|
267
310
|
}
|
|
268
|
-
|
|
311
|
+
|
|
269
312
|
const onCanPlayWrapper = () => {
|
|
270
313
|
onCanPlay();
|
|
271
314
|
media.removeEventListener('canplay', onCanPlayWrapper);
|
|
315
|
+
media.removeEventListener('canplaythrough', onCanPlayWrapper);
|
|
272
316
|
};
|
|
273
317
|
|
|
274
318
|
const onError = () => {
|
|
275
319
|
const reason = this.getErrorReason(media.error?.code);
|
|
276
|
-
|
|
320
|
+
const srcValue = this.src();
|
|
321
|
+
|
|
322
|
+
console.log(`ERROR: Error loading video: src="${srcValue}", ${reason}`);
|
|
323
|
+
console.log(`Media element src: "${media.src}"`);
|
|
277
324
|
media.removeEventListener('error', onError);
|
|
325
|
+
media.removeEventListener('canplay', onCanPlayWrapper);
|
|
326
|
+
media.removeEventListener('canplaythrough', onCanPlayWrapper);
|
|
278
327
|
};
|
|
279
328
|
|
|
329
|
+
// Listen for both canplay and canplaythrough events
|
|
280
330
|
media.addEventListener('canplay', onCanPlayWrapper);
|
|
331
|
+
media.addEventListener('canplaythrough', onCanPlayWrapper);
|
|
281
332
|
media.addEventListener('error', onError);
|
|
282
333
|
}
|
|
283
334
|
|
|
@@ -295,42 +346,167 @@ export abstract class Media extends Rect {
|
|
|
295
346
|
);
|
|
296
347
|
}
|
|
297
348
|
|
|
298
|
-
public
|
|
299
|
-
|
|
349
|
+
public play() {
|
|
350
|
+
console.log('=== Media.play() called ===');
|
|
351
|
+
// Set the playing state first
|
|
352
|
+
this.playing(true);
|
|
300
353
|
|
|
301
|
-
|
|
302
|
-
this.
|
|
303
|
-
media.pause();
|
|
354
|
+
// Schedule the actual play operation for when media is ready
|
|
355
|
+
this.schedulePlay();
|
|
304
356
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
+
}
|
|
308
455
|
|
|
309
456
|
// Set playback rate on media element
|
|
310
457
|
media.playbackRate = this.playbackRate();
|
|
311
458
|
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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');
|
|
319
479
|
}
|
|
320
480
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
// Update time based on thread time
|
|
324
|
-
const time = useThread().time;
|
|
325
|
-
const start = time();
|
|
481
|
+
// Set up time synchronization
|
|
482
|
+
const start = timeFunction();
|
|
326
483
|
const offset = media.currentTime;
|
|
327
484
|
|
|
485
|
+
// Update time signal
|
|
328
486
|
this.time(() => {
|
|
329
|
-
const newTime = this.clampTime(offset + (
|
|
487
|
+
const newTime = this.clampTime(offset + (timeFunction() - start) * this.playbackRate());
|
|
330
488
|
return newTime;
|
|
331
489
|
});
|
|
332
490
|
}
|
|
333
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
|
+
|
|
334
510
|
public clampTime(time: number): number {
|
|
335
511
|
const duration = this.getDuration();
|
|
336
512
|
if (this.loop()) {
|
|
@@ -342,6 +518,27 @@ export abstract class Media extends Rect {
|
|
|
342
518
|
protected override collectAsyncResources() {
|
|
343
519
|
super.collectAsyncResources();
|
|
344
520
|
this.seekedMedia();
|
|
521
|
+
// Ensure volume is set when media becomes available
|
|
522
|
+
this.setVolume(this.volume);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
protected autoPlayBasedOnRevideo() {
|
|
526
|
+
// Auto-start/stop playback based on Revideo's playback state
|
|
527
|
+
const playbackState = this.view().playbackState();
|
|
528
|
+
// console.log('autoPlayBasedOnRevideo 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
|
+
}
|
|
345
542
|
}
|
|
346
543
|
|
|
347
544
|
protected getErrorReason(errCode?: number) {
|
|
@@ -107,25 +107,77 @@ export class Video extends Media {
|
|
|
107
107
|
@computed()
|
|
108
108
|
private video(): HTMLVideoElement {
|
|
109
109
|
const src = this.src();
|
|
110
|
-
|
|
110
|
+
|
|
111
|
+
// Use a temporary key for undefined src to avoid conflicts
|
|
112
|
+
const key = `${this.key}/${src || 'pending'}`;
|
|
113
|
+
|
|
111
114
|
let video = Video.pool[key];
|
|
112
115
|
if (!video) {
|
|
113
116
|
video = document.createElement('video');
|
|
114
117
|
video.crossOrigin = 'anonymous';
|
|
115
118
|
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
// Only set src if it's valid, otherwise leave it empty
|
|
120
|
+
if (src && src !== 'undefined') {
|
|
121
|
+
try {
|
|
122
|
+
const parsedSrc = new URL(src, window.location.origin);
|
|
123
|
+
|
|
124
|
+
if (parsedSrc.pathname.endsWith('.m3u8')) {
|
|
125
|
+
const hls = new Hls();
|
|
126
|
+
hls.loadSource(src);
|
|
127
|
+
hls.attachMedia(video);
|
|
128
|
+
} else {
|
|
129
|
+
video.src = src;
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
// Fallback to direct assignment
|
|
133
|
+
video.src = src;
|
|
134
|
+
}
|
|
123
135
|
}
|
|
124
136
|
|
|
125
137
|
Video.pool[key] = video;
|
|
138
|
+
} else if (src && src !== 'undefined' && video.src !== src) {
|
|
139
|
+
// Update existing video element if src has changed and is now valid
|
|
140
|
+
try {
|
|
141
|
+
const parsedSrc = new URL(src, window.location.origin);
|
|
142
|
+
|
|
143
|
+
if (parsedSrc.pathname.endsWith('.m3u8')) {
|
|
144
|
+
const hls = new Hls();
|
|
145
|
+
hls.loadSource(src);
|
|
146
|
+
hls.attachMedia(video);
|
|
147
|
+
} else {
|
|
148
|
+
video.src = src;
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
// Fallback to direct assignment
|
|
152
|
+
video.src = src;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Move video to correct pool key
|
|
156
|
+
delete Video.pool[key];
|
|
157
|
+
const newKey = `${this.key}/${src}`;
|
|
158
|
+
Video.pool[newKey] = video;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// If src is still undefined, wait for it to become available
|
|
162
|
+
if (!src || src === 'undefined') {
|
|
163
|
+
DependencyContext.collectPromise(
|
|
164
|
+
new Promise<void>(resolve => {
|
|
165
|
+
// Check periodically for valid src
|
|
166
|
+
const checkSrc = () => {
|
|
167
|
+
const currentSrc = this.src();
|
|
168
|
+
if (currentSrc && currentSrc !== 'undefined') {
|
|
169
|
+
resolve();
|
|
170
|
+
} else {
|
|
171
|
+
setTimeout(checkSrc, 10);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
checkSrc();
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
126
177
|
}
|
|
127
178
|
|
|
128
179
|
const weNeedToWait = this.waitForCanPlayNecessary(video);
|
|
180
|
+
|
|
129
181
|
if (!weNeedToWait) {
|
|
130
182
|
return video;
|
|
131
183
|
}
|
|
@@ -172,6 +224,7 @@ export class Video extends Media {
|
|
|
172
224
|
|
|
173
225
|
const playing =
|
|
174
226
|
this.playing() && time < video.duration && video.playbackRate > 0;
|
|
227
|
+
|
|
175
228
|
if (playing) {
|
|
176
229
|
if (video.paused) {
|
|
177
230
|
DependencyContext.collectPromise(video.play());
|
|
@@ -283,6 +336,9 @@ export class Video extends Media {
|
|
|
283
336
|
}
|
|
284
337
|
|
|
285
338
|
protected override async draw(context: CanvasRenderingContext2D) {
|
|
339
|
+
// Auto-start playback if Revideo is playing but media isn't
|
|
340
|
+
this.autoPlayBasedOnRevideo();
|
|
341
|
+
|
|
286
342
|
this.drawShape(context);
|
|
287
343
|
const alpha = this.alpha();
|
|
288
344
|
if (alpha > 0) {
|
|
@@ -308,15 +364,23 @@ export class Video extends Media {
|
|
|
308
364
|
|
|
309
365
|
protected override applyFlex() {
|
|
310
366
|
super.applyFlex();
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
367
|
+
try {
|
|
368
|
+
const video = this.video();
|
|
369
|
+
// Only set aspect ratio if video element is available and has valid dimensions
|
|
370
|
+
if (video && video.videoWidth > 0 && video.videoHeight > 0) {
|
|
371
|
+
this.element.style.aspectRatio = (
|
|
372
|
+
this.ratio() ?? video.videoWidth / video.videoHeight
|
|
373
|
+
).toString();
|
|
374
|
+
}
|
|
375
|
+
} catch (error) {
|
|
376
|
+
// If video element is not ready yet, skip setting aspect ratio
|
|
377
|
+
// It will be set later when the video becomes available
|
|
378
|
+
}
|
|
315
379
|
}
|
|
316
380
|
|
|
317
381
|
public override remove() {
|
|
318
382
|
super.remove();
|
|
319
|
-
dropExtractor(this.key, this.
|
|
383
|
+
dropExtractor(this.key, this.src());
|
|
320
384
|
return this;
|
|
321
385
|
}
|
|
322
386
|
|