@clockworkdog/cogs-client 3.0.0-alpha.15 → 3.0.0-alpha.17
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/CogsConnection.d.ts +0 -4
- package/dist/CogsConnection.js +0 -10
- package/dist/browser/index.mjs +1753 -2679
- package/dist/browser/index.umd.js +13 -13
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -3
- package/dist/state-based/MediaClipManager.d.ts +3 -4
- package/dist/state-based/MediaClipManager.js +165 -81
- package/dist/state-based/MediaPreloader.d.ts +1 -1
- package/dist/types/MediaSchema.d.ts +6 -2
- package/dist/types/MediaSchema.js +2 -0
- package/dist/utils/device.d.ts +2 -0
- package/dist/utils/device.js +4 -0
- package/dist/utils/getStateAtTime.d.ts +12 -2
- package/dist/utils/getStateAtTime.js +1 -1
- package/dist/utils/modulo.d.ts +6 -0
- package/dist/utils/modulo.js +17 -0
- package/package.json +2 -4
- package/dist/AudioPlayer.d.ts +0 -49
- package/dist/AudioPlayer.js +0 -474
- package/dist/VideoPlayer.d.ts +0 -49
- package/dist/VideoPlayer.js +0 -385
- package/dist/types/AllMediaClipStatesMessage.d.ts +0 -5
- package/dist/types/AllMediaClipStatesMessage.js +0 -1
- package/dist/types/AudioState.d.ts +0 -39
- package/dist/types/AudioState.js +0 -1
- package/dist/types/MediaClipStateMessage.d.ts +0 -7
- package/dist/types/MediaClipStateMessage.js +0 -1
- package/dist/types/VideoState.d.ts +0 -26
- package/dist/types/VideoState.js +0 -5
package/dist/index.d.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
export { default as CogsConnection } from './CogsConnection';
|
|
2
2
|
export * from './CogsConnection';
|
|
3
3
|
export type { default as CogsClientMessage, MediaClientConfig } from './types/CogsClientMessage';
|
|
4
|
-
export type { default as MediaClipStateMessage } from './types/MediaClipStateMessage';
|
|
5
4
|
export type { default as ShowPhase } from './types/ShowPhase';
|
|
6
|
-
export type { default as MediaObjectFit } from './types/MediaObjectFit';
|
|
7
5
|
export * as MediaSchema from './types/MediaSchema';
|
|
8
|
-
export { default as CogsAudioPlayer } from './AudioPlayer';
|
|
9
|
-
export { default as CogsVideoPlayer } from './VideoPlayer';
|
|
10
6
|
export { SurfaceManager } from './state-based/SurfaceManager';
|
|
11
7
|
export { MediaPreloader } from './state-based/MediaPreloader';
|
|
12
|
-
export * from './types/AudioState';
|
|
13
8
|
export { assetUrl, preloadUrl } from './utils/urls';
|
|
14
9
|
export { getStateAtTime } from './utils/getStateAtTime';
|
|
15
10
|
export * from './types/CogsPluginManifest';
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
export { default as CogsConnection } from './CogsConnection';
|
|
2
2
|
export * from './CogsConnection';
|
|
3
3
|
export * as MediaSchema from './types/MediaSchema';
|
|
4
|
-
export { default as CogsAudioPlayer } from './AudioPlayer';
|
|
5
|
-
export { default as CogsVideoPlayer } from './VideoPlayer';
|
|
6
4
|
export { SurfaceManager } from './state-based/SurfaceManager';
|
|
7
5
|
export { MediaPreloader } from './state-based/MediaPreloader';
|
|
8
|
-
export * from './types/AudioState';
|
|
9
6
|
export { assetUrl, preloadUrl } from './utils/urls';
|
|
10
7
|
export { getStateAtTime } from './utils/getStateAtTime';
|
|
11
8
|
export * from './types/CogsPluginManifest';
|
|
@@ -37,14 +37,13 @@ export declare function assertVisualProperties(mediaElement: HTMLMediaElement |
|
|
|
37
37
|
*/
|
|
38
38
|
export declare function assertAudialProperties(mediaElement: HTMLMediaElement, properties: AudialProperties, sinkId: string, surfaceVolume: number): void;
|
|
39
39
|
interface TemporalSyncState {
|
|
40
|
-
state: 'idle' | 'seeking' | 'intercepting';
|
|
40
|
+
state: 'idle' | 'seeking' | 'intercepting' | 'seeking-ahead' | 'seeked-ahead';
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
43
|
* Makes sure the media is at the correct time and speed.
|
|
44
|
-
* -
|
|
45
|
-
* - If we are too far away to smoothly realign, we will seek to the correct time.
|
|
44
|
+
* - Algorithms and constants defined above
|
|
46
45
|
*/
|
|
47
|
-
export declare function assertTemporalProperties(mediaElement: HTMLMediaElement, properties: TemporalProperties, keyframes: VideoState['keyframes'], syncState: TemporalSyncState,
|
|
46
|
+
export declare function assertTemporalProperties(mediaElement: HTMLMediaElement, properties: TemporalProperties, keyframes: VideoState['keyframes'], syncState: TemporalSyncState, enablePlaybackRateAdjustment: boolean): TemporalSyncState;
|
|
48
47
|
export declare class ImageManager extends MediaClipManager<ImageState> {
|
|
49
48
|
private imageElement;
|
|
50
49
|
protected update(): void;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { getStateAtTime } from '../utils/getStateAtTime';
|
|
2
|
+
import { IS_IOS, IS_WEBKIT } from '../utils/device';
|
|
3
|
+
import { modulo, moduloDiff } from '../utils/modulo';
|
|
2
4
|
const getPath = (url) => {
|
|
3
5
|
try {
|
|
4
6
|
const { pathname } = new URL(url, window.location.href);
|
|
@@ -52,7 +54,7 @@ export class MediaClipManager {
|
|
|
52
54
|
clearTimeout(this.timeout);
|
|
53
55
|
if (this.isConnected()) {
|
|
54
56
|
this.update();
|
|
55
|
-
this.timeout = setTimeout(this.loop,
|
|
57
|
+
this.timeout = setTimeout(this.loop, SYNC_INNER_TARGET_THRESHOLD_MS);
|
|
56
58
|
}
|
|
57
59
|
else {
|
|
58
60
|
this.destroy();
|
|
@@ -90,6 +92,10 @@ export function assertElement(mediaElement, parentElement, clip, constructAssetU
|
|
|
90
92
|
if (!element) {
|
|
91
93
|
element = preloader.getElement(clip.file, clip.type);
|
|
92
94
|
}
|
|
95
|
+
// Required for iOS
|
|
96
|
+
if (element instanceof HTMLVideoElement && !element.playsInline) {
|
|
97
|
+
element.playsInline = true;
|
|
98
|
+
}
|
|
93
99
|
break;
|
|
94
100
|
}
|
|
95
101
|
}
|
|
@@ -124,121 +130,209 @@ export function assertVisualProperties(mediaElement, properties, objectFit) {
|
|
|
124
130
|
*/
|
|
125
131
|
export function assertAudialProperties(mediaElement, properties, sinkId, surfaceVolume) {
|
|
126
132
|
const clipVolume = properties.volume * surfaceVolume;
|
|
127
|
-
if (
|
|
128
|
-
|
|
133
|
+
if (IS_IOS) {
|
|
134
|
+
// For iOS devices HTMLMediaElement.volume is readonly
|
|
135
|
+
// The best we can do is mute if the volume should be 0
|
|
136
|
+
if (clipVolume === 0 && !mediaElement.muted) {
|
|
137
|
+
mediaElement.muted = true;
|
|
138
|
+
}
|
|
139
|
+
else if (clipVolume > 0 && mediaElement.muted) {
|
|
140
|
+
mediaElement.muted = false;
|
|
141
|
+
}
|
|
129
142
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
mediaElement.
|
|
133
|
-
/* Do nothing, will be tried in next loop */
|
|
134
|
-
});
|
|
135
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
143
|
+
else {
|
|
144
|
+
if (mediaElement.muted) {
|
|
145
|
+
mediaElement.muted = false;
|
|
136
146
|
}
|
|
137
|
-
|
|
138
|
-
|
|
147
|
+
if (mediaElement.volume !== clipVolume) {
|
|
148
|
+
mediaElement.volume = clipVolume;
|
|
149
|
+
}
|
|
150
|
+
if (mediaElement.sinkId !== sinkId) {
|
|
151
|
+
try {
|
|
152
|
+
mediaElement.setSinkId(sinkId).catch(() => {
|
|
153
|
+
/* Do nothing, will be tried in next loop */
|
|
154
|
+
});
|
|
155
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
156
|
+
}
|
|
157
|
+
catch (_) {
|
|
158
|
+
/* Do nothing, will be tried in next loop */
|
|
159
|
+
}
|
|
139
160
|
}
|
|
140
161
|
}
|
|
141
162
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
163
|
+
/*
|
|
164
|
+
* When playbackRate adjustment is disabled (no-sync) we will attempt to seek-ahead, then wait to play.
|
|
165
|
+
* - This is a recovery situation, so we only do it when 2s out of sync. (outer threshold)
|
|
166
|
+
* - We seek 1s into the future to allow a lot of buffering time (lookahead)
|
|
167
|
+
* - We press play 0.1s early (inner threshold)
|
|
168
|
+
*/
|
|
169
|
+
const NO_SYNC_SEEK_AHEAD_OUTER_THRESHOLD_MS = 2_000;
|
|
170
|
+
const NO_SYNC_SEEK_LOOKAHEAD_MS = 1_000;
|
|
171
|
+
const NO_SYNC_SEEK_AHEAD_INNER_THRESHOLD_MS = 100;
|
|
172
|
+
/**
|
|
173
|
+
* When playbackRate adjustment is enabled (sync) we will attempt to speed ramp to get closer to the correct time.
|
|
174
|
+
* - Whenever we are out of sync by an amount we think we can improve (outer threshold)
|
|
175
|
+
* - We adjust the playbackRate until we are close enough (inner threshold)
|
|
176
|
+
* - If we're too far away that it would take too long to sync (max threshold), then we seek instead.
|
|
177
|
+
* - If we seek ahead we may as well attempt to add a little time for buffering (lookahead)
|
|
178
|
+
*/
|
|
179
|
+
const SYNC_OUTER_TARGET_THRESHOLD_MS = 50;
|
|
180
|
+
const SYNC_INNER_TARGET_THRESHOLD_MS = 5;
|
|
181
|
+
const SYNC_MAX_THRESHOLD_MS = 1_000;
|
|
182
|
+
const SYNC_SEEK_LOOKAHEAD_MS = 10;
|
|
183
|
+
// If the media is scheduled to go back to the start close in time to the end of the video, we'll use the loop attribute.
|
|
147
184
|
const LOOPING_EPSILON_MS = 5;
|
|
148
185
|
const PLAYBACK_ADJUSTMENT_SMOOTHING = 0.3;
|
|
149
186
|
const MAX_PLAYBACK_RATE_ADJUSTMENT = 0.1;
|
|
150
187
|
function playbackSmoothing(deltaTime) {
|
|
151
|
-
return Math.sign(deltaTime) * Math.pow(Math.abs(deltaTime) /
|
|
188
|
+
return Math.sign(deltaTime) * Math.pow(Math.abs(deltaTime) / SYNC_MAX_THRESHOLD_MS, PLAYBACK_ADJUSTMENT_SMOOTHING) * MAX_PLAYBACK_RATE_ADJUSTMENT;
|
|
152
189
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
190
|
+
function assertPlaybackRate(mediaElement, playbackRate) {
|
|
191
|
+
if (mediaElement.playbackRate !== playbackRate) {
|
|
192
|
+
mediaElement.playbackRate = playbackRate;
|
|
193
|
+
}
|
|
194
|
+
// It's more responsive on chromium to set playbackRate to 0 instead of pausing.
|
|
195
|
+
// It also makes it more responsive to start again.
|
|
196
|
+
// On iOS it doesn't make a difference, so we may as well.
|
|
197
|
+
if (mediaElement.paused) {
|
|
160
198
|
mediaElement.play().catch(() => {
|
|
161
|
-
/*
|
|
199
|
+
/* do nothing*/
|
|
162
200
|
});
|
|
163
201
|
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Makes sure the media is at the correct time and speed.
|
|
205
|
+
* - Algorithms and constants defined above
|
|
206
|
+
*/
|
|
207
|
+
export function assertTemporalProperties(mediaElement, properties, keyframes, syncState, enablePlaybackRateAdjustment) {
|
|
208
|
+
// On Webkit (using the simulator on safari and COGS mobile app on iOS) changes to currentTime and playbackRate are much less responsive.
|
|
209
|
+
// We make sure we only do lower frequency updates, and don't change playbackRate.
|
|
210
|
+
const playbackRateSync = enablePlaybackRateAdjustment && !IS_WEBKIT;
|
|
164
211
|
// At the end of the media, is it set back to the start?
|
|
165
212
|
// Sounds like looping to me!
|
|
213
|
+
let isLooping = false;
|
|
166
214
|
if (mediaElement.duration) {
|
|
167
215
|
const nextTemporalKeyframe = keyframes.filter(([t, kf]) => t > properties.t && (kf?.set?.t !== undefined || kf?.set?.rate !== undefined))[0];
|
|
168
216
|
if (nextTemporalKeyframe?.[1]?.set?.t === 0) {
|
|
169
217
|
const timeRemaining = (mediaElement.duration - properties.t) / properties.rate;
|
|
170
218
|
const timeUntilKeyframe = nextTemporalKeyframe[0] - properties.t;
|
|
171
|
-
|
|
219
|
+
isLooping = Math.abs(timeRemaining - timeUntilKeyframe) <= LOOPING_EPSILON_MS;
|
|
172
220
|
if (mediaElement.loop !== isLooping) {
|
|
173
221
|
mediaElement.loop = isLooping;
|
|
174
222
|
}
|
|
175
223
|
}
|
|
176
224
|
}
|
|
177
225
|
const currentTime = mediaElement.currentTime * 1000;
|
|
178
|
-
const deltaTime =
|
|
226
|
+
const deltaTime = isLooping && mediaElement.duration !== undefined
|
|
227
|
+
? moduloDiff(currentTime, properties.t, mediaElement.duration * 1000)
|
|
228
|
+
: currentTime - properties.t;
|
|
179
229
|
const deltaTimeAbs = Math.abs(deltaTime);
|
|
180
230
|
switch (true) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
231
|
+
/**
|
|
232
|
+
* Seek ahead behavior
|
|
233
|
+
* When playbackRate adjustment is not enabled we will seek ahead and try to prepare to play.
|
|
234
|
+
* We'll make sure everything is buffered and ready, then wait until we're on time.
|
|
235
|
+
* We'll try to press play once and leave it to continue.
|
|
236
|
+
*/
|
|
237
|
+
case !playbackRateSync && syncState.state === 'idle' && properties.rate > 0 && deltaTimeAbs > NO_SYNC_SEEK_AHEAD_OUTER_THRESHOLD_MS: {
|
|
238
|
+
const target = (properties.t + properties.rate * NO_SYNC_SEEK_LOOKAHEAD_MS) / 1000;
|
|
239
|
+
if (mediaElement.duration !== undefined && target > mediaElement.duration && !isLooping) {
|
|
240
|
+
// We're not looping, and this is past the end of the video
|
|
241
|
+
return { state: 'idle' };
|
|
187
242
|
}
|
|
243
|
+
assertPlaybackRate(mediaElement, 0);
|
|
244
|
+
mediaElement.currentTime = isLooping ? modulo(target, mediaElement.duration * 1000) : target;
|
|
245
|
+
return { state: 'seeking-ahead' };
|
|
246
|
+
}
|
|
247
|
+
case syncState.state === 'seeking-ahead' && mediaElement.seeking === true:
|
|
248
|
+
return { state: 'seeking-ahead' };
|
|
249
|
+
case syncState.state === 'seeking-ahead' && mediaElement.seeking === false: {
|
|
250
|
+
assertPlaybackRate(mediaElement, 0);
|
|
251
|
+
return { state: 'seeked-ahead' };
|
|
252
|
+
}
|
|
253
|
+
case syncState.state === 'seeked-ahead' && deltaTime < -NO_SYNC_SEEK_AHEAD_INNER_THRESHOLD_MS: {
|
|
254
|
+
assertPlaybackRate(mediaElement, properties.rate);
|
|
255
|
+
console.warn('Failed to seek ahead in time');
|
|
188
256
|
return { state: 'idle' };
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
if (mediaElement.playbackRate !== properties.rate) {
|
|
198
|
-
mediaElement.playbackRate = properties.rate;
|
|
199
|
-
}
|
|
257
|
+
}
|
|
258
|
+
case syncState.state === 'seeked-ahead' && deltaTimeAbs <= NO_SYNC_SEEK_AHEAD_INNER_THRESHOLD_MS: {
|
|
259
|
+
assertPlaybackRate(mediaElement, properties.rate);
|
|
260
|
+
return { state: 'idle' };
|
|
261
|
+
}
|
|
262
|
+
case syncState.state === 'seeked-ahead' && deltaTimeAbs > NO_SYNC_SEEK_AHEAD_OUTER_THRESHOLD_MS * 1.5: {
|
|
263
|
+
// This is an escape mechanism for this behavior. This may happen if the state changes after we've seeked ahead.
|
|
264
|
+
console.warn('Failed to seek ahead');
|
|
200
265
|
return { state: 'idle' };
|
|
201
|
-
|
|
202
|
-
|
|
266
|
+
}
|
|
267
|
+
case syncState.state === 'seeked-ahead':
|
|
268
|
+
return { state: 'seeked-ahead' };
|
|
269
|
+
/**
|
|
270
|
+
* Time synchronization behavior
|
|
271
|
+
* When playbackRate adjustment is enabled we will address small deviations in time by ramping speed up and down.
|
|
272
|
+
* We address larger deviations with a seek, hoping to land close enough so we can finely adjust with playbackRate.
|
|
273
|
+
*/
|
|
274
|
+
// Start intercept
|
|
275
|
+
case playbackRateSync &&
|
|
276
|
+
syncState.state === 'idle' &&
|
|
203
277
|
properties.rate > 0 &&
|
|
204
|
-
deltaTimeAbs >
|
|
205
|
-
deltaTimeAbs <=
|
|
278
|
+
deltaTimeAbs > SYNC_OUTER_TARGET_THRESHOLD_MS &&
|
|
279
|
+
deltaTimeAbs <= SYNC_MAX_THRESHOLD_MS:
|
|
206
280
|
{
|
|
207
|
-
// We are close, we can smoothly adjust with playbackRate:
|
|
208
|
-
// - The video must be playing
|
|
209
|
-
// - We must be close in time to the server time
|
|
210
281
|
const playbackRateAdjustment = playbackSmoothing(deltaTime);
|
|
211
282
|
const adjustedPlaybackRate = Math.max(0, properties.rate - playbackRateAdjustment);
|
|
212
|
-
|
|
213
|
-
mediaElement.playbackRate = adjustedPlaybackRate;
|
|
214
|
-
}
|
|
283
|
+
assertPlaybackRate(mediaElement, adjustedPlaybackRate);
|
|
215
284
|
return { state: 'intercepting' };
|
|
216
285
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
mediaElement.playbackRate = properties.rate;
|
|
221
|
-
}
|
|
286
|
+
// Perfectly intercepted
|
|
287
|
+
case syncState.state === 'intercepting' && deltaTimeAbs <= SYNC_INNER_TARGET_THRESHOLD_MS: {
|
|
288
|
+
assertPlaybackRate(mediaElement, properties.rate);
|
|
222
289
|
return { state: 'idle' };
|
|
223
290
|
}
|
|
291
|
+
// Intercept went too far
|
|
224
292
|
case syncState.state === 'intercepting' && Math.sign(deltaTime) === Math.sign(mediaElement.playbackRate - properties.rate): {
|
|
225
|
-
|
|
293
|
+
assertPlaybackRate(mediaElement, properties.rate);
|
|
226
294
|
return { state: 'idle' };
|
|
227
295
|
}
|
|
228
|
-
|
|
296
|
+
// We're still on course
|
|
297
|
+
case syncState.state === 'intercepting' && deltaTimeAbs < SYNC_MAX_THRESHOLD_MS * 2:
|
|
229
298
|
return { state: 'intercepting' };
|
|
230
|
-
|
|
299
|
+
// We're way off track
|
|
300
|
+
case syncState.state === 'intercepting':
|
|
301
|
+
assertPlaybackRate(mediaElement, properties.rate);
|
|
302
|
+
return { state: 'idle' };
|
|
303
|
+
/**
|
|
304
|
+
* Time synchronization behavior
|
|
305
|
+
* When playbackRate adjustment is enabled we will address small deviations in time by ramping speed up and down.
|
|
306
|
+
* We address larger deviations with a seek, hoping to land close enough so we can finely adjust with playbackRate.
|
|
307
|
+
*/
|
|
308
|
+
case playbackRateSync && syncState.state === 'idle' && deltaTimeAbs > SYNC_MAX_THRESHOLD_MS: {
|
|
309
|
+
const seekTarget = (properties.t + properties.rate * SYNC_SEEK_LOOKAHEAD_MS) / 1000;
|
|
310
|
+
mediaElement.currentTime = isLooping ? modulo(seekTarget, mediaElement.duration * 1000) : seekTarget;
|
|
311
|
+
assertPlaybackRate(mediaElement, properties.rate);
|
|
231
312
|
return { state: 'seeking' };
|
|
232
313
|
}
|
|
233
|
-
|
|
234
|
-
// We cannot smoothly recover:
|
|
235
|
-
// - We seek just ahead of server time
|
|
236
|
-
if (mediaElement.playbackRate !== properties.rate) {
|
|
237
|
-
mediaElement.playbackRate = properties.rate;
|
|
238
|
-
}
|
|
239
|
-
mediaElement.currentTime = (properties.t + properties.rate * SEEK_LOOKAHEAD_MS) / 1000;
|
|
314
|
+
case syncState.state === 'seeking' && mediaElement.seeking: {
|
|
240
315
|
return { state: 'seeking' };
|
|
241
316
|
}
|
|
317
|
+
case syncState.state === 'seeking' && !mediaElement.seeking: {
|
|
318
|
+
return { state: 'idle' };
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Idle behavior
|
|
322
|
+
*/
|
|
323
|
+
case syncState.state === 'idle':
|
|
324
|
+
assertPlaybackRate(mediaElement, properties.rate);
|
|
325
|
+
if (properties.rate === 0 && deltaTimeAbs > SYNC_OUTER_TARGET_THRESHOLD_MS) {
|
|
326
|
+
mediaElement.currentTime = properties.t / 1000;
|
|
327
|
+
}
|
|
328
|
+
return { state: 'idle' };
|
|
329
|
+
/**
|
|
330
|
+
* If none of the above conditions are met, we should exit the behavior.
|
|
331
|
+
* For example: we are intercepting but the media has now been paused
|
|
332
|
+
*/
|
|
333
|
+
default: {
|
|
334
|
+
return { state: 'idle' };
|
|
335
|
+
}
|
|
242
336
|
}
|
|
243
337
|
}
|
|
244
338
|
export class ImageManager extends MediaClipManager {
|
|
@@ -279,12 +373,7 @@ export class AudioManager extends MediaClipManager {
|
|
|
279
373
|
return;
|
|
280
374
|
const sinkId = this.getAudioOutput(this._state.audioOutput);
|
|
281
375
|
assertAudialProperties(this.audioElement, currentState, sinkId, this.volume);
|
|
282
|
-
const nextSyncState = assertTemporalProperties(this.audioElement, currentState, this._state.keyframes, this.syncState, this._state.
|
|
283
|
-
if (this.syncState.state !== 'seeking' && nextSyncState.state === 'seeking') {
|
|
284
|
-
this.audioElement.addEventListener('seeked', () => {
|
|
285
|
-
this.syncState = { state: 'idle' };
|
|
286
|
-
}, { passive: true, once: true });
|
|
287
|
-
}
|
|
376
|
+
const nextSyncState = assertTemporalProperties(this.audioElement, currentState, this._state.keyframes, this.syncState, this._state.enablePlaybackRateAdjustment);
|
|
288
377
|
this.syncState = nextSyncState;
|
|
289
378
|
}
|
|
290
379
|
destroy() {
|
|
@@ -315,12 +404,7 @@ export class VideoManager extends MediaClipManager {
|
|
|
315
404
|
const sinkId = this.getAudioOutput(this._state.audioOutput);
|
|
316
405
|
assertVisualProperties(this.videoElement, currentState, this._state.fit);
|
|
317
406
|
assertAudialProperties(this.videoElement, currentState, sinkId, this.volume);
|
|
318
|
-
const nextSyncState = assertTemporalProperties(this.videoElement, currentState, this._state.keyframes, this.syncState, this._state.
|
|
319
|
-
if (this.syncState.state !== 'seeking' && nextSyncState.state === 'seeking') {
|
|
320
|
-
this.videoElement.addEventListener('seeked', () => {
|
|
321
|
-
this.syncState = { state: 'idle' };
|
|
322
|
-
}, { passive: true, once: true });
|
|
323
|
-
}
|
|
407
|
+
const nextSyncState = assertTemporalProperties(this.videoElement, currentState, this._state.keyframes, this.syncState, this._state.enablePlaybackRateAdjustment);
|
|
324
408
|
this.syncState = nextSyncState;
|
|
325
409
|
}
|
|
326
410
|
destroy() {
|
|
@@ -9,6 +9,6 @@ export declare class MediaPreloader {
|
|
|
9
9
|
};
|
|
10
10
|
setState(newState: MediaClientConfig['files']): void;
|
|
11
11
|
private update;
|
|
12
|
-
getElement(file: string, type: 'audio' | 'video'):
|
|
12
|
+
getElement(file: string, type: 'audio' | 'video'): HTMLMediaElement;
|
|
13
13
|
releaseElement(resource: string | HTMLElement): void;
|
|
14
14
|
}
|
|
@@ -24,12 +24,14 @@ declare const AudioMetadata: z.ZodObject<{
|
|
|
24
24
|
type: z.ZodLiteral<"audio">;
|
|
25
25
|
file: z.ZodString;
|
|
26
26
|
audioOutput: z.ZodString;
|
|
27
|
+
enablePlaybackRateAdjustment: z.ZodBoolean;
|
|
27
28
|
}, z.core.$strip>;
|
|
28
29
|
export type VideoMetadata = z.infer<typeof VideoMetadata>;
|
|
29
30
|
declare const VideoMetadata: z.ZodObject<{
|
|
30
31
|
type: z.ZodLiteral<"video">;
|
|
31
32
|
file: z.ZodString;
|
|
32
33
|
audioOutput: z.ZodString;
|
|
34
|
+
enablePlaybackRateAdjustment: z.ZodBoolean;
|
|
33
35
|
fit: z.ZodUnion<readonly [z.ZodLiteral<"contain">, z.ZodLiteral<"cover">, z.ZodLiteral<"none">]>;
|
|
34
36
|
}, z.core.$strip>;
|
|
35
37
|
export type NullKeyframe = z.infer<typeof NullKeyframe>;
|
|
@@ -153,6 +155,7 @@ export declare const MediaSurfaceStateSchema: z.ZodRecord<z.ZodString, z.ZodUnio
|
|
|
153
155
|
type: z.ZodLiteral<"audio">;
|
|
154
156
|
file: z.ZodString;
|
|
155
157
|
audioOutput: z.ZodString;
|
|
158
|
+
enablePlaybackRateAdjustment: z.ZodBoolean;
|
|
156
159
|
}, z.core.$strip>, z.ZodObject<{
|
|
157
160
|
keyframes: z.ZodTuple<[z.ZodTuple<[z.ZodNumber, z.ZodObject<{
|
|
158
161
|
set: z.ZodOptional<z.ZodObject<{
|
|
@@ -179,6 +182,7 @@ export declare const MediaSurfaceStateSchema: z.ZodRecord<z.ZodString, z.ZodUnio
|
|
|
179
182
|
type: z.ZodLiteral<"video">;
|
|
180
183
|
file: z.ZodString;
|
|
181
184
|
audioOutput: z.ZodString;
|
|
185
|
+
enablePlaybackRateAdjustment: z.ZodBoolean;
|
|
182
186
|
fit: z.ZodUnion<readonly [z.ZodLiteral<"contain">, z.ZodLiteral<"cover">, z.ZodLiteral<"none">]>;
|
|
183
187
|
}, z.core.$strip>]>>;
|
|
184
188
|
export type ImageOptions = VisualProperties;
|
|
@@ -194,7 +198,7 @@ export type AudioState = {
|
|
|
194
198
|
type: 'audio';
|
|
195
199
|
file: string;
|
|
196
200
|
audioOutput: string;
|
|
197
|
-
|
|
201
|
+
enablePlaybackRateAdjustment: boolean;
|
|
198
202
|
keyframes: [InitialAudioKeyframe, ...Array<AudioKeyframe | NullKeyframe>];
|
|
199
203
|
};
|
|
200
204
|
export type VideoState = {
|
|
@@ -202,7 +206,7 @@ export type VideoState = {
|
|
|
202
206
|
file: string;
|
|
203
207
|
fit: 'cover' | 'contain' | 'none';
|
|
204
208
|
audioOutput: string;
|
|
205
|
-
|
|
209
|
+
enablePlaybackRateAdjustment: boolean;
|
|
206
210
|
keyframes: [InitialVideoKeyframe, ...Array<VideoKeyframe | NullKeyframe>];
|
|
207
211
|
};
|
|
208
212
|
export type MediaClipState = ImageState | AudioState | VideoState;
|
|
@@ -19,11 +19,13 @@ const AudioMetadata = z.object({
|
|
|
19
19
|
type: z.literal('audio'),
|
|
20
20
|
file: z.string(),
|
|
21
21
|
audioOutput: z.string(),
|
|
22
|
+
enablePlaybackRateAdjustment: z.boolean(),
|
|
22
23
|
});
|
|
23
24
|
const VideoMetadata = z.object({
|
|
24
25
|
type: z.literal('video'),
|
|
25
26
|
file: z.string(),
|
|
26
27
|
audioOutput: z.string(),
|
|
28
|
+
enablePlaybackRateAdjustment: z.boolean(),
|
|
27
29
|
fit: z.union([z.literal('contain'), z.literal('cover'), z.literal('none')]),
|
|
28
30
|
});
|
|
29
31
|
const NullKeyframe = z.tuple([z.number(), z.null()]);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Check an iOS-only property (See https://developer.mozilla.org/en-US/docs/Web/API/Navigator#non-standard_properties)
|
|
2
|
+
export const IS_IOS = typeof navigator !== 'undefined' && typeof navigator.standalone !== 'undefined';
|
|
3
|
+
// https://evilmartians.com/chronicles/how-to-detect-safari-and-ios-versions-with-ease
|
|
4
|
+
export const IS_WEBKIT = 'GestureEvent' in window;
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export
|
|
1
|
+
import { AudioState, ImageState, TemporalProperties, VideoState } from '../types/MediaSchema';
|
|
2
|
+
export type MinimalClipState = {
|
|
3
|
+
type: 'image';
|
|
4
|
+
keyframes: ImageState['keyframes'];
|
|
5
|
+
} | {
|
|
6
|
+
type: 'audio';
|
|
7
|
+
keyframes: AudioState['keyframes'];
|
|
8
|
+
} | {
|
|
9
|
+
type: 'video';
|
|
10
|
+
keyframes: VideoState['keyframes'];
|
|
11
|
+
};
|
|
12
|
+
export declare function getStateAtTime<State extends MinimalClipState>(state: State, time: number): State['keyframes'][0][1]['set'] | undefined;
|
|
3
13
|
/**
|
|
4
14
|
* Goes through all keyframes to lerp between many different properties
|
|
5
15
|
* Note: This has no specific logic regarding types of properties
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defaultAudioOptions, defaultImageOptions, defaultVideoOptions } from '../types/MediaSchema';
|
|
1
|
+
import { defaultAudioOptions, defaultImageOptions, defaultVideoOptions, } from '../types/MediaSchema';
|
|
2
2
|
export function getStateAtTime(state, time) {
|
|
3
3
|
//If there are any null keyframes the clip has been terminated
|
|
4
4
|
const nullKeyframes = state.keyframes.filter((kf) => kf[1] === null);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Correct modulo operator
|
|
3
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Remainder#description
|
|
4
|
+
*/
|
|
5
|
+
export declare function modulo(n: number, divisor: number): number;
|
|
6
|
+
export declare function moduloDiff(n: number, m: number, divisor: number): number;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Correct modulo operator
|
|
3
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Remainder#description
|
|
4
|
+
*/
|
|
5
|
+
export function modulo(n, divisor) {
|
|
6
|
+
return ((n % divisor) + divisor) % divisor;
|
|
7
|
+
}
|
|
8
|
+
export function moduloDiff(n, m, divisor) {
|
|
9
|
+
n = modulo(n, divisor);
|
|
10
|
+
m = modulo(m, divisor);
|
|
11
|
+
if (Math.abs(n - m) < divisor / 2) {
|
|
12
|
+
return n - m;
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
return n < m ? n + divisor - m : n - (m + divisor);
|
|
16
|
+
}
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Connect to COGS to build a custom Media Master",
|
|
4
4
|
"author": "Clockwork Dog <info@clockwork.dog>",
|
|
5
5
|
"homepage": "https://github.com/clockwork-dog/cogs-sdk/tree/main/packages/javascript",
|
|
6
|
-
"version": "3.0.0-alpha.
|
|
6
|
+
"version": "3.0.0-alpha.17",
|
|
7
7
|
"keywords": [],
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"repository": {
|
|
@@ -37,15 +37,13 @@
|
|
|
37
37
|
"cy:generate": "cypress run --e2e"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@clockworkdog/timesync": "^3.0.0-alpha.
|
|
41
|
-
"howler": "clockwork-dog/howler.js#fix-looping-clips",
|
|
40
|
+
"@clockworkdog/timesync": "^3.0.0-alpha.17",
|
|
42
41
|
"reconnecting-websocket": "^4.4.0",
|
|
43
42
|
"zod": "^4.1.13"
|
|
44
43
|
},
|
|
45
44
|
"devDependencies": {
|
|
46
45
|
"@cypress/mount-utils": "^4.1.2",
|
|
47
46
|
"@eslint/js": "^9.17.0",
|
|
48
|
-
"@types/howler": "2.2.12",
|
|
49
47
|
"@types/jsdom": "^27",
|
|
50
48
|
"@types/node": "^22.10.2",
|
|
51
49
|
"cypress": "^14.5.4",
|
package/dist/AudioPlayer.d.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import CogsConnection from './CogsConnection';
|
|
2
|
-
import { AudioState } from './types/AudioState';
|
|
3
|
-
import MediaClipStateMessage from './types/MediaClipStateMessage';
|
|
4
|
-
type EventTypes = {
|
|
5
|
-
state: AudioState;
|
|
6
|
-
audioClipState: MediaClipStateMessage;
|
|
7
|
-
};
|
|
8
|
-
export default class AudioPlayer {
|
|
9
|
-
private cogsConnection;
|
|
10
|
-
private eventTarget;
|
|
11
|
-
private globalVolume;
|
|
12
|
-
private audioClipPlayers;
|
|
13
|
-
private sinkId;
|
|
14
|
-
constructor(cogsConnection: CogsConnection<any>);
|
|
15
|
-
setGlobalVolume(volume: number): void;
|
|
16
|
-
playAudioClip(path: string, { playId, volume, fade, loop }: {
|
|
17
|
-
playId: string;
|
|
18
|
-
volume: number;
|
|
19
|
-
fade?: number;
|
|
20
|
-
loop: boolean;
|
|
21
|
-
}): void;
|
|
22
|
-
pauseAudioClip(path: string, { fade }: {
|
|
23
|
-
fade?: number;
|
|
24
|
-
}, onlySoundId?: number, allowIfPauseRequested?: boolean): void;
|
|
25
|
-
stopAudioClip(path: string, { fade }: {
|
|
26
|
-
fade?: number;
|
|
27
|
-
}, onlySoundId?: number, allowIfStopRequested?: boolean): void;
|
|
28
|
-
stopAllAudioClips(options: {
|
|
29
|
-
fade?: number;
|
|
30
|
-
}): void;
|
|
31
|
-
setAudioClipVolume(path: string, { volume, fade }: {
|
|
32
|
-
volume: number;
|
|
33
|
-
fade?: number;
|
|
34
|
-
}): void;
|
|
35
|
-
private handleStoppedClip;
|
|
36
|
-
private updateActiveAudioClip;
|
|
37
|
-
private updateAudioClipPlayer;
|
|
38
|
-
setAudioSink(sinkId: string): void;
|
|
39
|
-
private updateConfig;
|
|
40
|
-
private notifyStateListeners;
|
|
41
|
-
private notifyClipStateListeners;
|
|
42
|
-
addEventListener<EventName extends keyof EventTypes>(type: EventName, listener: (ev: CustomEvent<EventTypes[EventName]>) => void, options?: boolean | AddEventListenerOptions): void;
|
|
43
|
-
removeEventListener<EventName extends keyof EventTypes>(type: EventName, listener: (ev: CustomEvent<EventTypes[EventName]>) => void, options?: boolean | EventListenerOptions): void;
|
|
44
|
-
private dispatchEvent;
|
|
45
|
-
private createPlayer;
|
|
46
|
-
private createClip;
|
|
47
|
-
private updatedClip;
|
|
48
|
-
}
|
|
49
|
-
export {};
|