@twick/2d 0.15.17 → 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.d.cts CHANGED
@@ -58,6 +58,10 @@ interface MediaProps extends RectProps {
58
58
  play?: boolean;
59
59
  awaitCanPlay?: SignalValue<boolean>;
60
60
  allowVolumeAmplificationInPreview?: SignalValue<boolean>;
61
+ /** Timeline time when this clip starts; when set, syncToCurrentTime(globalTime) uses clip-relative time. */
62
+ clipStart?: number;
63
+ /** Start offset in the media file (e.g. trim); used with clipStart for sync. */
64
+ trimStart?: number;
61
65
  }
62
66
  declare abstract class Media extends Rect {
63
67
  readonly src: SimpleSignal<string, this>;
@@ -75,6 +79,10 @@ declare abstract class Media extends Rect {
75
79
  }>;
76
80
  protected lastTime: number;
77
81
  private isSchedulingPlay;
82
+ /** When set, syncToCurrentTime(globalTime) converts to clip-relative time. */
83
+ protected clipStart: number | undefined;
84
+ /** Used with clipStart for sync (trim offset in source). */
85
+ protected trimStart: number;
78
86
  constructor(props: MediaProps);
79
87
  isPlaying(): boolean;
80
88
  getCurrentTime(): number;
@@ -88,16 +96,21 @@ declare abstract class Media extends Rect {
88
96
  protected abstract fastSeekedMedia(): HTMLMediaElement;
89
97
  /**
90
98
  * 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).
99
+ * Used for both Video and Audio. When `time` is passed (e.g. from Scene2D.draw), that
100
+ * value is used so sync does not depend on the node's time signal.
101
+ * If this node has clipStart set, global time is converted to clip-relative time:
102
+ * syncTime = trimStart + (time - clipStart) * playbackRate.
93
103
  * When omitted, falls back to this.time().
94
- * Uses skipCollectPromise so we don't leave a promise in DependencyContext.
104
+ * When waitForSeek is true, returns a promise that resolves when the seek completes so
105
+ * the draw can wait and show the correct frame/sample when paused.
95
106
  */
96
- syncToCurrentTime(time?: number): void;
107
+ syncToCurrentTime(time?: number, options?: {
108
+ waitForSeek?: boolean;
109
+ }): void | Promise<void>;
97
110
  protected abstract draw(context: CanvasRenderingContext2D): Promise<void>;
98
111
  protected setCurrentTime(value: number, options?: {
99
112
  skipCollectPromise?: boolean;
100
- }): void;
113
+ }): Promise<void>;
101
114
  setVolume(volume: number): void;
102
115
  protected amplify(node: HTMLMediaElement, volume: number): void;
103
116
  protected setPlaybackRate(playbackRate: number): void;
