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

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.
Files changed (42) hide show
  1. package/README.md +20 -9
  2. package/dist/CogsConnection.d.ts +0 -4
  3. package/dist/CogsConnection.js +0 -10
  4. package/dist/browser/index.mjs +1871 -2773
  5. package/dist/browser/index.umd.js +13 -13
  6. package/dist/index.d.ts +1 -7
  7. package/dist/index.js +1 -5
  8. package/dist/state-based/MediaClipManager.d.ts +66 -0
  9. package/dist/state-based/MediaClipManager.js +420 -0
  10. package/dist/state-based/MediaPreloader.d.ts +2 -2
  11. package/dist/state-based/MediaPreloader.js +10 -3
  12. package/dist/state-based/SurfaceManager.d.ts +5 -1
  13. package/dist/state-based/SurfaceManager.js +31 -10
  14. package/dist/types/MediaSchema.d.ts +19 -13
  15. package/dist/types/MediaSchema.js +3 -1
  16. package/dist/utils/device.d.ts +2 -0
  17. package/dist/utils/device.js +4 -0
  18. package/dist/utils/getStateAtTime.d.ts +12 -2
  19. package/dist/utils/getStateAtTime.js +6 -1
  20. package/dist/utils/modulo.d.ts +6 -0
  21. package/dist/utils/modulo.js +17 -0
  22. package/package.json +3 -6
  23. package/dist/AudioPlayer.d.ts +0 -49
  24. package/dist/AudioPlayer.js +0 -474
  25. package/dist/VideoPlayer.d.ts +0 -49
  26. package/dist/VideoPlayer.js +0 -385
  27. package/dist/state-based/AudioManager.d.ts +0 -17
  28. package/dist/state-based/AudioManager.js +0 -114
  29. package/dist/state-based/ClipManager.d.ts +0 -23
  30. package/dist/state-based/ClipManager.js +0 -60
  31. package/dist/state-based/ImageManager.d.ts +0 -9
  32. package/dist/state-based/ImageManager.js +0 -54
  33. package/dist/state-based/VideoManager.d.ts +0 -19
  34. package/dist/state-based/VideoManager.js +0 -215
  35. package/dist/types/AllMediaClipStatesMessage.d.ts +0 -5
  36. package/dist/types/AllMediaClipStatesMessage.js +0 -1
  37. package/dist/types/AudioState.d.ts +0 -39
  38. package/dist/types/AudioState.js +0 -1
  39. package/dist/types/MediaClipStateMessage.d.ts +0 -7
  40. package/dist/types/MediaClipStateMessage.js +0 -1
  41. package/dist/types/VideoState.d.ts +0 -26
  42. package/dist/types/VideoState.js +0 -5
