@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 +101 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -1
- package/dist/index.d.ts +30 -1
- package/dist/index.js +101 -16
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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;
|
|
@@ -86,8 +94,23 @@ declare abstract class Media extends Rect {
|
|
|
86
94
|
protected abstract mediaElement(): HTMLMediaElement;
|
|
87
95
|
protected abstract seekedMedia(): HTMLMediaElement;
|
|
88
96
|
protected abstract fastSeekedMedia(): HTMLMediaElement;
|
|
97
|
+
/**
|
|
98
|
+
* Sync the underlying media element to the given time (e.g. draw/playback time).
|
|
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.
|
|
103
|
+
* When omitted, falls back to this.time().
|
|
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.
|
|
106
|
+
*/
|
|
107
|
+
syncToCurrentTime(time?: number, options?: {
|
|
108
|
+
waitForSeek?: boolean;
|
|
109
|
+
}): void | Promise<void>;
|
|
89
110
|
protected abstract draw(context: CanvasRenderingContext2D): Promise<void>;
|
|
90
|
-
protected setCurrentTime(value: number
|
|
111
|
+
protected setCurrentTime(value: number, options?: {
|
|
112
|
+
skipCollectPromise?: boolean;
|
|
113
|
+
}): Promise<void>;
|
|
91
114
|
setVolume(volume: number): void;
|
|
92
115
|
protected amplify(node: HTMLMediaElement, volume: number): void;
|
|
93
116
|
protected setPlaybackRate(playbackRate: number): void;
|
|
@@ -2528,6 +2551,12 @@ declare class Scene2D extends GeneratorScene<View2D> implements Inspectable {
|
|
|
2528
2551
|
getNode(key: any): Node | null;
|
|
2529
2552
|
getDetachedNodes(): Generator<Node, void, unknown>;
|
|
2530
2553
|
getMediaAssets(): Array<AssetInfo>;
|
|
2554
|
+
/**
|
|
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.
|
|
2558
|
+
*/
|
|
2559
|
+
private syncAllMediaToCurrentTime;
|
|
2531
2560
|
stopAllMedia(): void;
|
|
2532
2561
|
adjustVolume(volumeScale: number): void;
|
|
2533
2562
|
protected recreateView(): 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;
|
|
@@ -86,8 +94,23 @@ declare abstract class Media extends Rect {
|
|
|
86
94
|
protected abstract mediaElement(): HTMLMediaElement;
|
|
87
95
|
protected abstract seekedMedia(): HTMLMediaElement;
|
|
88
96
|
protected abstract fastSeekedMedia(): HTMLMediaElement;
|
|
97
|
+
/**
|
|
98
|
+
* Sync the underlying media element to the given time (e.g. draw/playback time).
|
|
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.
|
|
103
|
+
* When omitted, falls back to this.time().
|
|
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.
|
|
106
|
+
*/
|
|
107
|
+
syncToCurrentTime(time?: number, options?: {
|
|
108
|
+
waitForSeek?: boolean;
|
|
109
|
+
}): void | Promise<void>;
|
|
89
110
|
protected abstract draw(context: CanvasRenderingContext2D): Promise<void>;
|
|
90
|
-
protected setCurrentTime(value: number
|
|
111
|
+
protected setCurrentTime(value: number, options?: {
|
|
112
|
+
skipCollectPromise?: boolean;
|
|
113
|
+
}): Promise<void>;
|
|
91
114
|
setVolume(volume: number): void;
|
|
92
115
|
protected amplify(node: HTMLMediaElement, volume: number): void;
|
|
93
116
|
protected setPlaybackRate(playbackRate: number): void;
|
|
@@ -2528,6 +2551,12 @@ declare class Scene2D extends GeneratorScene<View2D> implements Inspectable {
|
|
|
2528
2551
|
getNode(key: any): Node | null;
|
|
2529
2552
|
getDetachedNodes(): Generator<Node, void, unknown>;
|
|
2530
2553
|
getMediaAssets(): Array<AssetInfo>;
|
|
2554
|
+
/**
|
|
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.
|
|
2558
|
+
*/
|
|
2559
|
+
private syncAllMediaToCurrentTime;
|
|
2531
2560
|
stopAllMedia(): void;
|
|
2532
2561
|
adjustVolume(volumeScale: number): void;
|
|
2533
2562
|
protected recreateView(): void;
|
package/dist/index.js
CHANGED
|
@@ -5401,13 +5401,17 @@ 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
|
-
this.scheduleSeek(this.time());
|
|
5407
|
+
setTimeout(() => this.scheduleSeek(this.time()), 0);
|
|
5406
5408
|
}
|
|
5407
5409
|
if (props.play) {
|
|
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
|
}
|
|
@@ -5447,25 +5451,72 @@ var Media = class extends Rect {
|
|
|
5447
5451
|
completion() {
|
|
5448
5452
|
return this.clampTime(this.time()) / this.getDuration();
|
|
5449
5453
|
}
|
|
5450
|
-
|
|
5454
|
+
/**
|
|
5455
|
+
* Sync the underlying media element to the given time (e.g. draw/playback time).
|
|
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.
|
|
5460
|
+
* When omitted, falls back to this.time().
|
|
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.
|
|
5463
|
+
*/
|
|
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
|
+
}
|
|
5477
|
+
}
|
|
5478
|
+
setCurrentTime(value, options) {
|
|
5451
5479
|
try {
|
|
5452
5480
|
const media = this.mediaElement();
|
|
5453
|
-
|
|
5454
|
-
media.
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5481
|
+
const key = this.key ?? "media";
|
|
5482
|
+
if (media.readyState < 2) {
|
|
5483
|
+
this.lastTime = value;
|
|
5484
|
+
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);
|
|
5460
5492
|
resolve();
|
|
5461
|
-
media.removeEventListener("seeked", listener);
|
|
5462
5493
|
};
|
|
5463
|
-
media.
|
|
5464
|
-
|
|
5465
|
-
|
|
5494
|
+
if (media.seeking) {
|
|
5495
|
+
media.addEventListener("seeked", onSeeked);
|
|
5496
|
+
} else {
|
|
5497
|
+
resolve();
|
|
5498
|
+
}
|
|
5499
|
+
});
|
|
5500
|
+
});
|
|
5501
|
+
}
|
|
5502
|
+
media.currentTime = value;
|
|
5503
|
+
this.lastTime = value;
|
|
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();
|
|
5512
|
+
if (media.seeking && !options?.skipCollectPromise) {
|
|
5513
|
+
DependencyContext4.collectPromise(seekPromise);
|
|
5466
5514
|
}
|
|
5515
|
+
return seekPromise;
|
|
5467
5516
|
} catch (error) {
|
|
5468
5517
|
this.lastTime = value;
|
|
5518
|
+
this.time(value);
|
|
5519
|
+
return Promise.resolve();
|
|
5469
5520
|
}
|
|
5470
5521
|
}
|
|
5471
5522
|
setVolume(volume) {
|
|
@@ -10397,11 +10448,13 @@ var Video = class extends Media {
|
|
|
10397
10448
|
fastSeekedVideo() {
|
|
10398
10449
|
const video = this.video();
|
|
10399
10450
|
const time = this.clampTime(this.time());
|
|
10451
|
+
const playing = this.playing() && time < video.duration && video.playbackRate > 0;
|
|
10452
|
+
const wouldResetToZero = time < 0.5 && video.currentTime > 1 && Math.abs(video.currentTime - this.lastTime) < 0.5;
|
|
10453
|
+
const outOfSyncBig = Math.abs(video.currentTime - time) > 1;
|
|
10400
10454
|
video.playbackRate = this.playbackRate();
|
|
10401
10455
|
if (this.lastTime === time) {
|
|
10402
10456
|
return video;
|
|
10403
10457
|
}
|
|
10404
|
-
const playing = this.playing() && time < video.duration && video.playbackRate > 0;
|
|
10405
10458
|
if (playing) {
|
|
10406
10459
|
if (video.paused) {
|
|
10407
10460
|
DependencyContext8.collectPromise(video.play());
|
|
@@ -10411,10 +10464,11 @@ var Video = class extends Media {
|
|
|
10411
10464
|
video.pause();
|
|
10412
10465
|
}
|
|
10413
10466
|
}
|
|
10414
|
-
if (
|
|
10467
|
+
if (wouldResetToZero) {
|
|
10468
|
+
} else if (outOfSyncBig) {
|
|
10415
10469
|
this.setCurrentTime(time);
|
|
10416
10470
|
} else if (!playing) {
|
|
10417
|
-
|
|
10471
|
+
this.setCurrentTime(time);
|
|
10418
10472
|
}
|
|
10419
10473
|
return video;
|
|
10420
10474
|
}
|
|
@@ -10641,6 +10695,9 @@ var Scene2D = class extends GeneratorScene {
|
|
|
10641
10695
|
context.save();
|
|
10642
10696
|
this.renderLifecycle.dispatch([SceneRenderEvent.BeginRender, context]);
|
|
10643
10697
|
this.getView().playbackState(this.playback.state).globalTime(this.playback.time).fps(this.playback.fps);
|
|
10698
|
+
if (this.playback.state === PlaybackState5.Paused) {
|
|
10699
|
+
await this.syncAllMediaToCurrentTime(true);
|
|
10700
|
+
}
|
|
10644
10701
|
await this.getView().render(context);
|
|
10645
10702
|
this.renderLifecycle.dispatch([SceneRenderEvent.FinishRender, context]);
|
|
10646
10703
|
context.restore();
|
|
@@ -10771,6 +10828,34 @@ var Scene2D = class extends GeneratorScene {
|
|
|
10771
10828
|
);
|
|
10772
10829
|
return returnObjects;
|
|
10773
10830
|
}
|
|
10831
|
+
/**
|
|
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.
|
|
10835
|
+
*/
|
|
10836
|
+
async syncAllMediaToCurrentTime(waitForSeek) {
|
|
10837
|
+
const drawTime = this.playback.time;
|
|
10838
|
+
const mediaNodes = Array.from(this.registeredNodes.values()).filter(
|
|
10839
|
+
(node) => node instanceof Media
|
|
10840
|
+
);
|
|
10841
|
+
const results = mediaNodes.map((media) => {
|
|
10842
|
+
try {
|
|
10843
|
+
return media.syncToCurrentTime(drawTime, { waitForSeek });
|
|
10844
|
+
} catch (e) {
|
|
10845
|
+
this.logger.warn({
|
|
10846
|
+
message: `syncAllMediaToCurrentTime: skipped node ${media.key ?? "unknown"}`,
|
|
10847
|
+
object: e
|
|
10848
|
+
});
|
|
10849
|
+
return void 0;
|
|
10850
|
+
}
|
|
10851
|
+
});
|
|
10852
|
+
if (waitForSeek) {
|
|
10853
|
+
const promises = results.filter(
|
|
10854
|
+
(p) => p !== void 0
|
|
10855
|
+
);
|
|
10856
|
+
await Promise.all(promises);
|
|
10857
|
+
}
|
|
10858
|
+
}
|
|
10774
10859
|
stopAllMedia() {
|
|
10775
10860
|
const playingMedia = Array.from(this.registeredNodes.values()).filter((node) => node instanceof Media).filter((video) => video.isPlaying());
|
|
10776
10861
|
for (const media of playingMedia) {
|