@twick/2d 0.13.0 → 0.14.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.
@@ -8,7 +8,8 @@ import {ImageCommunication} from '../utils/video/ffmpeg-client';
8
8
  import {dropExtractor, getFrame} from '../utils/video/mp4-parser-manager';
9
9
  import type {MediaProps} from './Media';
10
10
  import {Media} from './Media';
11
-
11
+ import {waitUntil} from '../utils/waitUntil';
12
+
12
13
  export interface VideoProps extends MediaProps {
13
14
  /**
14
15
  * {@inheritDoc Video.alpha}
@@ -107,77 +108,74 @@ export class Video extends Media {
107
108
  @computed()
108
109
  private video(): HTMLVideoElement {
109
110
  const src = this.src();
110
-
111
- // Use a temporary key for undefined src to avoid conflicts
112
- const key = `${this.key}/${src || 'pending'}`;
113
-
111
+ const key = `${this.key}/${src}`;
114
112
  let video = Video.pool[key];
115
113
  if (!video) {
116
114
  video = document.createElement('video');
117
115
  video.crossOrigin = 'anonymous';
116
+ video.preload = 'metadata';
117
+ video.playsInline = true;
118
+ video.setAttribute('webkit-playsinline', 'true');
119
+ video.setAttribute('playsinline', 'true');
120
+
121
+ // Set initial volume
122
+ video.volume = this.getVolume();
123
+
124
+ const parsedSrc = new URL(src, window.location.origin);
125
+ if (parsedSrc.pathname.endsWith('.m3u8')) {
126
+ const hls = new Hls();
127
+ hls.loadSource(src);
128
+ hls.attachMedia(video);
129
+ } else {
130
+ video.src = src;
131
+ }
118
132
 
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;
133
+ // Add metadata event listeners
134
+ video.addEventListener('loadedmetadata', () => {
135
+ if (video.duration === Infinity || video.duration === 0) {
136
+ // For iOS, we need to seek to the end to get the duration
137
+ video.currentTime = 24 * 60 * 60; // 24 hours
134
138
  }
135
- }
139
+ });
136
140
 
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;
141
+ video.addEventListener('seeked', () => {
142
+ if (video.duration === Infinity || video.duration === 0) {
143
+ // If we still don't have duration, try a different approach
144
+ video.currentTime = 0;
149
145
  }
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
- }
146
+ });
147
+
148
+ // Add durationchange event listener
149
+ video.addEventListener('durationchange', () => {
150
+ if (video.duration === Infinity || video.duration === 0) {
151
+ // Try to force duration calculation
152
+ video.currentTime = 0.1;
153
+ }
154
+ });
155
+
156
+ // Add loadeddata event listener
157
+ video.addEventListener('loadeddata', () => {
158
+ if (video.duration === Infinity || video.duration === 0) {
159
+ // Try to force duration calculation
160
+ video.currentTime = 0.1;
161
+ }
162
+ });
163
+
164
+ // Add canplay event listener
165
+ video.addEventListener('canplay', () => {
166
+ if (video.duration === Infinity || video.duration === 0) {
167
+ // Try to force duration calculation
168
+ video.currentTime = 0.1;
169
+ }
170
+ });
160
171
 
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
- );
172
+ Video.pool[key] = video;
177
173
  }
178
174
 
175
+ // Update volume whenever video is accessed
176
+ video.volume = this.getVolume();
177
+
179
178
  const weNeedToWait = this.waitForCanPlayNecessary(video);
180
-
181
179
  if (!weNeedToWait) {
182
180
  return video;
183
181
  }
@@ -224,7 +222,6 @@ export class Video extends Media {
224
222
 
225
223
  const playing =
226
224
  this.playing() && time < video.duration && video.playbackRate > 0;
227
-
228
225
  if (playing) {
229
226
  if (video.paused) {
230
227
  DependencyContext.collectPromise(video.play());
@@ -336,9 +333,6 @@ export class Video extends Media {
336
333
  }
337
334
 
338
335
  protected override async draw(context: CanvasRenderingContext2D) {
339
- // Auto-start playback if Revideo is playing but media isn't
340
- this.autoPlayBasedOnRevideo();
341
-
342
336
  this.drawShape(context);
343
337
  const alpha = this.alpha();
344
338
  if (alpha > 0) {
@@ -364,23 +358,15 @@ export class Video extends Media {
364
358
 
365
359
  protected override applyFlex() {
366
360
  super.applyFlex();
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
- }
361
+ const video = this.video();
362
+ this.element.style.aspectRatio = (
363
+ this.ratio() ?? video.videoWidth / video.videoHeight
364
+ ).toString();
379
365
  }
380
366
 
381
367
  public override remove() {
382
368
  super.remove();
383
- dropExtractor(this.key, this.src());
369
+ dropExtractor(this.key, this.video().src);
384
370
  return this;
385
371
  }
386
372
 
@@ -458,4 +444,19 @@ export class Video extends Media {
458
444
  })(),
459
445
  );
460
446
  }
447
+
448
+ public *waitForMetadata() {
449
+ const video = this.video();
450
+
451
+ // If duration is already available and valid, return immediately
452
+ if (video.duration > 0 && video.duration !== Infinity) {
453
+ return;
454
+ }
455
+
456
+ // Try to force duration calculation
457
+ video.currentTime = 0.1;
458
+
459
+ // Wait for metadata to be loaded with a valid duration
460
+ yield* waitUntil(() => video.duration > 0 && video.duration !== Infinity);
461
+ }
461
462
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Waits until a condition is met.
3
+ * @param condition - Function that returns true when the condition is met
4
+ * @param timeout - Optional timeout in milliseconds
5
+ */
6
+ export function* waitUntil(
7
+ condition: () => boolean,
8
+ timeout: number = 10000,
9
+ ): Generator<void, void, unknown> {
10
+ const startTime = Date.now();
11
+
12
+ while (!condition()) {
13
+ if (Date.now() - startTime > timeout) {
14
+ throw new Error('Timeout waiting for condition');
15
+ }
16
+ yield;
17
+ }
18
+ }