@@ -2539,8 +2552,9 @@ declare class Scene2D extends GeneratorScene<View2D> implements Inspectable {
2539
2552
  getDetachedNodes(): Generator<Node, void, unknown>;
2540
2553
  getMediaAssets(): Array<AssetInfo>;
2541
2554
  /**
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.
2555
+ * Seek all registered Media nodes (Video and Audio) to the current playback time.
2556
+ * Passes draw time; nodes with clipStart convert it to clip-relative time.
2557
+ * When waitForSeek is true, waits for each media seek to complete so the next draw shows the correct frame/sample.
2544
2558
  */
2545
2559
  private syncAllMediaToCurrentTime;
2546
2560
  stopAllMedia(): void;
package/dist/index.d.ts CHANGED
@@ -58,6 +58,10 @@ interface MediaProps extends RectProps {
58
58
  play?: boolean;
59
59
  awaitCanPlay?: SignalValue<boolean>;
60
60
  allowVolumeAmplificationInPreview?: SignalValue<boolean>;
61
+ /** Timeline time when this clip starts; when set, syncToCurrentTime(globalTime) uses clip-relative time. */
62
+ clipStart?: number;
63
+ /** Start offset in the media file (e.g. trim); used with clipStart for sync. */
64
+ trimStart?: number;
61
65
  }
62
66
  declare abstract class Media extends Rect {
63
67
  readonly src: SimpleSignal<string, this>;
@@ -75,6 +79,10 @@ declare abstract class Media extends Rect {
75
79
  }>;
76
80
  protected lastTime: number;
77
81
  private isSchedulingPlay;
82
+ /** When set, syncToCurrentTime(globalTime) converts to clip-relative time. */
83
+ protected clipStart: number | undefined;
84
+ /** Used with clipStart for sync (trim offset in source). */
85
+ protected trimStart: number;
78
86
  constructor(props: MediaProps);
79
87
  isPlaying(): boolean;
80
88
  getCurrentTime(): number;
@@ -88,16 +96,21 @@ declare abstract class Media extends Rect {
88
96
  protected abstract fastSeekedMedia(): HTMLMediaElement;
89
97
  /**
90
98
  * 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).
99
+ * Used for both Video and Audio. When `time` is passed (e.g. from Scene2D.draw), that
100
+ * value is used so sync does not depend on the node's time signal.
101
+ * If this node has clipStart set, global time is converted to clip-relative time:
102
+ * syncTime = trimStart + (time - clipStart) * playbackRate.
93
103
  * When omitted, falls back to this.time().
94
- * Uses skipCollectPromise so we don't leave a promise in DependencyContext.
104
+ * When waitForSeek is true, returns a promise that resolves when the seek completes so
105
+ * the draw can wait and show the correct frame/sample when paused.
95
106
  */
96
- syncToCurrentTime(time?: number): void;
107
+ syncToCurrentTime(time?: number, options?: {
108
+ waitForSeek?: boolean;
109
+ }): void | Promise<void>;
97
110
  protected abstract draw(context: CanvasRenderingContext2D): Promise<void>;
98
111
  protected setCurrentTime(value: number, options?: {
99
112
  skipCollectPromise?: boolean;
100
- }): void;
113
+ }): Promise<void>;
101
114
  setVolume(volume: number): void;
102
115
  protected amplify(node: HTMLMediaElement, volume: number): void;
103
116
  protected setPlaybackRate(playbackRate: number): void;
@@ -2539,8 +2552,9 @@ declare class Scene2D extends GeneratorScene<View2D> implements Inspectable {
2539
2552
  getDetachedNodes(): Generator<Node, void, unknown>;
2540
2553
  getMediaAssets(): Array<AssetInfo>;
2541
2554
  /**
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.
2555
+ * Seek all registered Media nodes (Video and Audio) to the current playback time.
2556
+ * Passes draw time; nodes with clipStart convert it to clip-relative time.
2557
+ * When waitForSeek is true, waits for each media seek to complete so the next draw shows the correct frame/sample.
2544
2558
  */
2545
2559
  private syncAllMediaToCurrentTime;
2546
2560
  stopAllMedia(): void;
package/dist/index.js CHANGED
@@ -5401,6 +5401,8 @@ var Media = class extends Rect {
5401
5401
  super(props);
5402
5402
  this.lastTime = -1;
5403
5403
  this.isSchedulingPlay = false;
5404
+ /** Used with clipStart for sync (trim offset in source). */
5405
+ this.trimStart = 0;
5404
5406
  if (!this.awaitCanPlay()) {
5405
5407
  setTimeout(() => this.scheduleSeek(this.time()), 0);
5406
5408
  }
@@ -5408,6 +5410,8 @@ var Media = class extends Rect {
5408
5410
  this.play();
5409
5411
  }
5410
5412
  this.volume = props.volume ?? 1;
5413
+ this.clipStart = props.clipStart;
5414
+ this.trimStart = props.trimStart ?? 0;
5411
5415
  if (!this.awaitCanPlay()) {
5412
5416
  this.setVolume(this.volume);
5413
5417
  }
@@ -5449,14 +5453,27 @@ var Media = class extends Rect {
5449
5453
  }
5450
5454
  /**
5451
5455
  * 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).
5456
+ * Used for both Video and Audio. When `time` is passed (e.g. from Scene2D.draw), that
5457
+ * value is used so sync does not depend on the node's time signal.
5458
+ * If this node has clipStart set, global time is converted to clip-relative time:
5459
+ * syncTime = trimStart + (time - clipStart) * playbackRate.
5454
5460
  * When omitted, falls back to this.time().
5455
- * Uses skipCollectPromise so we don't leave a promise in DependencyContext.
5461
+ * When waitForSeek is true, returns a promise that resolves when the seek completes so
5462
+ * the draw can wait and show the correct frame/sample when paused.
5456
5463
  */
5457
- syncToCurrentTime(time) {
5458
- const syncTime = time ?? this.time();
5459
- this.setCurrentTime(syncTime, { skipCollectPromise: true });
5464
+ syncToCurrentTime(time, options) {
5465
+ let syncTime;
5466
+ if (time !== void 0 && this.clipStart !== void 0) {
5467
+ syncTime = this.trimStart + (time - this.clipStart) * this.playbackRate();
5468
+ } else {
5469
+ syncTime = time ?? this.time();
5470
+ }
5471
+ const promise = this.setCurrentTime(syncTime, {
5472
+ skipCollectPromise: options?.waitForSeek ?? true
5473
+ });
5474
+ if (options?.waitForSeek) {
5475
+ return promise;
5476
+ }
5460
5477
  }
5461
5478
  setCurrentTime(value, options) {
5462
5479
  try {
@@ -5465,30 +5482,41 @@ var Media = class extends Rect {
5465
5482
  if (media.readyState < 2) {
5466
5483
  this.lastTime = value;
5467
5484
  this.time(value);
5468
- this.waitForCanPlay(media, () => {
5469
- media.currentTime = value;
5470
- this.lastTime = value;
5471
- this.time(value);
5485
+ return new Promise((resolve) => {
5486
+ this.waitForCanPlay(media, () => {
5487
+ media.currentTime = value;
5488
+ this.lastTime = value;
5489
+ this.time(value);
5490
+ const onSeeked = () => {
5491
+ media.removeEventListener("seeked", onSeeked);
5492
+ resolve();
5493
+ };
5494
+ if (media.seeking) {
5495
+ media.addEventListener("seeked", onSeeked);
5496
+ } else {
5497
+ resolve();
5498
+ }
5499
+ });
5472
5500
  });
5473
- return;
5474
5501
  }
5475
5502
  media.currentTime = value;
5476
5503
  this.lastTime = value;
5477
5504
  this.time(value);
5505
+ const seekPromise = media.seeking ? new Promise((resolve) => {
5506
+ const listener = () => {
5507
+ resolve();
5508
+ media.removeEventListener("seeked", listener);
5509
+ };
5510
+ media.addEventListener("seeked", listener);
5511
+ }) : Promise.resolve();
5478
5512
  if (media.seeking && !options?.skipCollectPromise) {
5479
- DependencyContext4.collectPromise(
5480
- new Promise((resolve) => {
5481
- const listener = () => {
5482
- resolve();
5483
- media.removeEventListener("seeked", listener);
5484
- };
5485
- media.addEventListener("seeked", listener);
5486
- })
5487
- );
5513
+ DependencyContext4.collectPromise(seekPromise);
5488
5514
  }
5515
+ return seekPromise;
5489
5516
  } catch (error) {
5490
5517
  this.lastTime = value;
5491
5518
  this.time(value);
5519
+ return Promise.resolve();
5492
5520
  }
5493
5521
  }
5494
5522
  setVolume(volume) {
@@ -10668,7 +10696,7 @@ var Scene2D = class extends GeneratorScene {
10668
10696
  this.renderLifecycle.dispatch([SceneRenderEvent.BeginRender, context]);
10669
10697
  this.getView().playbackState(this.playback.state).globalTime(this.playback.time).fps(this.playback.fps);
10670
10698
  if (this.playback.state === PlaybackState5.Paused) {
10671
- this.syncAllMediaToCurrentTime();
10699
+ await this.syncAllMediaToCurrentTime(true);
10672
10700
  }
10673
10701
  await this.getView().render(context);
10674
10702
  this.renderLifecycle.dispatch([SceneRenderEvent.FinishRender, context]);
@@ -10801,23 +10829,31 @@ var Scene2D = class extends GeneratorScene {
10801
10829
  return returnObjects;
10802
10830
  }
10803
10831
  /**
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.
10832
+ * Seek all registered Media nodes (Video and Audio) to the current playback time.
10833
+ * Passes draw time; nodes with clipStart convert it to clip-relative time.
10834
+ * When waitForSeek is true, waits for each media seek to complete so the next draw shows the correct frame/sample.
10806
10835
  */
10807
- syncAllMediaToCurrentTime() {
10836
+ async syncAllMediaToCurrentTime(waitForSeek) {
10808
10837
  const drawTime = this.playback.time;
10809
10838
  const mediaNodes = Array.from(this.registeredNodes.values()).filter(
10810
10839
  (node) => node instanceof Media
10811
10840
  );
10812
- for (const media of mediaNodes) {
10841
+ const results = mediaNodes.map((media) => {
10813
10842
  try {
10814
- media.syncToCurrentTime(drawTime);
10843
+ return media.syncToCurrentTime(drawTime, { waitForSeek });
10815
10844
  } catch (e) {
10816
10845
  this.logger.warn({
10817
10846
  message: `syncAllMediaToCurrentTime: skipped node ${media.key ?? "unknown"}`,
10818
10847
  object: e
10819
10848
  });
10849
+ return void 0;
10820
10850
  }
10851
+ });
10852
+ if (waitForSeek) {
10853
+ const promises = results.filter(
10854
+ (p) => p !== void 0
10855
+ );
10856
+ await Promise.all(promises);
10821
10857
  }
10822
10858
  }
10823
10859
  stopAllMedia() {