@clockworkdog/cogs-client 3.0.0-alpha.8 → 3.0.0-alpha.9

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
@@ -5,8 +5,6 @@ export type { default as MediaClipStateMessage } from './types/MediaClipStateMes
5
5
  export type { default as ShowPhase } from './types/ShowPhase';
6
6
  export type { default as MediaObjectFit } from './types/MediaObjectFit';
7
7
  export * as MediaSchema from './types/MediaSchema';
8
- export * from './state-based/SurfaceManager';
9
- export * from './state-based/MediaPreloader';
10
8
  export { default as CogsAudioPlayer } from './AudioPlayer';
11
9
  export { default as CogsVideoPlayer } from './VideoPlayer';
12
10
  export { SurfaceManager } from './state-based/SurfaceManager';
package/dist/index.js CHANGED
@@ -1,8 +1,6 @@
1
1
  export { default as CogsConnection } from './CogsConnection';
2
2
  export * from './CogsConnection';
3
3
  export * as MediaSchema from './types/MediaSchema';
4
- export * from './state-based/SurfaceManager';
5
- export * from './state-based/MediaPreloader';
6
4
  export { default as CogsAudioPlayer } from './AudioPlayer';
7
5
  export { default as CogsVideoPlayer } from './VideoPlayer';
8
6
  export { SurfaceManager } from './state-based/SurfaceManager';
@@ -1,11 +1,9 @@
1
1
  import { AudioState } from '../types/MediaSchema';
2
2
  import { ClipManager } from './ClipManager';