@@ -1,215 +0,0 @@
1
- import { defaultVideoOptions } from '../types/MediaSchema';
2
- import { getStateAtTime } from '../utils/getStateAtTime';
3
- import { ClipManager } from './ClipManager';
4
- const DEFAULT_VIDEO_POLLING_MS = 1_000;
5
- const TARGET_SYNC_THRESHOLD_MS = 10; // If we're closer than this we're good enough
6
- const MAX_SYNC_THRESHOLD_MS = 1_000; // If we're further away than this, we'll seek instead
7
- 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
- // We smoothly ramp playbackRate up and down
11
- const PLAYBACK_ADJUSTMENT_SMOOTHING = 0.3;
12
- 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;
30
- }
31
- export class VideoManager extends ClipManager {
32
- mediaPreloader;
33
- videoElement;
34
- // We seek to another part of the video and do nothing until we get there
35
- 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;
41
- this.clipElement = clipElement;
42
- }
43
- updateVideoElement() {
44
- const element = this.mediaPreloader.getElement(this._state.file, 'video');
45
- this.videoElement = element;
46
- this.clipElement.replaceChildren(this.videoElement);
47
- this.videoElement.style.position = 'absolute';
48
- this.videoElement.style.width = '100%';
49
- this.videoElement.style.height = '100%';
50
- }
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
- /**
59
- * Helper function to seek to a specified time.
60
- * Works with the update loop to poll until seeked event has fired.
61
- */
62
- seekTo(time) {
63
- if (!this.videoElement)
64
- return;
65
- this.videoElement.addEventListener('seeked', () => {
66
- console.debug('seeked');
67
- this.isSeeking = false;
68
- }, { once: true, passive: true });
69
- this.videoElement.currentTime = time / 1_000;
70
- }
71
- update() {
72
- // Update loop used to poll until seek finished
73
- if (this.isSeeking)
74
- return;
75
- // Does the <video /> element need adding/removing?
76
- const now = Date.now();
77
- const currentState = getStateAtTime(this._state, now);
78
- if (currentState) {
79
- if (!this.videoElement || !this.isConnected(this.videoElement)) {
80
- this.updateVideoElement();
81
- }
82
- }
83
- else {
84
- this.videoElement?.remove();
85
- this.videoElement = undefined;
86
- }
87
- if (!currentState || !this.videoElement)
88
- return;
89
- const { t, rate, volume } = { ...defaultVideoOptions, ...currentState };
90
- // 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();
94
- }
95
- if (this.videoElement.style.objectFit !== this._state.fit) {
96
- this.videoElement.style.objectFit = this._state.fit;
97
- }
98
- if (parseFloat(this.videoElement.style.opacity) !== currentState.opacity) {
99
- this.videoElement.style.opacity = String(currentState.opacity ?? defaultVideoOptions.opacity);
100
- }
101
- const z = Math.round(currentState.zIndex ?? defaultVideoOptions.zIndex);
102
- if (parseInt(this.videoElement.style.zIndex) !== z) {
103
- this.videoElement.style.zIndex = String(z);
104
- }
105
- if (this.videoElement.volume !== volume) {
106
- this.videoElement.volume = volume;
107
- }
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
131
- 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
- }
137
- }
138
- const currentTime = this.videoElement.currentTime * 1000;
139
- const deltaTime = currentTime - t;
140
- 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
- }
162
- switch (true) {
163
- case deltaTimeAbs <= TARGET_SYNC_THRESHOLD_MS: {
164
- // We are on course:
165
- // - The video is within accepted latency of the server time
166
- // - The playback rate is aligned with the server rate
167
- console.debug(`${rate}x`, deltaTime.toFixed(0));
168
- this.timeToIntercept = undefined;
169
- if (this.videoElement.playbackRate !== rate) {
170
- this.videoElement.playbackRate = rate;
171
- }
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
- break;
181
- case rate > 0 && deltaTimeAbs > TARGET_SYNC_THRESHOLD_MS && deltaTimeAbs <= MAX_SYNC_THRESHOLD_MS && this.timeToIntercept === undefined: {
182
- // We are close, we can smoothly adjust with playbackRate:
183
- // - The video must be playing
184
- // - We must be close in time to the server time
185
- 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`);
189
- if (this.videoElement.playbackRate !== adjustedPlaybackRate) {
190
- this.videoElement.playbackRate = adjustedPlaybackRate;
191
- }
192
- this.delay = this.timeToIntercept * INTERCEPTION_EARLY_CHECK_IN;
193
- break;
194
- }
195
- default: {
196
- // We cannot smoothly recover:
197
- // - We seek just ahead of server time
198
- if (this.videoElement.playbackRate !== rate) {
199
- this.videoElement.playbackRate = rate;
200
- }
201
- // delay to poll until seeked
202
- console.debug('seeking');
203
- this.delay = 10;
204
- this.seekTo(t + rate * SEEK_LOOKAHEAD_MS);
205
- break;
206
- }
207
- }
208
- }
209
- destroy() {
210
- if (this.videoElement) {
211
- this.mediaPreloader.releaseElement(this.videoElement);
212
- this.videoElement.remove();
213
- }
214
- }
215
- }
@@ -1,5 +0,0 @@
1
- export type MediaStatus = 'playing' | 'paused' | 'stopped';
2
- export default interface AllMediaClipStatesMessage {
3
- mediaType: 'audio' | 'video';
4
- files: [string, MediaStatus][];
5
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,39 +0,0 @@
1
- export type ActiveAudioClipState = {
2
- type: 'paused';
3
- } | {
4
- type: 'pause_requested';
5
- fade: number | undefined;
6
- } | {
7
- type: 'pausing';
8
- } | {
9
- type: 'playing';
10
- } | {
11
- type: 'play_requested';
12
- } | {
13
- type: 'stopping';
14
- } | {
15
- type: 'stop_requested';
16
- fade: number | undefined;
17
- };
18
- export interface AudioClip {
19
- config: {
20
- preload: boolean;
21
- ephemeral: boolean;
22
- };
23
- activeClips: {
24
- [soundId: number]: ActiveClip;
25
- };
26
- }
27
- export interface ActiveClip {
28
- state: ActiveAudioClipState;
29
- loop: boolean;
30
- volume: number;
31
- playId: string;
32
- }
33
- export interface AudioState {
34
- isPlaying: boolean;
35
- globalVolume: number;
36
- clips: {
37
- [path: string]: AudioClip;
38
- };
39
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,7 +0,0 @@
1
- export type MediaStatus = 'playing' | 'paused' | 'stopped';
2
- export default interface MediaClipStateMessage {
3
- playId: string;
4
- mediaType: 'audio' | 'video';
5
- file: string;
6
- status: MediaStatus;
7
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,26 +0,0 @@
1
- import { MediaObjectFit } from '..';
2
- export declare enum ActiveVideoClipState {
3
- Paused = "paused",
4
- Playing = "playing"
5
- }
6
- export interface VideoClip {
7
- config: {
8
- preload: 'auto' | 'metadata' | 'none';
9
- ephemeral: boolean;
10
- fit: MediaObjectFit;
11
- };
12
- }
13
- export interface ActiveClip {
14
- path: string;
15
- state: ActiveVideoClipState;
16
- loop: boolean;
17
- volume: number;
18
- }
19
- export interface VideoState {
20
- isPlaying: boolean;
21
- globalVolume: number;
22
- clips: {
23
- [path: string]: VideoClip;
24
- };
25
- activeClip?: ActiveClip;
26
- }
@@ -1,5 +0,0 @@
1
- export var ActiveVideoClipState;
2
- (function (ActiveVideoClipState) {
3
- ActiveVideoClipState["Paused"] = "paused";
4
- ActiveVideoClipState["Playing"] = "playing";
5
- })(ActiveVideoClipState || (ActiveVideoClipState = {}));