fundus 0.11.0 → 0.11.1
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/README.md +4 -3
- package/build/client/_app/immutable/assets/LibraryContextMenu.B6By2-yb.css +1 -0
- package/build/client/_app/immutable/assets/LibraryContextMenu.B6By2-yb.css.br +0 -0
- package/build/client/_app/immutable/assets/LibraryContextMenu.B6By2-yb.css.gz +0 -0
- package/build/client/_app/immutable/chunks/B9HdDE5n.js +1 -0
- package/build/client/_app/immutable/chunks/B9HdDE5n.js.br +0 -0
- package/build/client/_app/immutable/chunks/B9HdDE5n.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{DFRlZOKz.js → CAQ5okPf.js} +2 -2
- package/build/client/_app/immutable/chunks/CAQ5okPf.js.br +0 -0
- package/build/client/_app/immutable/chunks/CAQ5okPf.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Ccz62-Hi.js +1 -0
- package/build/client/_app/immutable/chunks/Ccz62-Hi.js.br +0 -0
- package/build/client/_app/immutable/chunks/Ccz62-Hi.js.gz +0 -0
- package/build/client/_app/immutable/chunks/ChVOZl2x.js +1 -0
- package/build/client/_app/immutable/chunks/ChVOZl2x.js.br +0 -0
- package/build/client/_app/immutable/chunks/ChVOZl2x.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CiBv7EZv.js +108 -0
- package/build/client/_app/immutable/chunks/CiBv7EZv.js.br +0 -0
- package/build/client/_app/immutable/chunks/CiBv7EZv.js.gz +0 -0
- package/build/client/_app/immutable/chunks/UFAWUL-4.js +1 -0
- package/build/client/_app/immutable/chunks/UFAWUL-4.js.br +0 -0
- package/build/client/_app/immutable/chunks/UFAWUL-4.js.gz +0 -0
- package/build/client/_app/immutable/chunks/YWIvyYJl.js +1 -0
- package/build/client/_app/immutable/chunks/YWIvyYJl.js.br +0 -0
- package/build/client/_app/immutable/chunks/YWIvyYJl.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{BMORe2Gb.js → YpiA5Xry.js} +1 -1
- package/build/client/_app/immutable/chunks/YpiA5Xry.js.br +0 -0
- package/build/client/_app/immutable/chunks/{BMORe2Gb.js.gz → YpiA5Xry.js.gz} +0 -0
- package/build/client/_app/immutable/chunks/{DpFtnEPx.js → jjXOSnwf.js} +1 -1
- package/build/client/_app/immutable/chunks/jjXOSnwf.js.br +0 -0
- package/build/client/_app/immutable/chunks/jjXOSnwf.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.Rv8f5zSo.js +2 -0
- package/build/client/_app/immutable/entry/app.Rv8f5zSo.js.br +0 -0
- package/build/client/_app/immutable/entry/app.Rv8f5zSo.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.C5jRg_CF.js +1 -0
- package/build/client/_app/immutable/entry/start.C5jRg_CF.js.br +1 -0
- package/build/client/_app/immutable/entry/start.C5jRg_CF.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.Gv5d6CcW.js +1 -0
- package/build/client/_app/immutable/nodes/0.Gv5d6CcW.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.Gv5d6CcW.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.8J9y5zyF.js +1 -0
- package/build/client/_app/immutable/nodes/1.8J9y5zyF.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.8J9y5zyF.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.BNvTqsKZ.js +3 -0
- package/build/client/_app/immutable/nodes/2.BNvTqsKZ.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.BNvTqsKZ.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.CmlRI2XA.js +1 -0
- package/build/client/_app/immutable/nodes/3.CmlRI2XA.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.CmlRI2XA.js.gz +0 -0
- package/build/client/_app/immutable/nodes/4.CHTKk5QR.js +2 -0
- package/build/client/_app/immutable/nodes/4.CHTKk5QR.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.CHTKk5QR.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/handler.js +7 -7
- package/build/index.js +7 -7
- package/build/server/chunks/chunks/{exports.js-aVVdkXgn.js → exports.js-BcB5q0h5.js} +2 -2
- package/build/server/chunks/chunks/{exports.js-aVVdkXgn.js.map → exports.js-BcB5q0h5.js.map} +1 -1
- package/build/server/chunks/chunks/{internal.js-C6cIfe5d.js → internal.js-B6eePlP4.js} +2 -2
- package/build/server/chunks/chunks/{internal.js-C6cIfe5d.js.map → internal.js-B6eePlP4.js.map} +1 -1
- package/build/server/chunks/chunks/internal2.js-BlXt2rfE.js.map +1 -1
- package/build/server/chunks/chunks/{shared.js-BNd0HSxp.js → shared.js-BO7vJlPc.js} +2 -2
- package/build/server/chunks/chunks/{shared.js-BNd0HSxp.js.map → shared.js-BO7vJlPc.js.map} +1 -1
- package/build/server/chunks/chunks/{utils.js-6HSqnONB.js → utils.js-BTV5xnNv.js} +3 -3
- package/build/server/chunks/chunks/{utils.js-6HSqnONB.js.map → utils.js-BTV5xnNv.js.map} +1 -1
- package/build/server/chunks/{handler-rG16rikT.js → handler-DQdjBhko.js} +5 -5
- package/build/server/chunks/{handler-rG16rikT.js.map → handler-DQdjBhko.js.map} +1 -1
- package/build/server/chunks/{index.js-DYK_SfL5.js → index.js-DtKY5QD2.js} +5 -5
- package/build/server/chunks/{index.js-DYK_SfL5.js.map → index.js-DtKY5QD2.js.map} +1 -1
- package/build/server/chunks/{manifest.js-h2lDpEPs.js → manifest.js-CJMiboeI.js} +7 -7
- package/build/server/chunks/{manifest.js-h2lDpEPs.js.map → manifest.js-CJMiboeI.js.map} +1 -1
- package/build/server/chunks/nodes/{0.js-Db28RwnA.js → 0.js-Ce__dNtc.js} +2 -2
- package/build/server/chunks/nodes/{0.js-Db28RwnA.js.map → 0.js-Ce__dNtc.js.map} +1 -1
- package/build/server/chunks/nodes/1.js-BIwe5V8L.js +7 -0
- package/build/server/chunks/nodes/{1.js-D26Uv3YT.js.map → 1.js-BIwe5V8L.js.map} +1 -1
- package/build/server/chunks/nodes/2.js-Bj7c0eH1.js +7 -0
- package/build/server/chunks/nodes/{2.js-BeRlEFid.js.map → 2.js-Bj7c0eH1.js.map} +1 -1
- package/build/server/chunks/nodes/3.js-BMOf9FTK.js +7 -0
- package/build/server/chunks/nodes/{3.js-BRUNmCyG.js.map → 3.js-BMOf9FTK.js.map} +1 -1
- package/build/server/chunks/nodes/4.js-CNLb7Xc5.js +7 -0
- package/build/server/chunks/nodes/{4.js-CTHWqDf1.js.map → 4.js-CNLb7Xc5.js.map} +1 -1
- package/dist/runtime/chroma-key-video-lifecycle.d.ts +2 -2
- package/dist/runtime/chroma-key-video-lifecycle.js +221 -124
- package/dist/runtime/components/ChromaKeyVideo.svelte +13 -2
- package/dist/runtime/components/Slice.svelte +22 -12
- package/dist/runtime/components/Slice.svelte.d.ts +3 -6
- package/dist/runtime/components/SliceCanvas.svelte +51 -40
- package/dist/runtime/components/SliceCanvas.svelte.d.ts +2 -2
- package/dist/runtime/components/SliceDom.svelte +96 -42
- package/dist/runtime/components/SliceDom.svelte.d.ts +2 -2
- package/dist/runtime/slice-source-lifecycle.d.ts +14 -0
- package/dist/runtime/slice-source-lifecycle.js +70 -0
- package/package.json +4 -1
- package/build/client/_app/immutable/assets/LibraryContextMenu.BSnKt_Is.css +0 -1
- package/build/client/_app/immutable/assets/LibraryContextMenu.BSnKt_Is.css.br +0 -0
- package/build/client/_app/immutable/assets/LibraryContextMenu.BSnKt_Is.css.gz +0 -0
- package/build/client/_app/immutable/chunks/BAva9YXm.js +0 -1
- package/build/client/_app/immutable/chunks/BAva9YXm.js.br +0 -0
- package/build/client/_app/immutable/chunks/BAva9YXm.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BGAUmugt.js +0 -1
- package/build/client/_app/immutable/chunks/BGAUmugt.js.br +0 -0
- package/build/client/_app/immutable/chunks/BGAUmugt.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BMORe2Gb.js.br +0 -0
- package/build/client/_app/immutable/chunks/CBygkKLq.js +0 -1
- package/build/client/_app/immutable/chunks/CBygkKLq.js.br +0 -0
- package/build/client/_app/immutable/chunks/CBygkKLq.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Cil0r9BR.js +0 -108
- package/build/client/_app/immutable/chunks/Cil0r9BR.js.br +0 -0
- package/build/client/_app/immutable/chunks/Cil0r9BR.js.gz +0 -0
- package/build/client/_app/immutable/chunks/D-p-QSzz.js +0 -1
- package/build/client/_app/immutable/chunks/D-p-QSzz.js.br +0 -0
- package/build/client/_app/immutable/chunks/D-p-QSzz.js.gz +0 -0
- package/build/client/_app/immutable/chunks/D15yxLq9.js +0 -1
- package/build/client/_app/immutable/chunks/D15yxLq9.js.br +0 -0
- package/build/client/_app/immutable/chunks/D15yxLq9.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DFRlZOKz.js.br +0 -0
- package/build/client/_app/immutable/chunks/DFRlZOKz.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DpFtnEPx.js.br +0 -4
- package/build/client/_app/immutable/chunks/DpFtnEPx.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.BaP08B0_.js +0 -2
- package/build/client/_app/immutable/entry/app.BaP08B0_.js.br +0 -0
- package/build/client/_app/immutable/entry/app.BaP08B0_.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.DntfygIK.js +0 -1
- package/build/client/_app/immutable/entry/start.DntfygIK.js.br +0 -0
- package/build/client/_app/immutable/entry/start.DntfygIK.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.6A6JK0Xm.js +0 -1
- package/build/client/_app/immutable/nodes/0.6A6JK0Xm.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.6A6JK0Xm.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.8K7wLW9R.js +0 -1
- package/build/client/_app/immutable/nodes/1.8K7wLW9R.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.8K7wLW9R.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.CB_PeUBC.js +0 -3
- package/build/client/_app/immutable/nodes/2.CB_PeUBC.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.CB_PeUBC.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.qIkVQyP6.js +0 -1
- package/build/client/_app/immutable/nodes/3.qIkVQyP6.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.qIkVQyP6.js.gz +0 -0
- package/build/client/_app/immutable/nodes/4.SmoVIy5D.js +0 -2
- package/build/client/_app/immutable/nodes/4.SmoVIy5D.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.SmoVIy5D.js.gz +0 -0
- package/build/server/chunks/nodes/1.js-D26Uv3YT.js +0 -7
- package/build/server/chunks/nodes/2.js-BeRlEFid.js +0 -7
- package/build/server/chunks/nodes/3.js-BRUNmCyG.js +0 -7
- package/build/server/chunks/nodes/4.js-CTHWqDf1.js +0 -7
|
@@ -5,63 +5,82 @@ function copyChromaKey(parameters) {
|
|
|
5
5
|
keyColor: [...parameters.keyColor]
|
|
6
6
|
};
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
function copyPlaybackSettings(from, to) {
|
|
9
|
+
to.loop = from.loop;
|
|
10
|
+
to.playbackRate = from.playbackRate;
|
|
11
|
+
}
|
|
12
|
+
/** Internal media state machine that stages replacements before presenting them. */
|
|
9
13
|
export class ChromaKeyVideoLifecycle {
|
|
10
|
-
#
|
|
14
|
+
#videos;
|
|
11
15
|
#renderer;
|
|
12
|
-
#
|
|
16
|
+
#onPresentedVideoChange;
|
|
17
|
+
#presented = null;
|
|
18
|
+
#loading = null;
|
|
13
19
|
#sourceGeneration = 0;
|
|
14
20
|
#playing = false;
|
|
15
21
|
#disposed = false;
|
|
22
|
+
#frameHeld = false;
|
|
16
23
|
#loopGeneration = 0;
|
|
17
24
|
#loopRunning = false;
|
|
18
25
|
#pendingFrame = null;
|
|
19
|
-
constructor(video, renderer) {
|
|
20
|
-
this.#
|
|
26
|
+
constructor(video, renderer, standbyVideo = video, onPresentedVideoChange) {
|
|
27
|
+
this.#videos = [video, standbyVideo];
|
|
21
28
|
this.#renderer = renderer;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
video.addEventListener('seeked', this.#handleSeeked);
|
|
26
|
-
video.addEventListener('loadeddata', this.#handleLoadedData);
|
|
27
|
-
video.addEventListener('error', this.#handleError);
|
|
29
|
+
this.#onPresentedVideoChange = onPresentedVideoChange;
|
|
30
|
+
for (const element of new Set(this.#videos))
|
|
31
|
+
this.#listen(element);
|
|
28
32
|
}
|
|
29
33
|
switchSource(snapshot) {
|
|
30
34
|
if (this.#disposed)
|
|
31
35
|
return;
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
const loading = this.#loading;
|
|
37
|
+
if (loading && this.#matches(loading, snapshot) && loading.status === 'loading')
|
|
38
|
+
return;
|
|
39
|
+
const presented = this.#presented;
|
|
40
|
+
if (presented && this.#matches(presented, snapshot) && presented.status === 'settled') {
|
|
41
|
+
this.#cancelLoading();
|
|
42
|
+
presented.chromaKey = copyChromaKey(snapshot.chromaKey);
|
|
37
43
|
return;
|
|
38
44
|
}
|
|
39
|
-
this.#
|
|
40
|
-
this.#
|
|
45
|
+
this.#cancelLoading();
|
|
46
|
+
const loadingVideo = this.#loadingVideoFor(presented);
|
|
47
|
+
const playAfterHandoff = presented
|
|
48
|
+
? this.#playing &&
|
|
49
|
+
!presented.video.ended &&
|
|
50
|
+
(!presented.video.paused || presented.playRequestPending)
|
|
51
|
+
: this.#playing;
|
|
52
|
+
if (presented && loadingVideo === presented.video) {
|
|
53
|
+
this.#invalidatePlayRequest(presented);
|
|
54
|
+
this.#stopLoop();
|
|
55
|
+
presented.status = 'disposed';
|
|
56
|
+
this.#presented = null;
|
|
57
|
+
presented.video.pause();
|
|
58
|
+
}
|
|
41
59
|
const active = {
|
|
42
60
|
generation: ++this.#sourceGeneration,
|
|
43
61
|
status: 'loading',
|
|
62
|
+
video: loadingVideo,
|
|
44
63
|
rawVideoUrl: snapshot.rawVideoUrl,
|
|
45
64
|
normalizedVideoUrl: snapshot.resolvedVideoUrl,
|
|
46
65
|
maskUrl: snapshot.maskUrl,
|
|
66
|
+
maskImage: null,
|
|
47
67
|
chromaKey: copyChromaKey(snapshot.chromaKey),
|
|
48
68
|
mediaSettled: false,
|
|
49
69
|
maskSettled: !snapshot.maskUrl,
|
|
70
|
+
playAfterHandoff,
|
|
50
71
|
playRequestToken: 0,
|
|
51
72
|
playRequestPending: false,
|
|
52
73
|
playRejectedForCurrentIntent: false
|
|
53
74
|
};
|
|
54
|
-
this.#
|
|
55
|
-
this.#
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
// draw and the new source's first decodable frame.
|
|
60
|
-
this.#video.src = snapshot.resolvedVideoUrl;
|
|
75
|
+
this.#loading = active;
|
|
76
|
+
if (!this.#frameHeld)
|
|
77
|
+
this.#renderer.setMask(null);
|
|
78
|
+
loadingVideo.pause();
|
|
79
|
+
loadingVideo.src = snapshot.resolvedVideoUrl;
|
|
61
80
|
// Reading the reflected property gives the browser-normalized absolute URL.
|
|
62
|
-
active.normalizedVideoUrl =
|
|
63
|
-
|
|
64
|
-
this.#primeMedia();
|
|
81
|
+
active.normalizedVideoUrl = loadingVideo.src;
|
|
82
|
+
loadingVideo.load();
|
|
83
|
+
this.#primeMedia(active);
|
|
65
84
|
if (snapshot.maskUrl)
|
|
66
85
|
this.#loadMask(active, snapshot.maskUrl);
|
|
67
86
|
}
|
|
@@ -70,30 +89,34 @@ export class ChromaKeyVideoLifecycle {
|
|
|
70
89
|
return;
|
|
71
90
|
const changed = this.#playing !== playing;
|
|
72
91
|
this.#playing = playing;
|
|
73
|
-
const
|
|
92
|
+
const presented = this.#presented;
|
|
93
|
+
const loading = this.#loading;
|
|
94
|
+
if (loading?.status === 'loading')
|
|
95
|
+
loading.playAfterHandoff = playing;
|
|
74
96
|
if (!playing) {
|
|
75
|
-
this.#invalidatePlayRequest(
|
|
97
|
+
this.#invalidatePlayRequest(presented);
|
|
76
98
|
this.#stopLoop();
|
|
77
|
-
|
|
78
|
-
// decodable frame. Some engines treat preload="auto" as metadata-only and
|
|
79
|
-
// do not reach loadeddata until playback has started.
|
|
80
|
-
if (!active || active.status !== 'loading' || active.mediaSettled) {
|
|
81
|
-
this.#video.pause();
|
|
82
|
-
}
|
|
99
|
+
presented?.video.pause();
|
|
83
100
|
return;
|
|
84
101
|
}
|
|
85
|
-
if (changed &&
|
|
86
|
-
|
|
87
|
-
|
|
102
|
+
if (changed && presented)
|
|
103
|
+
presented.playRejectedForCurrentIntent = false;
|
|
104
|
+
if (presented) {
|
|
105
|
+
presented.playAfterHandoff = true;
|
|
106
|
+
this.#requestPlayback(presented);
|
|
107
|
+
}
|
|
88
108
|
}
|
|
89
109
|
setChromaKey(parameters) {
|
|
90
110
|
if (this.#disposed)
|
|
91
111
|
return;
|
|
92
|
-
const active = this.#
|
|
112
|
+
const active = this.#loading ?? this.#presented;
|
|
93
113
|
if (!active || active.status === 'failed' || active.status === 'disposed')
|
|
94
114
|
return;
|
|
95
115
|
active.chromaKey = copyChromaKey(parameters);
|
|
96
|
-
if (active
|
|
116
|
+
if (active === this.#presented &&
|
|
117
|
+
active.status === 'settled' &&
|
|
118
|
+
active.video.paused &&
|
|
119
|
+
this.#isCurrentResource(active)) {
|
|
97
120
|
this.#draw(active);
|
|
98
121
|
}
|
|
99
122
|
}
|
|
@@ -102,19 +125,52 @@ export class ChromaKeyVideoLifecycle {
|
|
|
102
125
|
return;
|
|
103
126
|
this.#disposed = true;
|
|
104
127
|
this.#sourceGeneration++;
|
|
105
|
-
|
|
106
|
-
this.#invalidatePlayRequest(
|
|
128
|
+
this.#invalidatePlayRequest(this.#presented);
|
|
129
|
+
this.#invalidatePlayRequest(this.#loading);
|
|
107
130
|
this.#stopLoop();
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
this.#
|
|
111
|
-
|
|
112
|
-
this.#
|
|
113
|
-
this.#
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
131
|
+
if (this.#presented)
|
|
132
|
+
this.#presented.status = 'disposed';
|
|
133
|
+
if (this.#loading)
|
|
134
|
+
this.#loading.status = 'disposed';
|
|
135
|
+
this.#presented = null;
|
|
136
|
+
this.#loading = null;
|
|
137
|
+
for (const video of new Set(this.#videos)) {
|
|
138
|
+
this.#unlisten(video);
|
|
139
|
+
video.pause();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
#listen(video) {
|
|
143
|
+
video.addEventListener('play', this.#handlePlay);
|
|
144
|
+
video.addEventListener('pause', this.#handleStop);
|
|
145
|
+
video.addEventListener('ended', this.#handleStop);
|
|
146
|
+
video.addEventListener('seeked', this.#handleSeeked);
|
|
147
|
+
video.addEventListener('loadeddata', this.#handleLoadedData);
|
|
148
|
+
video.addEventListener('error', this.#handleError);
|
|
149
|
+
}
|
|
150
|
+
#unlisten(video) {
|
|
151
|
+
video.removeEventListener('play', this.#handlePlay);
|
|
152
|
+
video.removeEventListener('pause', this.#handleStop);
|
|
153
|
+
video.removeEventListener('ended', this.#handleStop);
|
|
154
|
+
video.removeEventListener('seeked', this.#handleSeeked);
|
|
155
|
+
video.removeEventListener('loadeddata', this.#handleLoadedData);
|
|
156
|
+
video.removeEventListener('error', this.#handleError);
|
|
157
|
+
}
|
|
158
|
+
#matches(active, snapshot) {
|
|
159
|
+
return active.rawVideoUrl === snapshot.rawVideoUrl && active.maskUrl === snapshot.maskUrl;
|
|
160
|
+
}
|
|
161
|
+
#loadingVideoFor(presented) {
|
|
162
|
+
if (!presented)
|
|
163
|
+
return this.#videos[0];
|
|
164
|
+
return this.#videos[0] === presented.video ? this.#videos[1] : this.#videos[0];
|
|
165
|
+
}
|
|
166
|
+
#cancelLoading() {
|
|
167
|
+
const loading = this.#loading;
|
|
168
|
+
if (!loading)
|
|
169
|
+
return;
|
|
170
|
+
this.#invalidatePlayRequest(loading);
|
|
171
|
+
loading.status = 'disposed';
|
|
172
|
+
loading.video.pause();
|
|
173
|
+
this.#loading = null;
|
|
118
174
|
}
|
|
119
175
|
#loadMask(active, maskUrl) {
|
|
120
176
|
const image = new Image();
|
|
@@ -122,122 +178,161 @@ export class ChromaKeyVideoLifecycle {
|
|
|
122
178
|
image
|
|
123
179
|
.decode()
|
|
124
180
|
.then(() => {
|
|
125
|
-
if (!this.#
|
|
181
|
+
if (!this.#isLoading(active))
|
|
126
182
|
return;
|
|
127
|
-
|
|
183
|
+
active.maskImage = image;
|
|
128
184
|
active.maskSettled = true;
|
|
129
185
|
this.#settleIfReady(active);
|
|
130
186
|
})
|
|
131
187
|
.catch(() => {
|
|
132
|
-
if (!this.#
|
|
188
|
+
if (!this.#isLoading(active))
|
|
133
189
|
return;
|
|
134
190
|
console.warn(`Failed to load chroma-key mask "${maskUrl}".`);
|
|
135
191
|
active.maskSettled = true;
|
|
136
192
|
this.#settleIfReady(active);
|
|
137
193
|
});
|
|
138
194
|
}
|
|
139
|
-
#primeMedia() {
|
|
195
|
+
#primeMedia(active) {
|
|
140
196
|
let request;
|
|
141
197
|
try {
|
|
142
|
-
request =
|
|
198
|
+
request = active.video.play();
|
|
143
199
|
}
|
|
144
200
|
catch {
|
|
145
201
|
return;
|
|
146
202
|
}
|
|
147
203
|
Promise.resolve(request).catch(() => {
|
|
148
|
-
// Muted warm-up is best-effort.
|
|
149
|
-
// normal settled playback request below gets its own retry state.
|
|
204
|
+
// Muted warm-up is best-effort. preload may still reach loadeddata.
|
|
150
205
|
});
|
|
151
206
|
}
|
|
152
|
-
#handleLoadedData = () => {
|
|
153
|
-
const active = this.#
|
|
154
|
-
if (!active || active.
|
|
207
|
+
#handleLoadedData = (event) => {
|
|
208
|
+
const active = this.#loading;
|
|
209
|
+
if (!active || active.video !== event.currentTarget || !this.#isLoading(active))
|
|
155
210
|
return;
|
|
156
211
|
if (!this.#isCurrentResource(active))
|
|
157
212
|
return;
|
|
158
213
|
active.mediaSettled = true;
|
|
159
|
-
|
|
160
|
-
|
|
214
|
+
// Freeze the staged source at its first decodable frame until its mask is ready.
|
|
215
|
+
active.video.pause();
|
|
161
216
|
this.#settleIfReady(active);
|
|
162
217
|
};
|
|
163
|
-
#handleError = () => {
|
|
164
|
-
const
|
|
165
|
-
|
|
218
|
+
#handleError = (event) => {
|
|
219
|
+
const video = event.currentTarget;
|
|
220
|
+
const loading = this.#loading;
|
|
221
|
+
if (loading?.video === video && loading.status === 'loading') {
|
|
222
|
+
loading.status = 'failed';
|
|
223
|
+
this.#invalidatePlayRequest(loading);
|
|
224
|
+
loading.video.pause();
|
|
225
|
+
// A failed replacement never erases an already presented frame.
|
|
226
|
+
if (!this.#presented && !this.#frameHeld)
|
|
227
|
+
this.#clear();
|
|
166
228
|
return;
|
|
167
|
-
|
|
168
|
-
this.#
|
|
229
|
+
}
|
|
230
|
+
const presented = this.#presented;
|
|
231
|
+
if (!presented || presented.video !== video || presented.status !== 'settled')
|
|
232
|
+
return;
|
|
233
|
+
presented.status = 'failed';
|
|
234
|
+
this.#invalidatePlayRequest(presented);
|
|
169
235
|
this.#stopLoop();
|
|
170
|
-
|
|
171
|
-
this.#
|
|
236
|
+
presented.video.pause();
|
|
237
|
+
if (!this.#loading)
|
|
238
|
+
this.#clear();
|
|
172
239
|
};
|
|
173
|
-
#handlePlay = () => {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
// Drawing and the frame loop remain gated on video + mask settlement.
|
|
177
|
-
if (active?.status === 'loading')
|
|
240
|
+
#handlePlay = (event) => {
|
|
241
|
+
const video = event.currentTarget;
|
|
242
|
+
if (this.#loading?.video === video)
|
|
178
243
|
return;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
244
|
+
const presented = this.#presented;
|
|
245
|
+
if (!presented || presented.video !== video || presented.status !== 'settled') {
|
|
246
|
+
if (video && 'pause' in video)
|
|
247
|
+
video.pause();
|
|
182
248
|
return;
|
|
183
249
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
250
|
+
presented.playRejectedForCurrentIntent = false;
|
|
251
|
+
presented.playAfterHandoff = true;
|
|
252
|
+
if (this.#loading?.status === 'loading')
|
|
253
|
+
this.#loading.playAfterHandoff = true;
|
|
254
|
+
if (!presented.video.paused)
|
|
255
|
+
this.#startLoop(presented);
|
|
187
256
|
};
|
|
188
|
-
#handleStop = () => {
|
|
189
|
-
const
|
|
257
|
+
#handleStop = (event) => {
|
|
258
|
+
const presented = this.#presented;
|
|
259
|
+
if (!presented || presented.video !== event.currentTarget || presented.status !== 'settled')
|
|
260
|
+
return;
|
|
261
|
+
this.#invalidatePlayRequest(presented);
|
|
190
262
|
this.#stopLoop();
|
|
191
|
-
|
|
192
|
-
|
|
263
|
+
this.#draw(presented);
|
|
264
|
+
presented.playAfterHandoff = false;
|
|
265
|
+
if (this.#loading?.status === 'loading')
|
|
266
|
+
this.#loading.playAfterHandoff = false;
|
|
193
267
|
};
|
|
194
|
-
#handleSeeked = () => {
|
|
195
|
-
const
|
|
196
|
-
if (!
|
|
268
|
+
#handleSeeked = (event) => {
|
|
269
|
+
const presented = this.#presented;
|
|
270
|
+
if (!presented ||
|
|
271
|
+
presented.video !== event.currentTarget ||
|
|
272
|
+
presented.status !== 'settled' ||
|
|
273
|
+
!presented.video.paused) {
|
|
197
274
|
return;
|
|
198
|
-
|
|
275
|
+
}
|
|
276
|
+
this.#draw(presented);
|
|
199
277
|
};
|
|
200
278
|
#settleIfReady(active) {
|
|
201
|
-
if (!this.#
|
|
202
|
-
active.status !== 'loading' ||
|
|
279
|
+
if (!this.#isLoading(active) ||
|
|
203
280
|
!active.mediaSettled ||
|
|
204
|
-
!active.maskSettled
|
|
281
|
+
!active.maskSettled ||
|
|
282
|
+
!this.#isCurrentResource(active)) {
|
|
205
283
|
return;
|
|
206
284
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
285
|
+
const previous = this.#presented;
|
|
286
|
+
this.#stopLoop();
|
|
287
|
+
this.#invalidatePlayRequest(previous);
|
|
288
|
+
if (previous && previous.video !== active.video) {
|
|
289
|
+
copyPlaybackSettings(previous.video, active.video);
|
|
210
290
|
}
|
|
211
|
-
|
|
212
|
-
this.#video.pause();
|
|
291
|
+
active.video.pause();
|
|
213
292
|
active.status = 'settled';
|
|
293
|
+
this.#loading = null;
|
|
294
|
+
this.#presented = active;
|
|
295
|
+
this.#renderer.setMask(active.maskImage);
|
|
214
296
|
this.#draw(active);
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
297
|
+
this.#onPresentedVideoChange?.(active.video);
|
|
298
|
+
if (previous && previous !== active) {
|
|
299
|
+
previous.status = 'disposed';
|
|
300
|
+
previous.video.pause();
|
|
301
|
+
}
|
|
302
|
+
if (this.#playing && active.playAfterHandoff)
|
|
218
303
|
this.#requestPlayback(active);
|
|
219
304
|
}
|
|
220
305
|
#draw(active) {
|
|
221
|
-
if (
|
|
306
|
+
if (this.#disposed ||
|
|
307
|
+
this.#presented !== active ||
|
|
222
308
|
active.status !== 'settled' ||
|
|
223
309
|
!this.#isCurrentResource(active)) {
|
|
224
310
|
return;
|
|
225
311
|
}
|
|
226
|
-
this.#renderer.draw(
|
|
312
|
+
this.#renderer.draw(active.video, active.chromaKey);
|
|
313
|
+
this.#frameHeld = true;
|
|
314
|
+
}
|
|
315
|
+
#clear() {
|
|
316
|
+
this.#renderer.clear();
|
|
317
|
+
this.#frameHeld = false;
|
|
227
318
|
}
|
|
228
319
|
#isCurrentResource(active) {
|
|
229
|
-
return (
|
|
230
|
-
|
|
320
|
+
return (active.video.readyState >= HAVE_CURRENT_DATA &&
|
|
321
|
+
active.video.currentSrc === active.normalizedVideoUrl);
|
|
231
322
|
}
|
|
232
|
-
#
|
|
233
|
-
return (!this.#disposed &&
|
|
323
|
+
#isLoading(active) {
|
|
324
|
+
return (!this.#disposed &&
|
|
325
|
+
this.#loading === active &&
|
|
326
|
+
active.status === 'loading' &&
|
|
327
|
+
active.generation === this.#sourceGeneration);
|
|
234
328
|
}
|
|
235
329
|
#requestPlayback(active) {
|
|
236
|
-
if (
|
|
237
|
-
|
|
330
|
+
if (this.#disposed ||
|
|
331
|
+
this.#presented !== active ||
|
|
238
332
|
active.status !== 'settled' ||
|
|
239
333
|
!this.#playing ||
|
|
240
|
-
!
|
|
334
|
+
!active.playAfterHandoff ||
|
|
335
|
+
!active.video.paused ||
|
|
241
336
|
active.playRequestPending ||
|
|
242
337
|
active.playRejectedForCurrentIntent) {
|
|
243
338
|
return;
|
|
@@ -246,7 +341,7 @@ export class ChromaKeyVideoLifecycle {
|
|
|
246
341
|
active.playRequestPending = true;
|
|
247
342
|
let request;
|
|
248
343
|
try {
|
|
249
|
-
request =
|
|
344
|
+
request = active.video.play();
|
|
250
345
|
}
|
|
251
346
|
catch {
|
|
252
347
|
this.#finishPlayRequest(active, requestToken, false);
|
|
@@ -255,7 +350,7 @@ export class ChromaKeyVideoLifecycle {
|
|
|
255
350
|
Promise.resolve(request).then(() => this.#finishPlayRequest(active, requestToken, true), () => this.#finishPlayRequest(active, requestToken, false));
|
|
256
351
|
}
|
|
257
352
|
#finishPlayRequest(active, requestToken, succeeded) {
|
|
258
|
-
if (
|
|
353
|
+
if (this.#disposed || this.#presented !== active || active.playRequestToken !== requestToken)
|
|
259
354
|
return;
|
|
260
355
|
active.playRequestPending = false;
|
|
261
356
|
active.playRejectedForCurrentIntent = !succeeded;
|
|
@@ -268,9 +363,10 @@ export class ChromaKeyVideoLifecycle {
|
|
|
268
363
|
}
|
|
269
364
|
#startLoop(active) {
|
|
270
365
|
if (this.#loopRunning ||
|
|
271
|
-
|
|
366
|
+
this.#disposed ||
|
|
367
|
+
this.#presented !== active ||
|
|
272
368
|
active.status !== 'settled' ||
|
|
273
|
-
|
|
369
|
+
active.video.paused) {
|
|
274
370
|
return;
|
|
275
371
|
}
|
|
276
372
|
this.#loopRunning = true;
|
|
@@ -279,6 +375,7 @@ export class ChromaKeyVideoLifecycle {
|
|
|
279
375
|
}
|
|
280
376
|
#scheduleNextFrame(active, loopGeneration) {
|
|
281
377
|
const sourceGeneration = active.generation;
|
|
378
|
+
const video = active.video;
|
|
282
379
|
let handle = 0;
|
|
283
380
|
const callback = () => {
|
|
284
381
|
const pending = this.#pendingFrame;
|
|
@@ -287,28 +384,28 @@ export class ChromaKeyVideoLifecycle {
|
|
|
287
384
|
pending.handle !== handle ||
|
|
288
385
|
this.#loopGeneration !== loopGeneration ||
|
|
289
386
|
!this.#loopRunning ||
|
|
290
|
-
|
|
387
|
+
this.#presented !== active ||
|
|
291
388
|
active.generation !== sourceGeneration ||
|
|
292
389
|
active.status !== 'settled' ||
|
|
293
|
-
|
|
390
|
+
video.paused) {
|
|
294
391
|
return;
|
|
295
392
|
}
|
|
296
393
|
this.#pendingFrame = null;
|
|
297
394
|
this.#draw(active);
|
|
298
395
|
if (this.#loopRunning &&
|
|
299
396
|
this.#loopGeneration === loopGeneration &&
|
|
300
|
-
this.#
|
|
301
|
-
!
|
|
397
|
+
this.#presented === active &&
|
|
398
|
+
!video.paused) {
|
|
302
399
|
this.#scheduleNextFrame(active, loopGeneration);
|
|
303
400
|
}
|
|
304
401
|
};
|
|
305
|
-
if (
|
|
306
|
-
handle =
|
|
307
|
-
this.#pendingFrame = { generation: loopGeneration, kind: 'rvfc', handle };
|
|
402
|
+
if (video.requestVideoFrameCallback) {
|
|
403
|
+
handle = video.requestVideoFrameCallback(callback);
|
|
404
|
+
this.#pendingFrame = { generation: loopGeneration, kind: 'rvfc', handle, video };
|
|
308
405
|
}
|
|
309
406
|
else {
|
|
310
407
|
handle = requestAnimationFrame(callback);
|
|
311
|
-
this.#pendingFrame = { generation: loopGeneration, kind: 'raf', handle };
|
|
408
|
+
this.#pendingFrame = { generation: loopGeneration, kind: 'raf', handle, video };
|
|
312
409
|
}
|
|
313
410
|
}
|
|
314
411
|
#stopLoop() {
|
|
@@ -319,7 +416,7 @@ export class ChromaKeyVideoLifecycle {
|
|
|
319
416
|
if (!pending)
|
|
320
417
|
return;
|
|
321
418
|
if (pending.kind === 'rvfc')
|
|
322
|
-
|
|
419
|
+
pending.video.cancelVideoFrameCallback?.(pending.handle);
|
|
323
420
|
else
|
|
324
421
|
cancelAnimationFrame(pending.handle);
|
|
325
422
|
}
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
}: Props = $props();
|
|
34
34
|
|
|
35
35
|
let canvasElement = $state<HTMLCanvasElement>();
|
|
36
|
+
let primaryVideoElement = $state<HTMLVideoElement>();
|
|
37
|
+
let standbyVideoElement = $state<HTMLVideoElement>();
|
|
36
38
|
let webglUnavailable = $state(false);
|
|
37
39
|
|
|
38
40
|
let renderer: ChromaKeyGl | null = null;
|
|
@@ -49,7 +51,15 @@
|
|
|
49
51
|
return;
|
|
50
52
|
}
|
|
51
53
|
const activeRenderer = renderer;
|
|
52
|
-
|
|
54
|
+
videoElement = primaryVideoElement!;
|
|
55
|
+
const activeLifecycle = new ChromaKeyVideoLifecycle(
|
|
56
|
+
primaryVideoElement!,
|
|
57
|
+
activeRenderer,
|
|
58
|
+
standbyVideoElement!,
|
|
59
|
+
(activeVideo) => {
|
|
60
|
+
videoElement = activeVideo;
|
|
61
|
+
}
|
|
62
|
+
);
|
|
53
63
|
lifecycle = activeLifecycle;
|
|
54
64
|
|
|
55
65
|
return () => {
|
|
@@ -102,7 +112,8 @@
|
|
|
102
112
|
{/if}
|
|
103
113
|
{:else}
|
|
104
114
|
<canvas bind:this={canvasElement}></canvas>
|
|
105
|
-
<video bind:this={
|
|
115
|
+
<video bind:this={primaryVideoElement} {loop} muted playsinline preload="auto"></video>
|
|
116
|
+
<video bind:this={standbyVideoElement} {loop} muted playsinline preload="auto"></video>
|
|
106
117
|
{/if}
|
|
107
118
|
</div>
|
|
108
119
|
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
The slice component hosts
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
The slice component hosts rendering: one component, backend chosen per use.
|
|
3
|
+
Dispatches between the two private implementations — SliceDom (default) and
|
|
4
|
+
SliceCanvas — which share `computeRenderCells` geometry. The DOM backend maps
|
|
5
|
+
that geometry to one CSS border-image paint; Canvas draws the cells into one
|
|
6
|
+
canvas backing store.
|
|
6
7
|
-->
|
|
7
8
|
<script lang="ts">
|
|
9
|
+
import { onDestroy } from 'svelte';
|
|
8
10
|
import type { SliceManifestEntry } from '../../core/index.js';
|
|
11
|
+
import { SliceSourceLifecycle, type LoadedSliceSnapshot } from '../slice-source-lifecycle.js';
|
|
9
12
|
import SliceDom from './SliceDom.svelte';
|
|
10
13
|
import SliceCanvas from './SliceCanvas.svelte';
|
|
11
14
|
|
|
12
15
|
interface Props {
|
|
13
16
|
src: SliceManifestEntry;
|
|
14
17
|
/**
|
|
15
|
-
* Rendering backend. `dom` (default) paints
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* flicker in some situations (cells are separate elements the browser
|
|
19
|
-
* composites independently, so a resize mid-animation can tear), so
|
|
20
|
-
* hosts pick `canvas` where that hurts.
|
|
18
|
+
* Rendering backend. `dom` (default) paints the shared slice geometry as
|
|
19
|
+
* one CSS `border-image`; `canvas` draws the same cells into one `<canvas>`
|
|
20
|
+
* with a device-pixel-ratio-sized backing store.
|
|
21
21
|
*/
|
|
22
22
|
backend?: 'dom' | 'canvas';
|
|
23
23
|
/** Accessible label for the rendered graphic. */
|
|
@@ -26,10 +26,20 @@
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
let { src, backend = 'dom', alt = '', class: className = '' }: Props = $props();
|
|
29
|
+
let snapshot = $state.raw<LoadedSliceSnapshot | null>(null);
|
|
30
|
+
const sourceLifecycle = new SliceSourceLifecycle((loaded) => {
|
|
31
|
+
snapshot = loaded;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
$effect(() => {
|
|
35
|
+
sourceLifecycle.request(src);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
onDestroy(() => sourceLifecycle.dispose());
|
|
29
39
|
</script>
|
|
30
40
|
|
|
31
41
|
{#if backend === 'canvas'}
|
|
32
|
-
<SliceCanvas {
|
|
42
|
+
<SliceCanvas {snapshot} {alt} class={className} />
|
|
33
43
|
{:else}
|
|
34
|
-
<SliceDom {
|
|
44
|
+
<SliceDom {snapshot} {alt} class={className} />
|
|
35
45
|
{/if}
|
|
@@ -2,12 +2,9 @@ import type { SliceManifestEntry } from '../../core/index.js';
|
|
|
2
2
|
interface Props {
|
|
3
3
|
src: SliceManifestEntry;
|
|
4
4
|
/**
|
|
5
|
-
* Rendering backend. `dom` (default) paints
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* flicker in some situations (cells are separate elements the browser
|
|
9
|
-
* composites independently, so a resize mid-animation can tear), so
|
|
10
|
-
* hosts pick `canvas` where that hurts.
|
|
5
|
+
* Rendering backend. `dom` (default) paints the shared slice geometry as
|
|
6
|
+
* one CSS `border-image`; `canvas` draws the same cells into one `<canvas>`
|
|
7
|
+
* with a device-pixel-ratio-sized backing store.
|
|
11
8
|
*/
|
|
12
9
|
backend?: 'dom' | 'canvas';
|
|
13
10
|
/** Accessible label for the rendered graphic. */
|