3
- import { MediaPreloader } from './MediaPreloader';
4
3
  export declare class AudioManager extends ClipManager<AudioState> {
5
- private mediaPreloader;
6
4
  private audioElement?;
7
5
  private isSeeking;
8
- constructor(surfaceElement: HTMLElement, clipElement: HTMLElement, state: AudioState, constructAssetURL: (file: string) => string, mediaPreloader: MediaPreloader);
6
+ constructor(surfaceElement: HTMLElement, clipElement: HTMLElement, state: AudioState);
9
7
  private updateAudioElement;
10
8
  /**
11
9
  * Helper function to seek to a specified time.
@@ -1,7 +1,9 @@
1
1
  import { defaultAudioOptions } from '../types/MediaSchema';
2
2
  import { getStateAtTime } from '../utils/getStateAtTime';
3
3
  import { ClipManager } from './ClipManager';
4
- const DEFAULT_AUDIO_POLLING = 1_000;
4
+ const NO_AUDIO_POLLING = 1_000;
5
+ const AUDIO_PLAYBACK_POLLING = 100;
6
+ const SEEKING_POLLING = 10;
5
7
  const TARGET_SYNC_THRESHOLD_MS = 10; // If we're closer than this we're good enough
6
8
  const MAX_SYNC_THRESHOLD_MS = 1_000; // If we're further away than this, we'll seek instead
7
9
  const SEEK_LOOKAHEAD_MS = 200; // We won't seek ahead instantly, so lets seek ahead
@@ -12,17 +14,15 @@ function playbackSmoothing(deltaTime) {
12
14
  return Math.sign(deltaTime) * Math.pow(Math.abs(deltaTime) / MAX_SYNC_THRESHOLD_MS, PLAYBACK_ADJUSTMENT_SMOOTHING) * MAX_PLAYBACK_RATE_ADJUSTMENT;
13
15
  }
14
16
  export class AudioManager extends ClipManager {
15
- mediaPreloader;
16
17
  audioElement;
17
18
  isSeeking = false;
18
- constructor(surfaceElement, clipElement, state, constructAssetURL, mediaPreloader) {
19
- super(surfaceElement, clipElement, state, constructAssetURL);
20
- this.mediaPreloader = mediaPreloader;
19
+ constructor(surfaceElement, clipElement, state) {
20
+ super(surfaceElement, clipElement, state);
21
21
  this.clipElement = clipElement;
22
22
  }
23
23
  updateAudioElement() {
24
- const element = this.mediaPreloader.getElement(this._state.file, 'audio');
25
- this.audioElement = element;
24
+ this.destroy();
25
+ this.audioElement = document.createElement('audio');
26
26
  this.clipElement.replaceChildren(this.audioElement);
27
27
  }
28
28
  /**
@@ -32,6 +32,8 @@ export class AudioManager extends ClipManager {
32
32
  seekTo(time) {
33
33
  if (!this.audioElement)
34
34
  return;
35
+ this.delay = SEEKING_POLLING;
36
+ this.isSeeking = true;
35
37
  this.audioElement.addEventListener('seeked', () => {
36
38
  this.isSeeking = false;
37
39
  }, { once: true, passive: true });
@@ -41,7 +43,7 @@ export class AudioManager extends ClipManager {
41
43
  // Update loop used to poll until seek finished
42
44
  if (this.isSeeking)
43
45
  return;
44
- this.delay = DEFAULT_AUDIO_POLLING;
46
+ this.delay = NO_AUDIO_POLLING;
45
47
  // Does the <audio /> element need adding/removing?
46
48
  const currentState = getStateAtTime(this._state, Date.now());
47
49
  if (currentState) {
@@ -55,10 +57,9 @@ export class AudioManager extends ClipManager {
55
57
  if (!currentState || !this.audioElement)
56
58
  return;
57
59
  const { t, rate, volume } = { ...defaultAudioOptions, ...currentState };
58
- // this.videoElement.src will be a fully qualified URL
59
- const assetURL = this.constructAssetURL(this._state.file);
60
- if (!this.audioElement.src.includes(assetURL)) {
61
- this.updateAudioElement();
60
+ // this.audioElement.src will be a fully qualified URL
61
+ if (!this.audioElement.src.endsWith(this._state.file)) {
62
+ this.audioElement.src = this._state.file;
62
63
  }
63
64
  if (this.audioElement.volume !== volume) {
64
65
  this.audioElement.volume = volume;
@@ -72,7 +73,7 @@ export class AudioManager extends ClipManager {
72
73
  const currentTime = this.audioElement.currentTime * 1000;
73
74
  const deltaTime = currentTime - t;
74
75
  const deltaTimeAbs = Math.abs(deltaTime);
75
- this.delay = 100;
76
+ this.delay = AUDIO_PLAYBACK_POLLING;
76
77
  switch (true) {
77
78
  case deltaTimeAbs <= TARGET_SYNC_THRESHOLD_MS:
78
79
  // We are on course:
@@ -108,6 +109,7 @@ export class AudioManager extends ClipManager {
108
109
  }
109
110
  destroy() {
110
111
  if (this.audioElement) {
112
+ this.audioElement.src = '';
111
113
  this.audioElement.remove();
112
114
  }
113
115
  }
@@ -6,8 +6,7 @@ import { MediaClipState } from '../types/MediaSchema';
6
6
  export declare abstract class ClipManager<T extends MediaClipState> {
7
7
  private surfaceElement;
8
8
  protected clipElement: HTMLElement;
9
- protected constructAssetURL: (file: string) => string;
10
- constructor(surfaceElement: HTMLElement, clipElement: HTMLElement, state: T, constructAssetURL: (file: string) => string);
9
+ constructor(surfaceElement: HTMLElement, clipElement: HTMLElement, state: T);
11
10
  /**
12
11
  * This is the delay to be used in the update loop.
13
12
  * It is intended to be dynamic for each loop.
@@ -6,11 +6,9 @@ const DEFAULT_DELAY = 1_000;
6
6
  export class ClipManager {
7
7
  surfaceElement;
8
8
  clipElement;
9
- constructAssetURL;
10
- constructor(surfaceElement, clipElement, state, constructAssetURL) {
9
+ constructor(surfaceElement, clipElement, state) {
11
10
  this.surfaceElement = surfaceElement;
12
11
  this.clipElement = clipElement;
13
- this.constructAssetURL = constructAssetURL;
14
12
  this._state = state;
15
13
  // Allow the class to be constructed, then call the loop
16
14
  setTimeout(this.loop);
@@ -38,9 +36,6 @@ export class ClipManager {
38
36
  }
39
37
  _state;
40
38
  setState(newState) {
41
- if (this._state.file !== newState.file) {
42
- throw new Error(`Cannot change from ${this._state.file} to ${newState.file}. Create a new clip instead.`);
43
- }
44
39
  this._state = newState;
45
40
  clearTimeout(this.timeout);
46
41
  this.loop();
@@ -49,9 +44,7 @@ export class ClipManager {
49
44
  loop = async () => {
50
45
  if (this.isConnected()) {
51
46
  this.update();
52
- if (isFinite(this.delay)) {
53
- this.timeout = setTimeout(this.loop, this.delay);
54
- }
47
+ this.timeout = setTimeout(this.loop, this.delay);
55
48
  }
56
49
  else {
57
50
  this.destroy();
@@ -2,7 +2,7 @@ import { ImageState } from '../types/MediaSchema';
2
2
  import { ClipManager } from './ClipManager';
3
3
  export declare class ImageManager extends ClipManager<ImageState> {
4
4
  private imageElement?;
5
- constructor(surfaceElement: HTMLElement, clipElement: HTMLElement, state: ImageState, constructAssetURL: (file: string) => string);
5
+ constructor(surfaceElement: HTMLElement, clipElement: HTMLElement, state: ImageState);
6
6
  private updateImageElement;
7
7
  protected update(): void;
8
8
  destroy(): void;
@@ -3,8 +3,8 @@ import { getStateAtTime } from '../utils/getStateAtTime';
3
3
  import { ClipManager } from './ClipManager';
4
4
  export class ImageManager extends ClipManager {
5
5
  imageElement;
6
- constructor(surfaceElement, clipElement, state, constructAssetURL) {
7
- super(surfaceElement, clipElement, state, constructAssetURL);
6
+ constructor(surfaceElement, clipElement, state) {
7
+ super(surfaceElement, clipElement, state);
8
8
  this.clipElement = clipElement;
9
9
  }
10
10
  updateImageElement() {
@@ -12,7 +12,7 @@ export class ImageManager extends ClipManager {
12
12
  this.clipElement.replaceChildren(this.imageElement);
13
13
  this.imageElement.style.position = 'absolute';
14
14
  this.imageElement.style.height = '100%';
15
- this.imageElement.style.widows = '100%';
15
+ this.imageElement.style.width = '100%';
16
16
  }
17
17
  update() {
18
18
  const currentState = getStateAtTime(this._state, Date.now());
@@ -28,10 +28,9 @@ export class ImageManager extends ClipManager {
28
28
  }
29
29
  if (!this.imageElement || !currentState)
30
30
  return;
31
- // this.videoElement.src will be a fully qualified URL
32
- const assetURL = this.constructAssetURL(this._state.file);
33
- if (!this.imageElement.src.includes(assetURL)) {
34
- this.imageElement.src = assetURL;
31
+ // this.imageElement.src will be a fully qualified URL
32
+ if (!this.imageElement.src.endsWith(this._state.file)) {
33
+ this.imageElement.src = this._state.file;
35
34
  }
36
35
  if (this.imageElement.style.objectFit !== this._state.fit) {
37
36
  this.imageElement.style.objectFit = this._state.fit;
@@ -43,9 +42,9 @@ export class ImageManager extends ClipManager {
43
42
  if (parseInt(this.imageElement.style.zIndex) !== z) {
44
43
  this.imageElement.style.zIndex = String(z);
45
44
  }
46
- const { opacity } = currentState;
47
- if (typeof opacity === 'string' && opacity !== this.imageElement.style.opacity) {
48
- this.imageElement.style.opacity = opacity;
45
+ const opacityString = String(currentState.opacity);
46
+ if (this.imageElement.style.opacity !== opacityString) {
47
+ this.imageElement.style.opacity = opacityString;
49
48
  }
50
49
  }
51
50
  destroy() {
@@ -1,5 +1,4 @@
1
1
  import { MediaSurfaceState } from '../types/MediaSchema';
2
- import { MediaPreloader } from './MediaPreloader';
3
2
  export declare const DATA_CLIP_ID = "data-clip-id";
4
3
  /**
5
4
  * The SurfaceManager will receive state updates and:
@@ -7,13 +6,11 @@ export declare const DATA_CLIP_ID = "data-clip-id";
7
6
  * - Instantiate a ClipManager attached to each respective element
8
7
  */
9
8
  export declare class SurfaceManager {
10
- private constructAssetUrl;
11
- private mediaPreloader;
12
9
  private _state;
13
10
  setState(newState: MediaSurfaceState): void;
14
11
  private _element;
15
12
  get element(): HTMLDivElement;
16
13
  private resources;
17
- constructor(constructAssetUrl: (file: string) => string, initialState?: MediaSurfaceState, mediaPreloader?: MediaPreloader);
14
+ constructor(testState?: MediaSurfaceState);
18
15
  update(): void;
19
16
  }
@@ -1,7 +1,6 @@
1
1
  import { ImageManager } from './ImageManager';
2
2
  import { VideoManager } from './VideoManager';
3
3
  import { AudioManager } from './AudioManager';
4
- import { MediaPreloader } from './MediaPreloader';
5
4
  export const DATA_CLIP_ID = 'data-clip-id';
6
5
  /**
7
6
  * The SurfaceManager will receive state updates and:
@@ -9,8 +8,6 @@ export const DATA_CLIP_ID = 'data-clip-id';
9
8
  * - Instantiate a ClipManager attached to each respective element
10
9
  */
11
10
  export class SurfaceManager {
12
- constructAssetUrl;
13
- mediaPreloader;
14
11
  _state = {};
15
12
  setState(newState) {
16
13
  this._state = newState;
@@ -21,14 +18,11 @@ export class SurfaceManager {
21
18
  return this._element;
22
19
  }
23
20
  resources = {};
24
- constructor(constructAssetUrl, initialState = {}, mediaPreloader = new MediaPreloader(constructAssetUrl)) {
25
- this.constructAssetUrl = constructAssetUrl;
26
- this.mediaPreloader = mediaPreloader;
21
+ constructor(testState) {
27
22
  this._element = document.createElement('div');
28
- this._element.className = 'surface-manager';
29
23
  this._element.style.width = '100%';
30
24
  this._element.style.height = '100%';
31
- this._state = initialState || {};
25
+ this._state = testState || {};
32
26
  this.update();
33
27
  }
34
28
  update() {
@@ -68,13 +62,13 @@ export class SurfaceManager {
68
62
  if (!resource.manager) {
69
63
  switch (clip.type) {
70
64
  case 'image':
71
- resource.manager = new ImageManager(this._element, resource.element, clip, this.constructAssetUrl);
65
+ resource.manager = new ImageManager(this._element, resource.element, clip);
72
66
  break;
73
67
  case 'audio':
74
- resource.manager = new AudioManager(this._element, resource.element, clip, this.constructAssetUrl, this.mediaPreloader);
68
+ resource.manager = new AudioManager(this._element, resource.element, clip);
75
69
  break;
76
70
  case 'video':
77
- resource.manager = new VideoManager(this._element, resource.element, clip, this.constructAssetUrl, this.mediaPreloader);
71
+ resource.manager = new VideoManager(this._element, resource.element, clip);
78
72
  break;
79
73
  }
80
74
  }
@@ -1,14 +1,10 @@
1
1
  import { VideoState } from '../types/MediaSchema';
2
2
  import { ClipManager } from './ClipManager';
3
- import { MediaPreloader } from './MediaPreloader';
4
3
  export declare class VideoManager extends ClipManager<VideoState> {
5
- private mediaPreloader;
6
4
  private videoElement?;
7
5
  private isSeeking;
8
- private timeToIntercept;
9
- constructor(surfaceElement: HTMLElement, clipElement: HTMLElement, state: VideoState, constructAssetURL: (file: string) => string, mediaPreloader: MediaPreloader);
6
+ constructor(surfaceElement: HTMLElement, clipElement: HTMLElement, state: VideoState);
10
7
  private updateVideoElement;
11
- private get videoDuration();
12
8
  /**
13
9
  * Helper function to seek to a specified time.
14
10
  * Works with the update loop to poll until seeked event has fired.
@@ -1,80 +1,54 @@
1
1
  import { defaultVideoOptions } from '../types/MediaSchema';
2
2
  import { getStateAtTime } from '../utils/getStateAtTime';
3
3
  import { ClipManager } from './ClipManager';
4
- const DEFAULT_VIDEO_POLLING_MS = 1_000;
4
+ const NO_VIDEO_POLLING = 1_000;
5
+ const VIDEO_PLAYBACK_POLLING = 100;
6
+ const SEEKING_POLLING = 10;
5
7
  const TARGET_SYNC_THRESHOLD_MS = 10; // If we're closer than this we're good enough
6
8
  const MAX_SYNC_THRESHOLD_MS = 1_000; // If we're further away than this, we'll seek instead
7
9
  const SEEK_LOOKAHEAD_MS = 200; // We won't seek ahead instantly, so lets seek ahead
8
- const MAX_PLAYBACK_RATE_ADJUSTMENT = 0.15; // Don't speed up or slow down the video more than this
9
- const INTERCEPTION_EARLY_CHECK_IN = 0.7; // When on course for interception of server time, how early to check in beforehand.
10
+ const MAX_PLAYBACK_RATE_ADJUSTMENT = 0.5;
10
11
  // We smoothly ramp playbackRate up and down
11
12
  const PLAYBACK_ADJUSTMENT_SMOOTHING = 0.3;
12
13
  function playbackSmoothing(deltaTime) {
13
- return -Math.sign(deltaTime) * Math.pow(Math.abs(deltaTime) / MAX_SYNC_THRESHOLD_MS, PLAYBACK_ADJUSTMENT_SMOOTHING) * MAX_PLAYBACK_RATE_ADJUSTMENT;
14
- }
15
- // If we notice that at the end of the current playback, we set t=0 we should loop
16
- const LOOPING_EPSILON_MS = 5;
17
- function isLooping(state, time, duration) {
18
- const currentState = getStateAtTime(state, time);
19
- if (!currentState)
20
- return false;
21
- const { t, rate } = currentState;
22
- if (t === undefined || rate === undefined)
23
- return false;
24
- const nextTemporalKeyframe = state.keyframes.filter(([t, kf]) => t > time && (kf?.set?.t !== undefined || kf?.set?.rate !== undefined))[0];
25
- if (nextTemporalKeyframe?.[1]?.set?.t !== 0)
26
- return false;
27
- const timeRemaining = (duration - t) / rate;
28
- const timeUntilKeyframe = nextTemporalKeyframe[0] - time;
29
- return Math.abs(timeRemaining - timeUntilKeyframe) <= LOOPING_EPSILON_MS;
14
+ return Math.sign(deltaTime) * Math.pow(Math.abs(deltaTime) / MAX_SYNC_THRESHOLD_MS, PLAYBACK_ADJUSTMENT_SMOOTHING) * MAX_PLAYBACK_RATE_ADJUSTMENT;
30
15
  }
31
16
  export class VideoManager extends ClipManager {
32
- mediaPreloader;
33
17
  videoElement;
34
- // We seek to another part of the video and do nothing until we get there
35
18
  isSeeking = false;
36
- // We change playbackRate to intercept the server time of the video and don't change course until we intercept
37
- timeToIntercept = undefined;
38
- constructor(surfaceElement, clipElement, state, constructAssetURL, mediaPreloader) {
39
- super(surfaceElement, clipElement, state, constructAssetURL);
40
- this.mediaPreloader = mediaPreloader;
19
+ constructor(surfaceElement, clipElement, state) {
20
+ super(surfaceElement, clipElement, state);
41
21
  this.clipElement = clipElement;
42
22
  }
43
23
  updateVideoElement() {
44
- const element = this.mediaPreloader.getElement(this._state.file, 'video');
45
- this.videoElement = element;
24
+ this.destroy();
25
+ this.videoElement = document.createElement('video');
46
26
  this.clipElement.replaceChildren(this.videoElement);
47
27
  this.videoElement.style.position = 'absolute';
48
28
  this.videoElement.style.width = '100%';
49
29
  this.videoElement.style.height = '100%';
50
30
  }
51
- get videoDuration() {
52
- if (!this.videoElement)
53
- return undefined;
54
- if (this.videoElement.readyState < HTMLMediaElement.HAVE_METADATA)
55
- return undefined;
56
- return this.videoElement.duration * 1000;
57
- }
58
31
  /**
59
32
  * Helper function to seek to a specified time.
60
33
  * Works with the update loop to poll until seeked event has fired.
61
34
  */
62
- seekTo(time) {
35
+ seekTo(ms) {
63
36
  if (!this.videoElement)
64
37
  return;
38
+ this.delay = SEEKING_POLLING;
39
+ this.isSeeking = true;
65
40
  this.videoElement.addEventListener('seeked', () => {
66
- console.debug('seeked');
67
41
  this.isSeeking = false;
68
42
  }, { once: true, passive: true });
69
- this.videoElement.currentTime = time / 1_000;
43
+ this.videoElement.currentTime = ms / 1_000;
70
44
  }
71
45
  update() {
72
46
  // Update loop used to poll until seek finished
73
47
  if (this.isSeeking)
74
48
  return;
49
+ this.delay = NO_VIDEO_POLLING;
75
50
  // Does the <video /> element need adding/removing?
76
- const now = Date.now();
77
- const currentState = getStateAtTime(this._state, now);
51
+ const currentState = getStateAtTime(this._state, Date.now());
78
52
  if (currentState) {
79
53
  if (!this.videoElement || !this.isConnected(this.videoElement)) {
80
54
  this.updateVideoElement();
@@ -88,108 +62,51 @@ export class VideoManager extends ClipManager {
88
62
  return;
89
63
  const { t, rate, volume } = { ...defaultVideoOptions, ...currentState };
90
64
  // this.videoElement.src will be a fully qualified URL
91
- const assetURL = this.constructAssetURL(this._state.file);
92
- if (!this.videoElement.src.includes(assetURL)) {
93
- this.updateVideoElement();
65
+ if (!this.videoElement.src.endsWith(this._state.file)) {
66
+ this.videoElement.src = this._state.file;
94
67
  }
95
68
  if (this.videoElement.style.objectFit !== this._state.fit) {
96
69
  this.videoElement.style.objectFit = this._state.fit;
97
70
  }
98
- if (parseFloat(this.videoElement.style.opacity) !== currentState.opacity) {
99
- this.videoElement.style.opacity = String(currentState.opacity ?? defaultVideoOptions.opacity);
71
+ const opacityString = String(currentState.opacity);
72
+ if (this.videoElement.style.opacity !== opacityString) {
73
+ this.videoElement.style.opacity = opacityString;
100
74
  }
101
- const z = Math.round(currentState.zIndex ?? defaultVideoOptions.zIndex);
102
- if (parseInt(this.videoElement.style.zIndex) !== z) {
103
- this.videoElement.style.zIndex = String(z);
75
+ const zIndex = Math.round(currentState.zIndex ?? defaultVideoOptions.zIndex);
76
+ if (parseInt(this.videoElement.style.zIndex) !== zIndex) {
77
+ this.videoElement.style.zIndex = String(zIndex);
104
78
  }
105
79
  if (this.videoElement.volume !== volume) {
106
80
  this.videoElement.volume = volume;
107
81
  }
108
- const duration = this.videoDuration;
109
- if (duration !== undefined) {
110
- // Is the video looping?
111
- if (isLooping(this._state, now, duration)) {
112
- if (!this.videoElement.loop) {
113
- console.debug('starting loop');
114
- this.videoElement.loop = true;
115
- }
116
- }
117
- else {
118
- if (this.videoElement.loop) {
119
- console.debug('stopping loop');
120
- this.videoElement.loop = false;
121
- }
122
- // Has the video finished
123
- if (t > duration) {
124
- console.debug('ended');
125
- this.delay = Infinity;
126
- return;
127
- }
128
- }
129
- }
130
- // Should the video be playing
82
+ // Should the element be playing?
131
83
  if (this.videoElement.paused && rate > 0) {
132
- if (duration === undefined || duration > t) {
133
- this.videoElement.play().catch(() => {
134
- // Do nothing - this will be retried in the next loop
135
- });
136
- }
84
+ this.videoElement.play().catch(() => {
85
+ // Do nothing - this will be retried in the next loop
86
+ });
137
87
  }
138
88
  const currentTime = this.videoElement.currentTime * 1000;
139
89
  const deltaTime = currentTime - t;
140
90
  const deltaTimeAbs = Math.abs(deltaTime);
141
- // Handle current playbackRateAdjustment
142
- if (this.timeToIntercept !== undefined) {
143
- if (deltaTimeAbs <= TARGET_SYNC_THRESHOLD_MS) {
144
- // We've successfully got back on track
145
- console.log('intercepted', `${deltaTime.toFixed(0)}ms`);
146
- this.timeToIntercept = undefined;
147
- }
148
- else {
149
- const newTimeToIntercept = deltaTime / (rate - this.videoElement.playbackRate);
150
- if (newTimeToIntercept < this.timeToIntercept && newTimeToIntercept > 0) {
151
- // We're getting there, let's stay on course
152
- console.debug(`intercepting ${newTimeToIntercept.toFixed(0)}ms`, `${deltaTime.toFixed(0)}ms`);
153
- this.timeToIntercept = newTimeToIntercept;
154
- }
155
- else {
156
- // We've gone too far
157
- console.debug('missed intercept', deltaTime, this.timeToIntercept, newTimeToIntercept);
158
- this.timeToIntercept = undefined;
159
- }
160
- }
161
- }
91
+ this.delay = VIDEO_PLAYBACK_POLLING;
162
92
  switch (true) {
163
- case deltaTimeAbs <= TARGET_SYNC_THRESHOLD_MS: {
93
+ case deltaTimeAbs <= TARGET_SYNC_THRESHOLD_MS:
164
94
  // We are on course:
165
95
  // - The video is within accepted latency of the server time
166
96
  // - The playback rate is aligned with the server rate
167
- console.debug(`${rate}x`, deltaTime.toFixed(0));
168
- this.timeToIntercept = undefined;
169
97
  if (this.videoElement.playbackRate !== rate) {
170
98
  this.videoElement.playbackRate = rate;
171
99
  }
172
- this.delay = DEFAULT_VIDEO_POLLING_MS;
173
- break;
174
- }
175
- case this.timeToIntercept !== undefined:
176
- // We are currently on course to intercept
177
- // - We don't want to adjust the playbackRate excessively to pop audio
178
- // - We are on track to get back on time. So we can wait.
179
- this.delay = this.timeToIntercept * INTERCEPTION_EARLY_CHECK_IN;
180
100
  break;
181
- case rate > 0 && deltaTimeAbs > TARGET_SYNC_THRESHOLD_MS && deltaTimeAbs <= MAX_SYNC_THRESHOLD_MS && this.timeToIntercept === undefined: {
101
+ case rate > 0 && deltaTimeAbs > TARGET_SYNC_THRESHOLD_MS && deltaTimeAbs <= MAX_SYNC_THRESHOLD_MS: {
182
102
  // We are close, we can smoothly adjust with playbackRate:
183
103
  // - The video must be playing
184
104
  // - We must be close in time to the server time
185
105
  const playbackRateAdjustment = playbackSmoothing(deltaTime);
186
- const adjustedPlaybackRate = Math.max(0, rate + playbackRateAdjustment);
187
- this.timeToIntercept = deltaTime / (rate - adjustedPlaybackRate);
188
- console.debug(`${adjustedPlaybackRate.toFixed(2)}x`, `${deltaTime.toFixed(0)}ms`);
106
+ const adjustedPlaybackRate = Math.max(0, rate - playbackRateAdjustment);
189
107
  if (this.videoElement.playbackRate !== adjustedPlaybackRate) {
190
108
  this.videoElement.playbackRate = adjustedPlaybackRate;
191
109
  }
192
- this.delay = this.timeToIntercept * INTERCEPTION_EARLY_CHECK_IN;
193
110
  break;
194
111
  }
195
112
  default: {
@@ -198,9 +115,6 @@ export class VideoManager extends ClipManager {
198
115
  if (this.videoElement.playbackRate !== rate) {
199
116
  this.videoElement.playbackRate = rate;
200
117
  }
201
- // delay to poll until seeked
202
- console.debug('seeking');
203
- this.delay = 10;
204
118
  this.seekTo(t + rate * SEEK_LOOKAHEAD_MS);
205
119
  break;
206
120
  }
@@ -208,7 +122,7 @@ export class VideoManager extends ClipManager {
208
122
  }
209
123
  destroy() {
210
124
  if (this.videoElement) {
211
- this.mediaPreloader.releaseElement(this.videoElement);
125
+ this.videoElement.src = '';
212
126
  this.videoElement.remove();
213
127
  }
214
128
  }
@@ -7,7 +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.ZodNumber;
10
+ zIndex: z.ZodDefault<z.ZodNumber>;
11
11
  }, z.core.$strip>;
12
12
  export type AudialProperties = z.infer<typeof AudialProperties>;
13
13
  declare const AudialProperties: z.ZodObject<{
@@ -41,7 +41,7 @@ export type InitialImageKeyframe = z.infer<typeof InitialImageKeyframe>;
41
41
  declare const InitialImageKeyframe: z.ZodTuple<[z.ZodNumber, z.ZodObject<{
42
42
  set: z.ZodOptional<z.ZodObject<{
43
43
  opacity: z.ZodOptional<z.ZodNumber>;
44
- zIndex: z.ZodOptional<z.ZodNumber>;
44
+ zIndex: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
45
45
  }, z.core.$strip>>;
46
46
  }, z.core.$strip>], null>;
47
47
  /**
@@ -51,11 +51,11 @@ export type ImageKeyframe = z.infer<typeof ImageKeyframe>;
51
51
  declare const ImageKeyframe: z.ZodTuple<[z.ZodNumber, z.ZodObject<{
52
52
  set: z.ZodOptional<z.ZodObject<{
53
53
  opacity: z.ZodOptional<z.ZodNumber>;
54
- zIndex: z.ZodOptional<z.ZodNumber>;
54
+ zIndex: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
55
55
  }, z.core.$strip>>;
56
56
  lerp: z.ZodOptional<z.ZodObject<{
57
57
  opacity: z.ZodOptional<z.ZodNumber>;
58
- zIndex: z.ZodOptional<z.ZodNumber>;
58
+ zIndex: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
59
59
  }, z.core.$strip>>;
60
60
  }, z.core.$strip>], null>;
61
61
  /**
@@ -90,7 +90,7 @@ export type InitialVideoKeyframe = z.infer<typeof InitialVideoKeyframe>;
90
90
  declare const InitialVideoKeyframe: z.ZodTuple<[z.ZodNumber, z.ZodObject<{
91
91
  set: z.ZodOptional<z.ZodObject<{
92
92
  opacity: z.ZodOptional<z.ZodNumber>;
93
- zIndex: z.ZodOptional<z.ZodNumber>;
93
+ zIndex: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
94
94
  volume: z.ZodOptional<z.ZodNumber>;
95
95
  t: z.ZodOptional<z.ZodNumber>;
96
96
  rate: z.ZodOptional<z.ZodNumber>;
@@ -103,14 +103,14 @@ export type VideoKeyframe = z.infer<typeof VideoKeyframe>;
103
103
  declare const VideoKeyframe: z.ZodTuple<[z.ZodNumber, z.ZodObject<{
104
104
  set: z.ZodOptional<z.ZodObject<{
105
105
  opacity: z.ZodOptional<z.ZodNumber>;
106
- zIndex: z.ZodOptional<z.ZodNumber>;
106
+ zIndex: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
107
107
  volume: z.ZodOptional<z.ZodNumber>;
108
108
  t: z.ZodOptional<z.ZodNumber>;
109
109
  rate: z.ZodOptional<z.ZodNumber>;
110
110
  }, z.core.$strip>>;
111
111
  lerp: z.ZodOptional<z.ZodObject<{
112
112
  opacity: z.ZodOptional<z.ZodNumber>;
113
- zIndex: z.ZodOptional<z.ZodNumber>;
113
+ zIndex: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
114
114
  volume: z.ZodOptional<z.ZodNumber>;
115
115
  }, z.core.$strip>>;
116
116
  }, z.core.$strip>], null>;
@@ -118,16 +118,16 @@ export declare const MediaSurfaceStateSchema: z.ZodRecord<z.ZodString, z.ZodUnio
118
118
  keyframes: z.ZodTuple<[z.ZodTuple<[z.ZodNumber, z.ZodObject<{
119
119
  set: z.ZodOptional<z.ZodObject<{
120
120
  opacity: z.ZodOptional<z.ZodNumber>;
121
- zIndex: z.ZodOptional<z.ZodNumber>;
121
+ zIndex: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
122
122
  }, z.core.$strip>>;
123
123
  lerp: z.ZodOptional<z.ZodObject<{
124
124
  opacity: z.ZodOptional<z.ZodNumber>;
125
- zIndex: z.ZodOptional<z.ZodNumber>;
125
+ zIndex: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
126
126
  }, z.core.$strip>>;
127
127
  }, z.core.$strip>], null>], z.ZodUnion<readonly [z.ZodTuple<[z.ZodNumber, z.ZodObject<{
128
128
  set: z.ZodOptional<z.ZodObject<{
129
129
  opacity: z.ZodOptional<z.ZodNumber>;
130
- zIndex: z.ZodOptional<z.ZodNumber>;
130
+ zIndex: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
131
131
  }, z.core.$strip>>;
132
132
  }, z.core.$strip>], null>, z.ZodTuple<[z.ZodNumber, z.ZodNull], null>]>>;
133
133
  type: z.ZodLiteral<"image">;
@@ -157,20 +157,20 @@ export declare const MediaSurfaceStateSchema: z.ZodRecord<z.ZodString, z.ZodUnio
157
157
  keyframes: z.ZodTuple<[z.ZodTuple<[z.ZodNumber, z.ZodObject<{
158
158
  set: z.ZodOptional<z.ZodObject<{
159
159
  opacity: z.ZodOptional<z.ZodNumber>;
160
- zIndex: z.ZodOptional<z.ZodNumber>;
160
+ zIndex: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
161
161
  volume: z.ZodOptional<z.ZodNumber>;
162
162
  t: z.ZodOptional<z.ZodNumber>;
163
163
  rate: z.ZodOptional<z.ZodNumber>;
164
164
  }, z.core.$strip>>;
165
165
  lerp: z.ZodOptional<z.ZodObject<{
166
166
  opacity: z.ZodOptional<z.ZodNumber>;
167
- zIndex: z.ZodOptional<z.ZodNumber>;
167
+ zIndex: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
168
168
  volume: z.ZodOptional<z.ZodNumber>;
169
169
  }, z.core.$strip>>;
170
170
  }, z.core.$strip>], null>], z.ZodUnion<readonly [z.ZodTuple<[z.ZodNumber, z.ZodObject<{
171
171
  set: z.ZodOptional<z.ZodObject<{
172
172
  opacity: z.ZodOptional<z.ZodNumber>;
173
- zIndex: z.ZodOptional<z.ZodNumber>;
173
+ zIndex: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
174
174
  volume: z.ZodOptional<z.ZodNumber>;
175
175
  t: z.ZodOptional<z.ZodNumber>;
176
176
  rate: z.ZodOptional<z.ZodNumber>;
@@ -5,7 +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(),
8
+ zIndex: z.number().default(0),
9
9
  });
10
10
  const AudialProperties = z.object({
11
11
  volume: z.number().gte(0).lte(1),