@twick/2d 0.12.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.
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@twick/2d",
3
- "version": "0.12.0",
3
+ "version": "0.14.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.14.0",
35
35
  "clsx": "^2.0.0",
36
36
  "jsdom": "^22.1.0",
37
37
  "preact": "^10.19.2",
@@ -42,12 +42,12 @@
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.14.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
51
  },
52
- "gitHead": "59f38f4e7d3a9a30943bbad830dc0201eaa57ce7"
52
+ "gitHead": "54bd58bf80ea6afec48c27c061e6d455c1e0a885"
53
53
  }
@@ -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';
@@ -112,10 +112,7 @@ export abstract class Media extends Rect {
112
112
  }
113
113
 
114
114
  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;
115
+ return this.mediaElement().duration;
119
116
  }
120
117
 
121
118
  public getVolume(): number {
@@ -273,7 +270,7 @@ export abstract class Media extends Rect {
273
270
 
274
271
  const onError = () => {
275
272
  const reason = this.getErrorReason(media.error?.code);
276
- console.error(`ERROR: Error loading video: ${this.src()}, ${reason}`);
273
+ console.log(`ERROR: Error loading video: ${this.src()}, ${reason}`);
277
274
  media.removeEventListener('error', onError);
278
275
  };
279
276
 
@@ -295,40 +292,19 @@ export abstract class Media extends Rect {
295
292
  );
296
293
  }
297
294
 
298
- public pause() {
299
- const media = this.mediaElement();
300
-
301
- this.playing(false);
302
- this.time.save();
303
- media.pause();
304
- }
305
-
306
295
  public play() {
307
- const media = this.mediaElement();
308
-
309
- // Set playback rate on media element
310
- media.playbackRate = this.playbackRate();
311
-
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
- });
319
- }
320
-
321
- this.playing(true);
322
-
323
- // Update time based on thread time
324
296
  const time = useThread().time;
325
297
  const start = time();
326
- const offset = media.currentTime;
327
-
328
- this.time(() => {
329
- const newTime = this.clampTime(offset + (time() - start) * this.playbackRate());
330
- return newTime;
331
- });
298
+ const offset = this.time();
299
+ const playbackRate = this.playbackRate();
300
+ this.playing(true);
301
+ this.time(() => this.clampTime(offset + (time() - start) * playbackRate));
302
+ }
303
+
304
+ public pause() {
305
+ this.playing(false);
306
+ this.time.save();
307
+ this.mediaElement().pause();
332
308
  }
333
309
 
334
310
  public clampTime(time: number): number {
@@ -367,13 +343,4 @@ export abstract class Media extends Rect {
367
343
 
368
344
  return reason;
369
345
  }
370
-
371
- // Helper method to check if running on iOS
372
- protected isIOS(): boolean {
373
- if (typeof navigator === 'undefined') return false;
374
- const isIos = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
375
- (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
376
-
377
- return isIos;
378
- }
379
346
  }
@@ -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}
@@ -112,6 +113,13 @@ export class Video extends Media {
112
113
  if (!video) {
113
114
  video = document.createElement('video');
114
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();
115
123
 
116
124
  const parsedSrc = new URL(src, window.location.origin);
117
125
  if (parsedSrc.pathname.endsWith('.m3u8')) {
@@ -122,9 +130,51 @@ export class Video extends Media {
122
130
  video.src = src;
123
131
  }
124
132
 
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
138
+ }
139
+ });
140
+
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;
145
+ }
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
+ });
171
+
125
172
  Video.pool[key] = video;
126
173
  }
127
174
 
175
+ // Update volume whenever video is accessed
176
+ video.volume = this.getVolume();
177
+
128
178
  const weNeedToWait = this.waitForCanPlayNecessary(video);
129
179
  if (!weNeedToWait) {
130
180
  return video;
@@ -394,4 +444,19 @@ export class Video extends Media {
394
444
  })(),
395
445
  );
396
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
+ }
397
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
+ }