@twick/2d 0.15.15 → 0.15.17

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/dist/index.d.cts CHANGED
@@ -86,8 +86,18 @@ declare abstract class Media extends Rect {
86
86
  protected abstract mediaElement(): HTMLMediaElement;
87
87
  protected abstract seekedMedia(): HTMLMediaElement;
88
88
  protected abstract fastSeekedMedia(): HTMLMediaElement;
89
+ /**
90
+ * Sync the underlying media element to the given time (e.g. draw/playback time).
91
+ * When `time` is passed (e.g. from Scene2D.draw), that value is used so sync
92
+ * does not depend on the node's time signal (which may only update on projectData change).
93
+ * When omitted, falls back to this.time().
94
+ * Uses skipCollectPromise so we don't leave a promise in DependencyContext.
95
+ */
96
+ syncToCurrentTime(time?: number): void;
89
97
  protected abstract draw(context: CanvasRenderingContext2D): Promise<void>;
90
- protected setCurrentTime(value: number): void;
98
+ protected setCurrentTime(value: number, options?: {
99
+ skipCollectPromise?: boolean;
100
+ }): void;
91
101
  setVolume(volume: number): void;
92
102
  protected amplify(node: HTMLMediaElement, volume: number): void;
93
103
  protected setPlaybackRate(playbackRate: number): void;
@@ -2528,6 +2538,11 @@ declare class Scene2D extends GeneratorScene<View2D> implements Inspectable {
2528
2538
  getNode(key: any): Node | null;
2529
2539
  getDetachedNodes(): Generator<Node, void, unknown>;
2530
2540
  getMediaAssets(): Array<AssetInfo>;
2541
+ /**
2542
+ * Seek all registered Media nodes to the current playback time.
2543
+ * Passes draw time (playback.time) so media sync does not depend on node time signal.
2544
+ */
2545
+ private syncAllMediaToCurrentTime;
2531
2546
  stopAllMedia(): void;
2532
2547
  adjustVolume(volumeScale: number): void;
2533
2548
  protected recreateView(): void;
package/dist/index.d.ts CHANGED
@@ -86,8 +86,18 @@ declare abstract class Media extends Rect {
86
86
  protected abstract mediaElement(): HTMLMediaElement;
87
87
  protected abstract seekedMedia(): HTMLMediaElement;
88
88
  protected abstract fastSeekedMedia(): HTMLMediaElement;
89
+ /**
90
+ * Sync the underlying media element to the given time (e.g. draw/playback time).
91
+ * When `time` is passed (e.g. from Scene2D.draw), that value is used so sync
92
+ * does not depend on the node's time signal (which may only update on projectData change).
93
+ * When omitted, falls back to this.time().
94
+ * Uses skipCollectPromise so we don't leave a promise in DependencyContext.
95
+ */
96
+ syncToCurrentTime(time?: number): void;
89
97
  protected abstract draw(context: CanvasRenderingContext2D): Promise<void>;
90
- protected setCurrentTime(value: number): void;
98
+ protected setCurrentTime(value: number, options?: {
99
+ skipCollectPromise?: boolean;
100
+ }): void;
91
101
  setVolume(volume: number): void;
92
102
  protected amplify(node: HTMLMediaElement, volume: number): void;
93
103
  protected setPlaybackRate(playbackRate: number): void;
@@ -2528,6 +2538,11 @@ declare class Scene2D extends GeneratorScene<View2D> implements Inspectable {
2528
2538
  getNode(key: any): Node | null;
2529
2539
  getDetachedNodes(): Generator<Node, void, unknown>;
2530
2540
  getMediaAssets(): Array<AssetInfo>;
2541
+ /**
2542
+ * Seek all registered Media nodes to the current playback time.
2543
+ * Passes draw time (playback.time) so media sync does not depend on node time signal.
2544
+ */
2545
+ private syncAllMediaToCurrentTime;
2531
2546
  stopAllMedia(): void;
2532
2547
  adjustVolume(volumeScale: number): void;
2533
2548
  protected recreateView(): void;
package/dist/index.js CHANGED
@@ -5402,7 +5402,7 @@ var Media = class extends Rect {
5402
5402
  this.lastTime = -1;
5403
5403
  this.isSchedulingPlay = false;
5404
5404
  if (!this.awaitCanPlay()) {
5405
- this.scheduleSeek(this.time());
5405
+ setTimeout(() => this.scheduleSeek(this.time()), 0);
5406
5406
  }
5407
5407
  if (props.play) {
5408
5408
  this.play();
@@ -5447,13 +5447,35 @@ var Media = class extends Rect {
5447
5447
  completion() {
5448
5448
  return this.clampTime(this.time()) / this.getDuration();
5449
5449
  }
5450
- setCurrentTime(value) {
5450
+ /**
5451
+ * Sync the underlying media element to the given time (e.g. draw/playback time).
5452
+ * When `time` is passed (e.g. from Scene2D.draw), that value is used so sync
5453
+ * does not depend on the node's time signal (which may only update on projectData change).
5454
+ * When omitted, falls back to this.time().
5455
+ * Uses skipCollectPromise so we don't leave a promise in DependencyContext.
5456
+ */
5457
+ syncToCurrentTime(time) {
5458
+ const syncTime = time ?? this.time();
5459
+ this.setCurrentTime(syncTime, { skipCollectPromise: true });
5460
+ }
5461
+ setCurrentTime(value, options) {
5451
5462
  try {
5452
5463
  const media = this.mediaElement();
5453
- if (media.readyState < 2) return;
5464
+ const key = this.key ?? "media";
5465
+ if (media.readyState < 2) {
5466
+ this.lastTime = value;
5467
+ this.time(value);
5468
+ this.waitForCanPlay(media, () => {
5469
+ media.currentTime = value;
5470
+ this.lastTime = value;
5471
+ this.time(value);
5472
+ });
5473
+ return;
5474
+ }
5454
5475
  media.currentTime = value;
5455
5476
  this.lastTime = value;
5456
- if (media.seeking) {
5477
+ this.time(value);
5478
+ if (media.seeking && !options?.skipCollectPromise) {
5457
5479
  DependencyContext4.collectPromise(
5458
5480
  new Promise((resolve) => {
5459
5481
  const listener = () => {
@@ -5466,6 +5488,7 @@ var Media = class extends Rect {
5466
5488
  }
5467
5489
  } catch (error) {
5468
5490
  this.lastTime = value;
5491
+ this.time(value);
5469
5492
  }
5470
5493
  }
5471
5494
  setVolume(volume) {
@@ -10397,11 +10420,13 @@ var Video = class extends Media {
10397
10420
  fastSeekedVideo() {
10398
10421
  const video = this.video();
10399
10422
  const time = this.clampTime(this.time());
10423
+ const playing = this.playing() && time < video.duration && video.playbackRate > 0;
10424
+ const wouldResetToZero = time < 0.5 && video.currentTime > 1 && Math.abs(video.currentTime - this.lastTime) < 0.5;
10425
+ const outOfSyncBig = Math.abs(video.currentTime - time) > 1;
10400
10426
  video.playbackRate = this.playbackRate();
10401
10427
  if (this.lastTime === time) {
10402
10428
  return video;
10403
10429
  }
10404
- const playing = this.playing() && time < video.duration && video.playbackRate > 0;
10405
10430
  if (playing) {
10406
10431
  if (video.paused) {
10407
10432
  DependencyContext8.collectPromise(video.play());
@@ -10411,10 +10436,11 @@ var Video = class extends Media {
10411
10436
  video.pause();
10412
10437
  }
10413
10438
  }
10414
- if (Math.abs(video.currentTime - time) > 1) {
10439
+ if (wouldResetToZero) {
10440
+ } else if (outOfSyncBig) {
10415
10441
  this.setCurrentTime(time);
10416
10442
  } else if (!playing) {
10417
- video.currentTime = time;
10443
+ this.setCurrentTime(time);
10418
10444
  }
10419
10445
  return video;
10420
10446
  }
@@ -10641,6 +10667,9 @@ var Scene2D = class extends GeneratorScene {
10641
10667
  context.save();
10642
10668
  this.renderLifecycle.dispatch([SceneRenderEvent.BeginRender, context]);
10643
10669
  this.getView().playbackState(this.playback.state).globalTime(this.playback.time).fps(this.playback.fps);
10670
+ if (this.playback.state === PlaybackState5.Paused) {
10671
+ this.syncAllMediaToCurrentTime();
10672
+ }
10644
10673
  await this.getView().render(context);
10645
10674
  this.renderLifecycle.dispatch([SceneRenderEvent.FinishRender, context]);
10646
10675
  context.restore();
@@ -10771,6 +10800,26 @@ var Scene2D = class extends GeneratorScene {
10771
10800
  );
10772
10801
  return returnObjects;
10773
10802
  }
10803
+ /**
10804
+ * Seek all registered Media nodes to the current playback time.
10805
+ * Passes draw time (playback.time) so media sync does not depend on node time signal.
10806
+ */
10807
+ syncAllMediaToCurrentTime() {
10808
+ const drawTime = this.playback.time;
10809
+ const mediaNodes = Array.from(this.registeredNodes.values()).filter(
10810
+ (node) => node instanceof Media
10811
+ );
10812
+ for (const media of mediaNodes) {
10813
+ try {
10814
+ media.syncToCurrentTime(drawTime);
10815
+ } catch (e) {
10816
+ this.logger.warn({
10817
+ message: `syncAllMediaToCurrentTime: skipped node ${media.key ?? "unknown"}`,
10818
+ object: e
10819
+ });
10820
+ }
10821
+ }
10822
+ }
10774
10823
  stopAllMedia() {
10775
10824
  const playingMedia = Array.from(this.registeredNodes.values()).filter((node) => node instanceof Media).filter((video) => video.isPlaying());
10776
10825
  for (const media of playingMedia) {