@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/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@twick/2d",
3
- "version": "0.12.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/havenhq/twick/issues",
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/havenhq/twick.git"
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.12.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.12.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
- const key = `${this.key}/${src}`;
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
- audio.src = src;
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
- this.setVolume(this.volume);
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
- const mElement = this.mediaElement();
116
- const isVideo = (mElement instanceof HTMLVideoElement);
117
- const isAudio = (mElement instanceof HTMLVideoElement);
118
- return (this.isIOS() && (isVideo || isAudio)) ? 2 /** dummy duration for iOS */ : this.mediaElement().duration;
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
- return this.mediaElement().src;
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
- this.pause();
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
- const media = this.mediaElement();
152
- if (media.readyState < 2) return;
153
-
154
- media.currentTime = value;
155
- this.lastTime = value;
156
- if (media.seeking) {
157
- DependencyContext.collectPromise(
158
- new Promise<void>(resolve => {
159
- const listener = () => {
160
- resolve();
161
- media.removeEventListener('seeked', listener);
162
- };
163
- media.addEventListener('seeked', listener);
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
- const media = this.mediaElement();
176
- media.volume = Math.min(Math.max(volume, 0), 1);
177
-
178
- if (volume > 1) {
179
- if (this.allowVolumeAmplificationInPreview()) {
180
- this.amplify(media, volume);
181
- return;
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
- console.warn(
184
- `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.`,
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
- this.waitForCanPlay(this.mediaElement(), () => {
249
- const media = this.mediaElement();
250
- // Wait until the media is ready to seek again as
251
- // setting the time before the video doesn't work reliably.
252
- media.currentTime = time;
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
- if (media.readyState >= 2) {
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
- console.error(`ERROR: Error loading video: ${this.src()}, ${reason}`);
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 pause() {
299
- const media = this.mediaElement();
349
+ public play() {
350
+ console.log('=== Media.play() called ===');
351
+ // Set the playing state first
352
+ this.playing(true);
300
353
 
301
- this.playing(false);
302
- this.time.save();
303
- media.pause();
354
+ // Schedule the actual play operation for when media is ready
355
+ this.schedulePlay();
304
356
  }
305
-
306
- public play() {
307
- const media = this.mediaElement();
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
- // Start playing the media element
313
- const playPromise = media.play();
314
- if (playPromise !== undefined) {
315
- playPromise.catch(error => {
316
- console.warn('Error playing media:', error);
317
- this.playing(false);
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
- this.playing(true);
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 + (time() - start) * this.playbackRate());
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
- const key = `${this.key}/${src}`;
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
- const parsedSrc = new URL(src, window.location.origin);
117
- if (parsedSrc.pathname.endsWith('.m3u8')) {
118
- const hls = new Hls();
119
- hls.loadSource(src);
120
- hls.attachMedia(video);
121
- } else {
122
- video.src = src;
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
- const video = this.video();
312
- this.element.style.aspectRatio = (
313
- this.ratio() ?? video.videoWidth / video.videoHeight
314
- ).toString();
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.video().src);
383
+ dropExtractor(this.key, this.src());
320
384
  return this;
321
385
  }
322
386