@twick/2d 0.15.16 → 0.15.18

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.cjs CHANGED
@@ -5502,13 +5502,17 @@ var Media = class extends Rect {
5502
5502
  super(props);
5503
5503
  this.lastTime = -1;
5504
5504
  this.isSchedulingPlay = false;
5505
+ /** Used with clipStart for sync (trim offset in source). */
5506
+ this.trimStart = 0;
5505
5507
  if (!this.awaitCanPlay()) {
5506
- this.scheduleSeek(this.time());
5508
+ setTimeout(() => this.scheduleSeek(this.time()), 0);
5507
5509
  }
5508
5510
  if (props.play) {
5509
5511
  this.play();
5510
5512
  }
5511
5513
  this.volume = props.volume ?? 1;
5514
+ this.clipStart = props.clipStart;
5515
+ this.trimStart = props.trimStart ?? 0;
5512
5516
  if (!this.awaitCanPlay()) {
5513
5517
  this.setVolume(this.volume);
5514
5518
  }
@@ -5548,25 +5552,72 @@ var Media = class extends Rect {
5548
5552
  completion() {
5549
5553
  return this.clampTime(this.time()) / this.getDuration();
5550
5554
  }
5551
- setCurrentTime(value) {
5555
+ /**
5556
+ * Sync the underlying media element to the given time (e.g. draw/playback time).
5557
+ * Used for both Video and Audio. When `time` is passed (e.g. from Scene2D.draw), that
5558
+ * value is used so sync does not depend on the node's time signal.
5559
+ * If this node has clipStart set, global time is converted to clip-relative time:
5560
+ * syncTime = trimStart + (time - clipStart) * playbackRate.
5561
+ * When omitted, falls back to this.time().
5562
+ * When waitForSeek is true, returns a promise that resolves when the seek completes so
5563
+ * the draw can wait and show the correct frame/sample when paused.
5564
+ */
5565
+ syncToCurrentTime(time, options) {
5566
+ let syncTime;
5567
+ if (time !== void 0 && this.clipStart !== void 0) {
5568
+ syncTime = this.trimStart + (time - this.clipStart) * this.playbackRate();
5569
+ } else {
5570
+ syncTime = time ?? this.time();
5571
+ }
5572
+ const promise = this.setCurrentTime(syncTime, {
5573
+ skipCollectPromise: options?.waitForSeek ?? true
5574
+ });
5575
+ if (options?.waitForSeek) {
5576
+ return promise;
5577
+ }
5578
+ }
5579
+ setCurrentTime(value, options) {
5552
5580
  try {
5553
5581
  const media = this.mediaElement();
5554
- if (media.readyState < 2) return;
5555
- media.currentTime = value;
5556
- this.lastTime = value;
5557
- if (media.seeking) {
5558
- import_core32.DependencyContext.collectPromise(
5559
- new Promise((resolve) => {
5560
- const listener = () => {
5582
+ const key = this.key ?? "media";
5583
+ if (media.readyState < 2) {
5584
+ this.lastTime = value;
5585
+ this.time(value);
5586
+ return new Promise((resolve) => {
5587
+ this.waitForCanPlay(media, () => {
5588
+ media.currentTime = value;
5589
+ this.lastTime = value;
5590
+ this.time(value);
5591
+ const onSeeked = () => {
5592
+ media.removeEventListener("seeked", onSeeked);
5561
5593
  resolve();
5562
- media.removeEventListener("seeked", listener);
5563
5594
  };
5564
- media.addEventListener("seeked", listener);
5565
- })
5566
- );
5595
+ if (media.seeking) {
5596
+ media.addEventListener("seeked", onSeeked);
5597
+ } else {
5598
+ resolve();
5599
+ }
5600
+ });
5601
+ });
5602
+ }
5603
+ media.currentTime = value;
5604
+ this.lastTime = value;
5605
+ this.time(value);
5606
+ const seekPromise = media.seeking ? new Promise((resolve) => {
5607
+ const listener = () => {
5608
+ resolve();
5609
+ media.removeEventListener("seeked", listener);
5610
+ };
5611
+ media.addEventListener("seeked", listener);
5612
+ }) : Promise.resolve();
5613
+ if (media.seeking && !options?.skipCollectPromise) {
5614
+ import_core32.DependencyContext.collectPromise(seekPromise);
5567
5615
  }
5616
+ return seekPromise;
5568
5617
  } catch (error) {
5569
5618
  this.lastTime = value;
5619
+ this.time(value);
5620
+ return Promise.resolve();
5570
5621
  }
5571
5622
  }
5572
5623
  setVolume(volume) {
@@ -10444,11 +10495,13 @@ var Video = class extends Media {
10444
10495
  fastSeekedVideo() {
10445
10496
  const video = this.video();
10446
10497
  const time = this.clampTime(this.time());
10498
+ const playing = this.playing() && time < video.duration && video.playbackRate > 0;
10499
+ const wouldResetToZero = time < 0.5 && video.currentTime > 1 && Math.abs(video.currentTime - this.lastTime) < 0.5;
10500
+ const outOfSyncBig = Math.abs(video.currentTime - time) > 1;
10447
10501
  video.playbackRate = this.playbackRate();
10448
10502
  if (this.lastTime === time) {
10449
10503
  return video;
10450
10504
  }
10451
- const playing = this.playing() && time < video.duration && video.playbackRate > 0;
10452
10505
  if (playing) {
10453
10506
  if (video.paused) {
10454
10507
  import_core59.DependencyContext.collectPromise(video.play());
@@ -10458,10 +10511,11 @@ var Video = class extends Media {
10458
10511
  video.pause();
10459
10512
  }
10460
10513
  }
10461
- if (Math.abs(video.currentTime - time) > 1) {
10514
+ if (wouldResetToZero) {
10515
+ } else if (outOfSyncBig) {
10462
10516
  this.setCurrentTime(time);
10463
10517
  } else if (!playing) {
10464
- video.currentTime = time;
10518
+ this.setCurrentTime(time);
10465
10519
  }
10466
10520
  return video;
10467
10521
  }
@@ -10702,6 +10756,9 @@ var Scene2D = class extends import_core60.GeneratorScene {
10702
10756
  context.save();
10703
10757
  this.renderLifecycle.dispatch([import_core60.SceneRenderEvent.BeginRender, context]);
10704
10758
  this.getView().playbackState(this.playback.state).globalTime(this.playback.time).fps(this.playback.fps);
10759
+ if (this.playback.state === import_core60.PlaybackState.Paused) {
10760
+ await this.syncAllMediaToCurrentTime(true);
10761
+ }
10705
10762
  await this.getView().render(context);
10706
10763
  this.renderLifecycle.dispatch([import_core60.SceneRenderEvent.FinishRender, context]);
10707
10764
  context.restore();
@@ -10832,6 +10889,34 @@ var Scene2D = class extends import_core60.GeneratorScene {
10832
10889
  );
10833
10890
  return returnObjects;
10834
10891
  }
10892
+ /**
10893
+ * Seek all registered Media nodes (Video and Audio) to the current playback time.
10894
+ * Passes draw time; nodes with clipStart convert it to clip-relative time.
10895
+ * When waitForSeek is true, waits for each media seek to complete so the next draw shows the correct frame/sample.
10896
+ */
10897
+ async syncAllMediaToCurrentTime(waitForSeek) {
10898
+ const drawTime = this.playback.time;
10899
+ const mediaNodes = Array.from(this.registeredNodes.values()).filter(
10900
+ (node) => node instanceof Media
10901
+ );
10902
+ const results = mediaNodes.map((media) => {
10903
+ try {
10904
+ return media.syncToCurrentTime(drawTime, { waitForSeek });
10905
+ } catch (e) {
10906
+ this.logger.warn({
10907
+ message: `syncAllMediaToCurrentTime: skipped node ${media.key ?? "unknown"}`,
10908
+ object: e
10909
+ });
10910
+ return void 0;
10911
+ }
10912
+ });
10913
+ if (waitForSeek) {
10914
+ const promises = results.filter(
10915
+ (p) => p !== void 0
10916
+ );
10917
+ await Promise.all(promises);
10918
+ }
10919
+ }
10835
10920
  stopAllMedia() {
10836
10921
  const playingMedia = Array.from(this.registeredNodes.values()).filter((node) => node instanceof Media).filter((video) => video.isPlaying());
10837
10922
  for (const media of playingMedia) {