@clockworkdog/cogs-client 3.0.0-alpha.1 → 3.0.0-alpha.10

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.ts CHANGED
@@ -7,6 +7,7 @@ export type { default as MediaObjectFit } from './types/MediaObjectFit';
7
7
  export * as MediaSchema from './types/MediaSchema';
8
8
  export { default as CogsAudioPlayer } from './AudioPlayer';
9
9
  export { default as CogsVideoPlayer } from './VideoPlayer';
10
+ export { SurfaceManager } from './state-based/SurfaceManager';
10
11
  export * from './types/AudioState';
11
12
  export { assetUrl, preloadUrl } from './utils/urls';
12
13
  export { getStateAtTime } from './utils/getStateAtTime';
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export * from './CogsConnection';
3
3
  export * as MediaSchema from './types/MediaSchema';
4
4
  export { default as CogsAudioPlayer } from './AudioPlayer';
5
5
  export { default as CogsVideoPlayer } from './VideoPlayer';
6
+ export { SurfaceManager } from './state-based/SurfaceManager';
6
7
  export * from './types/AudioState';
7
8
  export { assetUrl, preloadUrl } from './utils/urls';
8
9
  export { getStateAtTime } from './utils/getStateAtTime';
@@ -0,0 +1,45 @@
1
+ import { AudialProperties, AudioState, ImageMetadata, ImageState, MediaClipState, TemporalProperties, VideoState, VisualProperties } from '../types/MediaSchema';
2
+ import { MediaPreloader } from './MediaPreloader';
3
+ /**
4
+ * Each instance of a MediaClipManager is responsible for displaying
5
+ * an image/audio/video clip in the correct state.
6
+ */
7
+ export declare abstract class MediaClipManager<T extends MediaClipState> {
8
+ private surfaceElement;
9
+ protected clipElement: HTMLElement;
10
+ protected constructAssetURL: (file: string) => string;
11
+ protected mediaPreloader: MediaPreloader;
12
+ constructor(surfaceElement: HTMLElement, clipElement: HTMLElement, state: T, constructAssetURL: (file: string) => string, mediaPreloader: MediaPreloader);
13
+ protected abstract update(): void;
14
+ abstract destroy(): void;
15
+ isConnected(element?: HTMLElement): boolean;
16
+ protected _state: T;
17
+ setState(newState: T): void;
18
+ private timeout;
19
+ private loop;
20
+ }
21
+ export declare function assertElement(mediaElement: HTMLElement | undefined, parentElement: HTMLElement, clip: MediaClipState, constructAssetURL: (file: string) => string, preloader: MediaPreloader): HTMLElement;
22
+ export declare function assertVisualProperties(mediaElement: HTMLMediaElement | HTMLImageElement, properties: VisualProperties, objectFit: ImageMetadata['fit']): void;
23
+ export declare function assertAudialProperties(mediaElement: HTMLMediaElement, properties: AudialProperties, sinkId: string): void;
24
+ interface TemporalSyncState {
25
+ state: 'idle' | 'seeking' | 'intercepting';
26
+ }
27
+ export declare function assertTemporalProperties(mediaElement: HTMLMediaElement, properties: TemporalProperties, keyframes: VideoState['keyframes'], syncState: TemporalSyncState): TemporalSyncState;
28
+ export declare class ImageManager extends MediaClipManager<ImageState> {
29
+ private imageElement;
30
+ protected update(): void;
31
+ destroy(): void;
32
+ }
33
+ export declare class AudioManager extends MediaClipManager<AudioState> {
34
+ private syncState;
35
+ private audioElement;
36
+ protected update(): void;
37
+ destroy(): void;
38
+ }
39
+ export declare class VideoManager extends MediaClipManager<VideoState> {
40
+ private syncState;
41
+ private videoElement?;
42
+ protected update(): void;
43
+ destroy(): void;
44
+ }
45
+ export {};
@@ -0,0 +1,261 @@
1
+ import { getStateAtTime } from '../utils/getStateAtTime';
2
+ /**
3
+ * Each instance of a MediaClipManager is responsible for displaying
4
+ * an image/audio/video clip in the correct state.
5
+ */
6
+ export class MediaClipManager {
7
+ surfaceElement;
8
+ clipElement;
9
+ constructAssetURL;
10
+ mediaPreloader;
11
+ constructor(surfaceElement, clipElement, state, constructAssetURL, mediaPreloader) {
12
+ this.surfaceElement = surfaceElement;
13
+ this.clipElement = clipElement;
14
+ this.constructAssetURL = constructAssetURL;
15
+ this.mediaPreloader = mediaPreloader;
16
+ this._state = state;
17
+ // Allow the class to be constructed, then call the loop
18
+ setTimeout(this.loop);
19
+ }
20
+ isConnected(element) {
21
+ if (!this.surfaceElement) {
22
+ return false;
23
+ }
24
+ if (!this.clipElement) {
25
+ return false;
26
+ }
27
+ if (!this.surfaceElement.contains(this.clipElement)) {
28
+ return false;
29
+ }
30
+ if (element) {
31
+ if (!this.clipElement.contains(element))
32
+ return false;
33
+ }
34
+ return true;
35
+ }
36
+ _state;
37
+ setState(newState) {
38
+ this._state = newState;
39
+ clearTimeout(this.timeout);
40
+ this.loop();
41
+ }
42
+ timeout;
43
+ loop = async () => {
44
+ if (this.isConnected()) {
45
+ this.update();
46
+ this.timeout = setTimeout(this.loop, 0);
47
+ }
48
+ else {
49
+ this.destroy();
50
+ }
51
+ };
52
+ }
53
+ export function assertElement(mediaElement, parentElement, clip, constructAssetURL, preloader) {
54
+ let element;
55
+ switch (clip.type) {
56
+ case 'image':
57
+ {
58
+ element = mediaElement instanceof HTMLImageElement ? mediaElement : document.createElement('img');
59
+ const assetURL = constructAssetURL(clip.file);
60
+ if (!element.src.includes(assetURL)) {
61
+ element.src = assetURL;
62
+ }
63
+ }
64
+ break;
65
+ case 'audio':
66
+ element = mediaElement instanceof HTMLAudioElement ? mediaElement : preloader.getElement(clip.file, clip.type);
67
+ break;
68
+ case 'video':
69
+ element = mediaElement instanceof HTMLVideoElement ? mediaElement : preloader.getElement(clip.file, clip.type);
70
+ break;
71
+ }
72
+ parentElement.replaceChildren(element);
73
+ element.style.position = 'absolute';
74
+ element.style.width = '100%';
75
+ element.style.height = '100%';
76
+ return element;
77
+ }
78
+ export function assertVisualProperties(mediaElement, properties, objectFit) {
79
+ const opacityString = String(properties.opacity);
80
+ if (mediaElement.style.opacity !== opacityString) {
81
+ mediaElement.style.opacity = opacityString;
82
+ }
83
+ const zIndex = Math.round(properties.zIndex ?? 0);
84
+ if (parseInt(mediaElement.style.zIndex) !== zIndex) {
85
+ mediaElement.style.zIndex = String(zIndex);
86
+ }
87
+ if (mediaElement.style.objectFit !== objectFit) {
88
+ mediaElement.style.objectFit = objectFit;
89
+ }
90
+ }
91
+ export function assertAudialProperties(mediaElement, properties, sinkId) {
92
+ if (mediaElement.volume !== properties.volume) {
93
+ mediaElement.volume = properties.volume;
94
+ }
95
+ if (mediaElement.sinkId !== sinkId) {
96
+ mediaElement.setSinkId(sinkId).catch(() => {
97
+ /* Do nothing, will be tried in next loop */
98
+ });
99
+ }
100
+ }
101
+ const OUTER_TARGET_SYNC_THRESHOLD_MS = 50; // When outside of this range we attempt to sync playback
102
+ const INNER_TARGET_SYNC_THRESHOLD_MS = 5; // When attempting to sync playback, we aim for this accuracy
103
+ const MAX_SYNC_THRESHOLD_MS = 1_000; // If we are further than this, we will seek instead
104
+ const SEEK_LOOKAHEAD_MS = 5; // If it takes time to seek, we should seek ahead a little
105
+ const LOOPING_EPSILON_MS = 5;
106
+ const PLAYBACK_ADJUSTMENT_SMOOTHING = 0.3;
107
+ const MAX_PLAYBACK_RATE_ADJUSTMENT = 0.1;
108
+ function playbackSmoothing(deltaTime) {
109
+ return Math.sign(deltaTime) * Math.pow(Math.abs(deltaTime) / MAX_SYNC_THRESHOLD_MS, PLAYBACK_ADJUSTMENT_SMOOTHING) * MAX_PLAYBACK_RATE_ADJUSTMENT;
110
+ }
111
+ export function assertTemporalProperties(mediaElement, properties, keyframes, syncState) {
112
+ if (mediaElement.paused && properties.rate > 0) {
113
+ mediaElement.play().catch(() => {
114
+ /* Do nothing, will be tried in next loop */
115
+ });
116
+ }
117
+ // At the end of the media, is it set back to the start?
118
+ // Sounds like looping to me!
119
+ if (mediaElement.duration) {
120
+ const nextTemporalKeyframe = keyframes.filter(([t, kf]) => t > properties.t && (kf?.set?.t !== undefined || kf?.set?.rate !== undefined))[0];
121
+ if (nextTemporalKeyframe?.[1]?.set?.t === 0) {
122
+ const timeRemaining = (mediaElement.duration - properties.t) / properties.rate;
123
+ const timeUntilKeyframe = nextTemporalKeyframe[0] - properties.t;
124
+ const isLooping = Math.abs(timeRemaining - timeUntilKeyframe) <= LOOPING_EPSILON_MS;
125
+ if (mediaElement.loop !== isLooping) {
126
+ mediaElement.loop = isLooping;
127
+ }
128
+ }
129
+ }
130
+ const currentTime = mediaElement.currentTime * 1000;
131
+ const deltaTime = currentTime - properties.t;
132
+ const deltaTimeAbs = Math.abs(deltaTime);
133
+ switch (true) {
134
+ case syncState.state === 'idle' && properties.rate > 0 && deltaTimeAbs <= OUTER_TARGET_SYNC_THRESHOLD_MS:
135
+ // We are on course:
136
+ // - The video is within accepted latency of the server time
137
+ // - The playback rate is aligned with the server rate
138
+ if (mediaElement.playbackRate !== properties.rate) {
139
+ mediaElement.playbackRate = properties.rate;
140
+ }
141
+ return { state: 'idle' };
142
+ case syncState.state === 'idle' &&
143
+ properties.rate > 0 &&
144
+ deltaTimeAbs > OUTER_TARGET_SYNC_THRESHOLD_MS &&
145
+ deltaTimeAbs <= MAX_SYNC_THRESHOLD_MS:
146
+ {
147
+ // We are close, we can smoothly adjust with playbackRate:
148
+ // - The video must be playing
149
+ // - We must be close in time to the server time
150
+ const playbackRateAdjustment = playbackSmoothing(deltaTime);
151
+ const adjustedPlaybackRate = Math.max(0, properties.rate - playbackRateAdjustment);
152
+ if (mediaElement.playbackRate !== adjustedPlaybackRate) {
153
+ mediaElement.playbackRate = adjustedPlaybackRate;
154
+ }
155
+ return { state: 'intercepting' };
156
+ }
157
+ case syncState.state === 'intercepting' && properties.rate > 0 && deltaTimeAbs <= INNER_TARGET_SYNC_THRESHOLD_MS: {
158
+ // We have intercepted, we can now play normally
159
+ if (mediaElement.playbackRate !== properties.rate) {
160
+ mediaElement.playbackRate = properties.rate;
161
+ }
162
+ return { state: 'idle' };
163
+ }
164
+ case syncState.state === 'intercepting' && Math.sign(deltaTime) === Math.sign(mediaElement.playbackRate - properties.rate): {
165
+ // We have missed our interception. Go back to idle to try again.
166
+ console.warn(deltaTime, 'missed intercept');
167
+ return { state: 'idle' };
168
+ }
169
+ case syncState.state === 'intercepting':
170
+ return { state: 'intercepting' };
171
+ case syncState.state === 'seeking': {
172
+ return { state: 'seeking' };
173
+ }
174
+ default: {
175
+ // We cannot smoothly recover:
176
+ // - We seek just ahead of server time
177
+ if (mediaElement.playbackRate !== properties.rate) {
178
+ mediaElement.playbackRate = properties.rate;
179
+ }
180
+ mediaElement.currentTime = (properties.t + properties.rate * SEEK_LOOKAHEAD_MS) / 1000;
181
+ return { state: 'seeking' };
182
+ }
183
+ }
184
+ }
185
+ export class ImageManager extends MediaClipManager {
186
+ imageElement;
187
+ update() {
188
+ const currentState = getStateAtTime(this._state, Date.now());
189
+ if (currentState) {
190
+ this.imageElement = assertElement(this.imageElement, this.clipElement, this._state, this.constructAssetURL, this.mediaPreloader);
191
+ }
192
+ else if (this.imageElement) {
193
+ this.destroy();
194
+ }
195
+ if (!currentState || !this.imageElement)
196
+ return;
197
+ assertVisualProperties(this.imageElement, currentState, this._state.fit);
198
+ }
199
+ destroy() {
200
+ if (this.imageElement) {
201
+ this.imageElement.remove();
202
+ this.imageElement.src = '';
203
+ this.imageElement = undefined;
204
+ }
205
+ }
206
+ }
207
+ export class AudioManager extends MediaClipManager {
208
+ syncState = { state: 'idle' };
209
+ audioElement;
210
+ update() {
211
+ const currentState = getStateAtTime(this._state, Date.now());
212
+ if (currentState) {
213
+ this.audioElement = assertElement(this.audioElement, this.clipElement, this._state, this.constructAssetURL, this.mediaPreloader);
214
+ }
215
+ else {
216
+ this.destroy();
217
+ }
218
+ if (!currentState || !this.audioElement)
219
+ return;
220
+ assertAudialProperties(this.audioElement, currentState, this._state.audioOutput);
221
+ const nextSyncState = assertTemporalProperties(this.audioElement, currentState, this._state.keyframes, this.syncState);
222
+ if (this.syncState.state !== 'seeking' && nextSyncState.state === 'seeking') {
223
+ this.audioElement.addEventListener('seeked', () => {
224
+ this.syncState = { state: 'idle' };
225
+ }, { passive: true, once: true });
226
+ }
227
+ this.syncState = nextSyncState;
228
+ }
229
+ destroy() {
230
+ this.audioElement?.remove();
231
+ this.audioElement = undefined;
232
+ }
233
+ }
234
+ export class VideoManager extends MediaClipManager {
235
+ syncState = { state: 'idle' };
236
+ videoElement;
237
+ update() {
238
+ const currentState = getStateAtTime(this._state, Date.now());
239
+ if (currentState) {
240
+ this.videoElement = assertElement(this.videoElement, this.clipElement, this._state, this.constructAssetURL, this.mediaPreloader);
241
+ }
242
+ else {
243
+ this.destroy();
244
+ }
245
+ if (!currentState || !this.videoElement)
246
+ return;
247
+ assertVisualProperties(this.videoElement, currentState, this._state.fit);
248
+ assertAudialProperties(this.videoElement, currentState, this._state.audioOutput);
249
+ const nextSyncState = assertTemporalProperties(this.videoElement, currentState, this._state.keyframes, this.syncState);
250
+ if (this.syncState.state !== 'seeking' && nextSyncState.state === 'seeking') {
251
+ this.videoElement.addEventListener('seeked', () => {
252
+ this.syncState = { state: 'idle' };
253
+ }, { passive: true, once: true });
254
+ }
255
+ this.syncState = nextSyncState;
256
+ }
257
+ destroy() {
258
+ this.videoElement?.remove();
259
+ this.videoElement = undefined;
260
+ }
261
+ }
@@ -0,0 +1,14 @@
1
+ import { MediaClientConfig } from '../types/CogsClientMessage';
2
+ export declare class MediaPreloader {
3
+ private _state;
4
+ private _elements;
5
+ private _constructAssetURL;
6
+ constructor(constructAssetURL: (file: string) => string, testState?: MediaClientConfig['files']);
7
+ get state(): {
8
+ [x: string]: import("../types/CogsClientMessage").Media;
9
+ };
10
+ setState(newState: MediaClientConfig['files']): void;
11
+ private update;
12
+ getElement(file: string, type: 'audio' | 'video'): HTMLAudioElement;
13
+ releaseElement(resource: string | HTMLMediaElement): void;
14
+ }
@@ -0,0 +1,86 @@
1
+ export class MediaPreloader {
2
+ _state;
3
+ _elements = {};
4
+ _constructAssetURL;
5
+ constructor(constructAssetURL, testState = {}) {
6
+ this._constructAssetURL = constructAssetURL;
7
+ this._state = testState;
8
+ }
9
+ get state() {
10
+ return { ...this._state };
11
+ }
12
+ setState(newState) {
13
+ this._state = newState;
14
+ this.update();
15
+ }
16
+ update() {
17
+ // Clean up previous elements
18
+ for (const [filename, media] of Object.entries(this._elements)) {
19
+ if (!(filename in this._state)) {
20
+ media.element.src = '';
21
+ delete this._elements[filename];
22
+ }
23
+ media.inUse = media.element.isConnected;
24
+ }
25
+ for (const [filename, fileConfig] of Object.entries(this._state)) {
26
+ if (filename in this._elements) {
27
+ continue;
28
+ }
29
+ // Create new elements
30
+ let preloadAttr;
31
+ if (fileConfig.preload === true) {
32
+ preloadAttr = 'auto';
33
+ }
34
+ else if (fileConfig.preload === false) {
35
+ preloadAttr = 'none';
36
+ }
37
+ else {
38
+ preloadAttr = fileConfig.preload;
39
+ }
40
+ switch (fileConfig.type) {
41
+ case 'audio': {
42
+ const element = document.createElement('audio');
43
+ element.src = this._constructAssetURL(filename);
44
+ element.preload = preloadAttr;
45
+ this._elements[filename] = { element, inUse: false, type: 'audio' };
46
+ break;
47
+ }
48
+ case 'video': {
49
+ const element = document.createElement('video');
50
+ element.src = this._constructAssetURL(filename);
51
+ element.preload = preloadAttr;
52
+ this._elements[filename] = { element, inUse: false, type: 'video' };
53
+ break;
54
+ }
55
+ }
56
+ }
57
+ }
58
+ getElement(file, type) {
59
+ const media = this._elements[file];
60
+ if (media && media.inUse === false) {
61
+ media.inUse = true;
62
+ return media.element;
63
+ }
64
+ else {
65
+ const element = document.createElement(type);
66
+ element.src = this._constructAssetURL(file);
67
+ this._elements[file] = { element, type, inUse: true };
68
+ return element;
69
+ }
70
+ }
71
+ releaseElement(resource) {
72
+ if (typeof resource === 'string') {
73
+ const media = this._elements[resource];
74
+ if (media) {
75
+ media.inUse = false;
76
+ }
77
+ }
78
+ else {
79
+ Object.entries(this._elements).forEach(([file, media]) => {
80
+ if (media.element === resource) {
81
+ delete this._elements[file];
82
+ }
83
+ });
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,19 @@
1
+ import { MediaSurfaceState } from '../types/MediaSchema';
2
+ import { MediaPreloader } from './MediaPreloader';
3
+ export declare const DATA_CLIP_ID = "data-clip-id";
4
+ /**
5
+ * The SurfaceManager will receive state updates and:
6
+ * - Ensure that each clip has a parent element
7
+ * - Instantiate a ClipManager attached to each respective element
8
+ */
9
+ export declare class SurfaceManager {
10
+ private constructAssetUrl;
11
+ private mediaPreloader;
12
+ private _state;
13
+ setState(newState: MediaSurfaceState): void;
14
+ private _element;
15
+ get element(): HTMLDivElement;
16
+ private resources;
17
+ constructor(constructAssetUrl: (file: string) => string, testState?: MediaSurfaceState, mediaPreloader?: MediaPreloader);
18
+ update(): void;
19
+ }
@@ -0,0 +1,84 @@
1
+ import { AudioManager, ImageManager, VideoManager } from './MediaClipManager';
2
+ import { MediaPreloader } from './MediaPreloader';
3
+ export const DATA_CLIP_ID = 'data-clip-id';
4
+ /**
5
+ * The SurfaceManager will receive state updates and:
6
+ * - Ensure that each clip has a parent element
7
+ * - Instantiate a ClipManager attached to each respective element
8
+ */
9
+ export class SurfaceManager {
10
+ constructAssetUrl;
11
+ mediaPreloader;
12
+ _state = {};
13
+ setState(newState) {
14
+ this._state = newState;
15
+ this.update();
16
+ }
17
+ _element;
18
+ get element() {
19
+ return this._element;
20
+ }
21
+ resources = {};
22
+ constructor(constructAssetUrl, testState, mediaPreloader = new MediaPreloader(constructAssetUrl)) {
23
+ this.constructAssetUrl = constructAssetUrl;
24
+ this.mediaPreloader = mediaPreloader;
25
+ this._element = document.createElement('div');
26
+ this._element.className = 'surface-manager';
27
+ this._element.style.width = '100%';
28
+ this._element.style.height = '100%';
29
+ this._state = testState || {};
30
+ this.update();
31
+ }
32
+ update() {
33
+ // Destroy stale managers
34
+ Object.entries(this.resources).forEach(([clipId, { element, manager }]) => {
35
+ if (!(clipId in this._state)) {
36
+ delete this.resources[clipId];
37
+ element.remove();
38
+ manager?.destroy();
39
+ }
40
+ });
41
+ // Create and attach new wrapper elements
42
+ const elements = Object.keys(this._state)
43
+ .toSorted()
44
+ .map((clipId) => {
45
+ const resource = this.resources[clipId];
46
+ if (resource) {
47
+ return resource.element;
48
+ }
49
+ else {
50
+ const element = document.createElement('div');
51
+ element.setAttribute(DATA_CLIP_ID, clipId);
52
+ this.resources[clipId] = { element };
53
+ return element;
54
+ }
55
+ });
56
+ this._element.replaceChildren(...elements);
57
+ // Create new managers
58
+ Object.keys(this._state)
59
+ .toSorted()
60
+ .forEach((clipId) => {
61
+ const clip = this._state[clipId];
62
+ const resource = this.resources[clipId];
63
+ if (!resource) {
64
+ throw new Error('Failed to create resource');
65
+ }
66
+ if (!resource.manager) {
67
+ switch (clip.type) {
68
+ case 'image':
69
+ resource.manager = new ImageManager(this._element, resource.element, clip, this.constructAssetUrl, this.mediaPreloader);
70
+ break;
71
+ case 'audio':
72
+ resource.manager = new AudioManager(this._element, resource.element, clip, this.constructAssetUrl, this.mediaPreloader);
73
+ break;
74
+ case 'video':
75
+ resource.manager = new VideoManager(this._element, resource.element, clip, this.constructAssetUrl, this.mediaPreloader);
76
+ break;
77
+ }
78
+ }
79
+ else {
80
+ resource.manager.setState(clip);
81
+ }
82
+ });
83
+ }
84
+ }
@@ -7,6 +7,7 @@ declare const TemporalProperties: z.ZodObject<{
7
7
  export type VisualProperties = z.infer<typeof VisualProperties>;
8
8
  declare const VisualProperties: z.ZodObject<{
9
9
  opacity: z.ZodNumber;
10
+ zIndex: z.ZodDefault<z.ZodNumber>;
10
11
  }, z.core.$strip>;
11
12
  export type AudialProperties = z.infer<typeof AudialProperties>;
12
13
  declare const AudialProperties: z.ZodObject<{
@@ -40,6 +41,7 @@ export type InitialImageKeyframe = z.infer<typeof InitialImageKeyframe>;
40
41
  declare const InitialImageKeyframe: z.ZodTuple<[z.ZodNumber, z.ZodObject<{
41
42
  set: z.ZodOptional<z.ZodObject<{
42
43
  opacity: z.ZodOptional<z.ZodNumber>;
44
+ zIndex: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
43
45
  }, z.core.$strip>>;
44
46
  }, z.core.$strip>], null>;
45
47
  /**
@@ -49,9 +51,11 @@ export type ImageKeyframe = z.infer<typeof ImageKeyframe>;
49
51
  declare const ImageKeyframe: z.ZodTuple<[z.ZodNumber, z.ZodObject<{
50
52
  set: z.ZodOptional<z.ZodObject<{
51
53
  opacity: z.ZodOptional<z.ZodNumber>;
54
+ zIndex: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
52
55
  }, z.core.$strip>>;
53
56
  lerp: z.ZodOptional<z.ZodObject<{
54
57
  opacity: z.ZodOptional<z.ZodNumber>;
58
+ zIndex: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
55
59
  }, z.core.$strip>>;
56
60
  }, z.core.$strip>], null>;
57
61
  /**
@@ -86,6 +90,7 @@ export type InitialVideoKeyframe = z.infer<typeof InitialVideoKeyframe>;
86
90
  declare const InitialVideoKeyframe: z.ZodTuple<[z.ZodNumber, z.ZodObject<{
87
91
  set: z.ZodOptional<z.ZodObject<{
88
92
  opacity: z.ZodOptional<z.ZodNumber>;
93
+ zIndex: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
89
94
  volume: z.ZodOptional<z.ZodNumber>;
90
95
  t: z.ZodOptional<z.ZodNumber>;
91
96
  rate: z.ZodOptional<z.ZodNumber>;
@@ -98,12 +103,14 @@ export type VideoKeyframe = z.infer<typeof VideoKeyframe>;
98
103
  declare const VideoKeyframe: z.ZodTuple<[z.ZodNumber, z.ZodObject<{
99
104
  set: z.ZodOptional<z.ZodObject<{
100
105
  opacity: z.ZodOptional<z.ZodNumber>;
106
+ zIndex: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
101
107
  volume: z.ZodOptional<z.ZodNumber>;
102
108
  t: z.ZodOptional<z.ZodNumber>;
103
109
  rate: z.ZodOptional<z.ZodNumber>;
104
110
  }, z.core.$strip>>;
105
111
  lerp: z.ZodOptional<z.ZodObject<{
106
112
  opacity: z.ZodOptional<z.ZodNumber>;
113
+ zIndex: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
107
114
  volume: z.ZodOptional<z.ZodNumber>;
108
115
  }, z.core.$strip>>;
109
116
  }, z.core.$strip>], null>;
@@ -111,13 +118,16 @@ export declare const MediaSurfaceStateSchema: z.ZodRecord<z.ZodString, z.ZodUnio
111
118
  keyframes: z.ZodTuple<[z.ZodTuple<[z.ZodNumber, z.ZodObject<{
112
119
  set: z.ZodOptional<z.ZodObject<{
113
120
  opacity: z.ZodOptional<z.ZodNumber>;
121
+ zIndex: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
114
122
  }, z.core.$strip>>;
115
123
  lerp: z.ZodOptional<z.ZodObject<{
116
124
  opacity: z.ZodOptional<z.ZodNumber>;
125
+ zIndex: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
117
126
  }, z.core.$strip>>;
118
127
  }, z.core.$strip>], null>], z.ZodUnion<readonly [z.ZodTuple<[z.ZodNumber, z.ZodObject<{
119
128
  set: z.ZodOptional<z.ZodObject<{
120
129
  opacity: z.ZodOptional<z.ZodNumber>;
130
+ zIndex: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
121
131
  }, z.core.$strip>>;
122
132
  }, z.core.$strip>], null>, z.ZodTuple<[z.ZodNumber, z.ZodNull], null>]>>;
123
133
  type: z.ZodLiteral<"image">;
@@ -147,17 +157,20 @@ export declare const MediaSurfaceStateSchema: z.ZodRecord<z.ZodString, z.ZodUnio
147
157
  keyframes: z.ZodTuple<[z.ZodTuple<[z.ZodNumber, z.ZodObject<{
148
158
  set: z.ZodOptional<z.ZodObject<{
149
159
  opacity: z.ZodOptional<z.ZodNumber>;
160
+ zIndex: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
150
161
  volume: z.ZodOptional<z.ZodNumber>;
151
162
  t: z.ZodOptional<z.ZodNumber>;
152
163
  rate: z.ZodOptional<z.ZodNumber>;
153
164
  }, z.core.$strip>>;
154
165
  lerp: z.ZodOptional<z.ZodObject<{
155
166
  opacity: z.ZodOptional<z.ZodNumber>;
167
+ zIndex: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
156
168
  volume: z.ZodOptional<z.ZodNumber>;
157
169
  }, z.core.$strip>>;
158
170
  }, z.core.$strip>], null>], z.ZodUnion<readonly [z.ZodTuple<[z.ZodNumber, z.ZodObject<{
159
171
  set: z.ZodOptional<z.ZodObject<{
160
172
  opacity: z.ZodOptional<z.ZodNumber>;
173
+ zIndex: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
161
174
  volume: z.ZodOptional<z.ZodNumber>;
162
175
  t: z.ZodOptional<z.ZodNumber>;
163
176
  rate: z.ZodOptional<z.ZodNumber>;
@@ -5,6 +5,7 @@ const TemporalProperties = z.object({
5
5
  });
6
6
  const VisualProperties = z.object({
7
7
  opacity: z.number().gte(0).lte(1),
8
+ zIndex: z.number().default(0),
8
9
  });
9
10
  const AudialProperties = z.object({
10
11
  volume: z.number().gte(0).lte(1),
@@ -139,6 +140,7 @@ true;
139
140
  true;
140
141
  export const defaultImageOptions = {
141
142
  opacity: 1,
143
+ zIndex: 0,
142
144
  };
143
145
  export const defaultAudioOptions = {
144
146
  t: 0,
@@ -150,4 +152,5 @@ export const defaultVideoOptions = {
150
152
  rate: 1,
151
153
  volume: 1,
152
154
  opacity: 1,
155
+ zIndex: 0,
153
156
  };
@@ -47,11 +47,11 @@ export function getPropertiesAtTime(keyframes, time) {
47
47
  // If lerp and set are both present, we assume we lerp up until the timestamp,
48
48
  // then set to a new value
49
49
  Object.entries(properties.lerp ?? {}).forEach(([property, value]) => {
50
- propertyKeyframes[property] ?? (propertyKeyframes[property] = {});
50
+ propertyKeyframes[property] ??= {};
51
51
  propertyKeyframes[property].before = [timestamp, value];
52
52
  });
53
53
  Object.entries(properties.set ?? {}).forEach(([property, value]) => {
54
- propertyKeyframes[property] ?? (propertyKeyframes[property] = {});
54
+ propertyKeyframes[property] ??= {};
55
55
  propertyKeyframes[property].before = [timestamp, value];
56
56
  });
57
57
  }
@@ -59,7 +59,7 @@ export function getPropertiesAtTime(keyframes, time) {
59
59
  // We're trying to find the closest timestamp afterwards for lerping
60
60
  // So only set if not yet set
61
61
  Object.entries(properties.lerp ?? {}).forEach(([property, value]) => {
62
- propertyKeyframes[property] ?? (propertyKeyframes[property] = {});
62
+ propertyKeyframes[property] ??= {};
63
63
  if (propertyKeyframes[property].after === undefined) {
64
64
  propertyKeyframes[property].after = [timestamp, value];
65
65
  }