@clockworkdog/cogs-client 3.1.4 → 3.2.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.
- package/dist/AudioPlayer.d.ts +49 -0
- package/dist/AudioPlayer.js +484 -0
- package/dist/RtspStreamer.d.ts +27 -0
- package/dist/RtspStreamer.js +101 -0
- package/dist/VideoPlayer.d.ts +49 -0
- package/dist/VideoPlayer.js +394 -0
- package/dist/helpers/urls.d.ts +6 -0
- package/dist/helpers/urls.js +19 -0
- package/dist/types/AllMediaClipStatesMessage.d.ts +5 -0
- package/dist/types/AllMediaClipStatesMessage.js +2 -0
- package/dist/types/AudioState.d.ts +39 -0
- package/dist/types/AudioState.js +2 -0
- package/dist/types/MediaClipStateMessage.d.ts +7 -0
- package/dist/types/MediaClipStateMessage.js +2 -0
- package/dist/types/VideoState.d.ts +26 -0
- package/dist/types/VideoState.js +8 -0
- package/dist/utils/setDate.d.ts +4 -0
- package/dist/utils/setDate.js +26 -0
- package/dist/utils/timesync.d.ts +28 -0
- package/dist/utils/timesync.js +67 -0
- package/package.json +2 -2
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { MediaObjectFit } from '.';
|
|
2
|
+
import CogsConnection from './CogsConnection';
|
|
3
|
+
import MediaClipStateMessage from './types/MediaClipStateMessage';
|
|
4
|
+
import { VideoState } from './types/VideoState';
|
|
5
|
+
type EventTypes = {
|
|
6
|
+
state: VideoState;
|
|
7
|
+
videoClipState: MediaClipStateMessage;
|
|
8
|
+
};
|
|
9
|
+
export default class VideoPlayer {
|
|
10
|
+
private cogsConnection;
|
|
11
|
+
private eventTarget;
|
|
12
|
+
private globalVolume;
|
|
13
|
+
private videoClipPlayers;
|
|
14
|
+
private activeClip?;
|
|
15
|
+
private pendingClip?;
|
|
16
|
+
private parentElement;
|
|
17
|
+
private sinkId;
|
|
18
|
+
constructor(cogsConnection: CogsConnection<any>, parentElement?: HTMLElement);
|
|
19
|
+
setParentElement(parentElement: HTMLElement): void;
|
|
20
|
+
resetParentElement(): void;
|
|
21
|
+
setGlobalVolume(globalVolume: number): void;
|
|
22
|
+
playVideoClip(path: string, { playId, volume, loop, fit }: {
|
|
23
|
+
playId: string;
|
|
24
|
+
volume: number;
|
|
25
|
+
loop: boolean;
|
|
26
|
+
fit: MediaObjectFit;
|
|
27
|
+
}): void;
|
|
28
|
+
pauseVideoClip(): void;
|
|
29
|
+
stopVideoClip(): void;
|
|
30
|
+
setVideoClipVolume({ volume }: {
|
|
31
|
+
volume: number;
|
|
32
|
+
}): void;
|
|
33
|
+
setVideoClipFit({ fit }: {
|
|
34
|
+
fit: MediaObjectFit;
|
|
35
|
+
}): void;
|
|
36
|
+
private handleStoppedClip;
|
|
37
|
+
private updateVideoClipPlayer;
|
|
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 createVideoElement;
|
|
46
|
+
private createClipPlayer;
|
|
47
|
+
private unloadClip;
|
|
48
|
+
}
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const VideoState_1 = require("./types/VideoState");
|
|
4
|
+
const DEFAULT_PARENT_ELEMENT = document.body;
|
|
5
|
+
class VideoPlayer {
|
|
6
|
+
constructor(
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
cogsConnection, parentElement = DEFAULT_PARENT_ELEMENT) {
|
|
9
|
+
this.cogsConnection = cogsConnection;
|
|
10
|
+
this.eventTarget = new EventTarget();
|
|
11
|
+
this.globalVolume = 1;
|
|
12
|
+
this.videoClipPlayers = {};
|
|
13
|
+
this.sinkId = '';
|
|
14
|
+
this.parentElement = parentElement;
|
|
15
|
+
// Send the current status of each clip to COGS
|
|
16
|
+
this.addEventListener('videoClipState', ({ detail }) => {
|
|
17
|
+
cogsConnection.sendMediaClipState(detail);
|
|
18
|
+
});
|
|
19
|
+
// Listen for video control messages
|
|
20
|
+
cogsConnection.addEventListener('message', ({ message }) => {
|
|
21
|
+
switch (message.type) {
|
|
22
|
+
case 'media_config_update':
|
|
23
|
+
this.setGlobalVolume(message.globalVolume);
|
|
24
|
+
if (message.audioOutput !== undefined) {
|
|
25
|
+
const sinkId = cogsConnection.getAudioSinkId(message.audioOutput);
|
|
26
|
+
this.setAudioSink(sinkId !== null && sinkId !== void 0 ? sinkId : '');
|
|
27
|
+
}
|
|
28
|
+
this.updateConfig(message.files);
|
|
29
|
+
break;
|
|
30
|
+
case 'video_play':
|
|
31
|
+
this.playVideoClip(message.file, {
|
|
32
|
+
playId: message.playId,
|
|
33
|
+
volume: message.volume,
|
|
34
|
+
loop: Boolean(message.loop),
|
|
35
|
+
fit: message.fit,
|
|
36
|
+
});
|
|
37
|
+
break;
|
|
38
|
+
case 'video_pause':
|
|
39
|
+
this.pauseVideoClip();
|
|
40
|
+
break;
|
|
41
|
+
case 'video_stop':
|
|
42
|
+
this.stopVideoClip();
|
|
43
|
+
break;
|
|
44
|
+
case 'video_set_volume':
|
|
45
|
+
this.setVideoClipVolume({ volume: message.volume });
|
|
46
|
+
break;
|
|
47
|
+
case 'video_set_fit':
|
|
48
|
+
this.setVideoClipFit({ fit: message.fit });
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
// On connection, send the current playing state of all clips
|
|
53
|
+
// (Usually empty unless websocket is reconnecting)
|
|
54
|
+
const sendInitialClipStates = () => {
|
|
55
|
+
const files = Object.entries(this.videoClipPlayers).map(([file, player]) => {
|
|
56
|
+
const status = !player.videoElement.paused
|
|
57
|
+
? 'playing'
|
|
58
|
+
: player.videoElement.currentTime === 0 || player.videoElement.currentTime === player.videoElement.duration
|
|
59
|
+
? 'paused'
|
|
60
|
+
: 'stopped';
|
|
61
|
+
return [file, status];
|
|
62
|
+
});
|
|
63
|
+
cogsConnection.sendInitialMediaClipStates({ mediaType: 'video', files });
|
|
64
|
+
};
|
|
65
|
+
cogsConnection.addEventListener('open', sendInitialClipStates);
|
|
66
|
+
sendInitialClipStates();
|
|
67
|
+
}
|
|
68
|
+
setParentElement(parentElement) {
|
|
69
|
+
this.parentElement = parentElement;
|
|
70
|
+
Object.values(this.videoClipPlayers).forEach((clipPlayer) => {
|
|
71
|
+
parentElement.appendChild(clipPlayer.videoElement);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
resetParentElement() {
|
|
75
|
+
this.setParentElement(DEFAULT_PARENT_ELEMENT);
|
|
76
|
+
}
|
|
77
|
+
setGlobalVolume(globalVolume) {
|
|
78
|
+
Object.values(this.videoClipPlayers).forEach((clipPlayer) => {
|
|
79
|
+
setVideoElementVolume(clipPlayer.videoElement, clipPlayer.volume * globalVolume);
|
|
80
|
+
});
|
|
81
|
+
this.globalVolume = globalVolume;
|
|
82
|
+
this.notifyStateListeners();
|
|
83
|
+
}
|
|
84
|
+
playVideoClip(path, { playId, volume, loop, fit }) {
|
|
85
|
+
var _a;
|
|
86
|
+
if (!this.videoClipPlayers[path]) {
|
|
87
|
+
this.videoClipPlayers[path] = this.createClipPlayer(path, { preload: 'none', ephemeral: true, fit });
|
|
88
|
+
}
|
|
89
|
+
// Check if there's already a pending clip, which has now been superseded and abort the play operation
|
|
90
|
+
if (this.pendingClip) {
|
|
91
|
+
this.updateVideoClipPlayer(this.pendingClip.path, (clipPlayer) => {
|
|
92
|
+
clipPlayer.videoElement.load(); // Resets the media element
|
|
93
|
+
return clipPlayer;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// New pending clip is video being requested
|
|
97
|
+
if (((_a = this.activeClip) === null || _a === void 0 ? void 0 : _a.path) !== path) {
|
|
98
|
+
this.pendingClip = { path, playId, actionOncePlaying: 'play' };
|
|
99
|
+
}
|
|
100
|
+
// Setup and play the pending clip's player
|
|
101
|
+
this.updateVideoClipPlayer(path, (clipPlayer) => {
|
|
102
|
+
clipPlayer.volume = volume;
|
|
103
|
+
setVideoElementVolume(clipPlayer.videoElement, volume * this.globalVolume);
|
|
104
|
+
clipPlayer.videoElement.loop = loop;
|
|
105
|
+
clipPlayer.videoElement.style.objectFit = fit;
|
|
106
|
+
if (clipPlayer.videoElement.currentTime === clipPlayer.videoElement.duration) {
|
|
107
|
+
clipPlayer.videoElement.currentTime = 0;
|
|
108
|
+
}
|
|
109
|
+
clipPlayer.videoElement.play();
|
|
110
|
+
// Display right away if there's currently no active clip
|
|
111
|
+
if (!this.activeClip) {
|
|
112
|
+
clipPlayer.videoElement.style.display = 'block';
|
|
113
|
+
}
|
|
114
|
+
return clipPlayer;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
pauseVideoClip() {
|
|
118
|
+
// Pending clip should be paused when it loads and becomes active
|
|
119
|
+
if (this.pendingClip) {
|
|
120
|
+
this.pendingClip.actionOncePlaying = 'pause';
|
|
121
|
+
}
|
|
122
|
+
// Pause the currently active clip
|
|
123
|
+
if (this.activeClip) {
|
|
124
|
+
const { playId, path } = this.activeClip;
|
|
125
|
+
this.updateVideoClipPlayer(path, (clipPlayer) => {
|
|
126
|
+
var _a;
|
|
127
|
+
(_a = clipPlayer.videoElement) === null || _a === void 0 ? void 0 : _a.pause();
|
|
128
|
+
return clipPlayer;
|
|
129
|
+
});
|
|
130
|
+
this.notifyClipStateListeners(playId, path, 'paused');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
stopVideoClip() {
|
|
134
|
+
// Pending clip should be stopped when it loads and becomes active
|
|
135
|
+
if (this.pendingClip) {
|
|
136
|
+
this.pendingClip.actionOncePlaying = 'stop';
|
|
137
|
+
}
|
|
138
|
+
// Stop the currently active clip
|
|
139
|
+
if (this.activeClip) {
|
|
140
|
+
this.handleStoppedClip(this.activeClip.path);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
setVideoClipVolume({ volume }) {
|
|
144
|
+
var _a, _b;
|
|
145
|
+
// If there is a pending clip, this is latest to have been played so update its volume
|
|
146
|
+
const clipToUpdate = (_b = (_a = this.pendingClip) !== null && _a !== void 0 ? _a : this.activeClip) !== null && _b !== void 0 ? _b : undefined;
|
|
147
|
+
if (!clipToUpdate) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (!(volume >= 0 && volume <= 1)) {
|
|
151
|
+
console.warn('Invalid volume', volume);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this.updateVideoClipPlayer(clipToUpdate.path, (clipPlayer) => {
|
|
155
|
+
if (clipPlayer.videoElement) {
|
|
156
|
+
clipPlayer.volume = volume;
|
|
157
|
+
setVideoElementVolume(clipPlayer.videoElement, volume * this.globalVolume);
|
|
158
|
+
}
|
|
159
|
+
return clipPlayer;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
setVideoClipFit({ fit }) {
|
|
163
|
+
var _a, _b;
|
|
164
|
+
// If there is a pending clip, this is latest to have been played so update its fit
|
|
165
|
+
const clipToUpdate = (_b = (_a = this.pendingClip) !== null && _a !== void 0 ? _a : this.activeClip) !== null && _b !== void 0 ? _b : undefined;
|
|
166
|
+
if (!clipToUpdate) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
this.updateVideoClipPlayer(clipToUpdate.path, (clipPlayer) => {
|
|
170
|
+
if (clipPlayer.videoElement) {
|
|
171
|
+
clipPlayer.videoElement.style.objectFit = fit;
|
|
172
|
+
}
|
|
173
|
+
return clipPlayer;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
handleStoppedClip(path) {
|
|
177
|
+
var _a;
|
|
178
|
+
if (!this.activeClip || this.activeClip.path !== path) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const playId = this.activeClip.playId;
|
|
182
|
+
// Once an ephemeral clip stops, cleanup and remove the player
|
|
183
|
+
if ((_a = this.videoClipPlayers[this.activeClip.path]) === null || _a === void 0 ? void 0 : _a.config.ephemeral) {
|
|
184
|
+
this.unloadClip(path);
|
|
185
|
+
}
|
|
186
|
+
this.activeClip = undefined;
|
|
187
|
+
this.updateVideoClipPlayer(path, (clipPlayer) => {
|
|
188
|
+
clipPlayer.videoElement.pause();
|
|
189
|
+
clipPlayer.videoElement.currentTime = 0;
|
|
190
|
+
clipPlayer.videoElement.style.display = 'none';
|
|
191
|
+
return clipPlayer;
|
|
192
|
+
});
|
|
193
|
+
this.notifyClipStateListeners(playId, path, 'stopped');
|
|
194
|
+
}
|
|
195
|
+
updateVideoClipPlayer(path, update) {
|
|
196
|
+
if (this.videoClipPlayers[path]) {
|
|
197
|
+
const newPlayer = update(this.videoClipPlayers[path]);
|
|
198
|
+
if (newPlayer) {
|
|
199
|
+
this.videoClipPlayers[path] = newPlayer;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
delete this.videoClipPlayers[path];
|
|
203
|
+
}
|
|
204
|
+
this.notifyStateListeners();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
setAudioSink(sinkId) {
|
|
208
|
+
for (const clipPlayer of Object.values(this.videoClipPlayers)) {
|
|
209
|
+
setPlayerSinkId(clipPlayer, sinkId);
|
|
210
|
+
}
|
|
211
|
+
this.sinkId = sinkId;
|
|
212
|
+
}
|
|
213
|
+
updateConfig(newPaths) {
|
|
214
|
+
const newVideoPaths = Object.fromEntries(Object.entries(newPaths).filter(([, { type }]) => type === 'video' || !type));
|
|
215
|
+
const previousClipPlayers = this.videoClipPlayers;
|
|
216
|
+
this.videoClipPlayers = (() => {
|
|
217
|
+
const clipPlayers = { ...previousClipPlayers };
|
|
218
|
+
const removedClips = Object.keys(previousClipPlayers).filter((previousPath) => !(previousPath in newVideoPaths));
|
|
219
|
+
removedClips.forEach((path) => {
|
|
220
|
+
var _a, _b;
|
|
221
|
+
if (((_a = this.activeClip) === null || _a === void 0 ? void 0 : _a.path) === path && ((_b = previousClipPlayers[path]) === null || _b === void 0 ? void 0 : _b.config.ephemeral) === false) {
|
|
222
|
+
this.updateVideoClipPlayer(path, (player) => {
|
|
223
|
+
player.config = { ...player.config, ephemeral: true };
|
|
224
|
+
return player;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
this.unloadClip(path);
|
|
229
|
+
delete clipPlayers[path];
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
const addedClips = Object.entries(newVideoPaths).filter(([newFile]) => !previousClipPlayers[newFile]);
|
|
233
|
+
addedClips.forEach(([path, config]) => {
|
|
234
|
+
clipPlayers[path] = this.createClipPlayer(path, { ...config, preload: preloadString(config.preload), ephemeral: false, fit: 'contain' });
|
|
235
|
+
});
|
|
236
|
+
const updatedClips = Object.entries(previousClipPlayers).filter(([previousPath]) => previousPath in newVideoPaths);
|
|
237
|
+
updatedClips.forEach(([path, previousClipPlayer]) => {
|
|
238
|
+
if (previousClipPlayer.config.preload !== newVideoPaths[path].preload) {
|
|
239
|
+
this.updateVideoClipPlayer(path, (player) => {
|
|
240
|
+
player.config = {
|
|
241
|
+
...player.config,
|
|
242
|
+
preload: preloadString(newVideoPaths[path].preload),
|
|
243
|
+
ephemeral: false,
|
|
244
|
+
};
|
|
245
|
+
player.videoElement.preload = player.config.preload;
|
|
246
|
+
return player;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
return clipPlayers;
|
|
251
|
+
})();
|
|
252
|
+
this.notifyStateListeners();
|
|
253
|
+
}
|
|
254
|
+
notifyStateListeners() {
|
|
255
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
256
|
+
const VideoState = {
|
|
257
|
+
globalVolume: this.globalVolume,
|
|
258
|
+
isPlaying: this.activeClip ? !((_a = this.videoClipPlayers[this.activeClip.path].videoElement) === null || _a === void 0 ? void 0 : _a.paused) : false,
|
|
259
|
+
clips: { ...this.videoClipPlayers },
|
|
260
|
+
activeClip: this.activeClip
|
|
261
|
+
? {
|
|
262
|
+
path: this.activeClip.path,
|
|
263
|
+
state: !((_b = this.videoClipPlayers[this.activeClip.path].videoElement) === null || _b === void 0 ? void 0 : _b.paused) ? VideoState_1.ActiveVideoClipState.Playing : VideoState_1.ActiveVideoClipState.Paused,
|
|
264
|
+
loop: (_d = (_c = this.videoClipPlayers[this.activeClip.path].videoElement) === null || _c === void 0 ? void 0 : _c.loop) !== null && _d !== void 0 ? _d : false,
|
|
265
|
+
volume: ((_e = this.videoClipPlayers[this.activeClip.path].videoElement) === null || _e === void 0 ? void 0 : _e.muted)
|
|
266
|
+
? 0
|
|
267
|
+
: ((_g = (_f = this.videoClipPlayers[this.activeClip.path].videoElement) === null || _f === void 0 ? void 0 : _f.volume) !== null && _g !== void 0 ? _g : 0),
|
|
268
|
+
}
|
|
269
|
+
: undefined,
|
|
270
|
+
};
|
|
271
|
+
this.dispatchEvent('state', VideoState);
|
|
272
|
+
}
|
|
273
|
+
notifyClipStateListeners(playId, file, status) {
|
|
274
|
+
this.dispatchEvent('videoClipState', { playId, mediaType: 'video', file, status });
|
|
275
|
+
}
|
|
276
|
+
// Type-safe wrapper around EventTarget
|
|
277
|
+
addEventListener(type, listener, options) {
|
|
278
|
+
this.eventTarget.addEventListener(type, listener, options);
|
|
279
|
+
}
|
|
280
|
+
removeEventListener(type, listener, options) {
|
|
281
|
+
this.eventTarget.removeEventListener(type, listener, options);
|
|
282
|
+
}
|
|
283
|
+
dispatchEvent(type, detail) {
|
|
284
|
+
this.eventTarget.dispatchEvent(new CustomEvent(type, { detail }));
|
|
285
|
+
}
|
|
286
|
+
createVideoElement(path, config, { volume }) {
|
|
287
|
+
const videoElement = document.createElement('video');
|
|
288
|
+
videoElement.playsInline = true; // Required for iOS
|
|
289
|
+
videoElement.src = this.cogsConnection.getAssetUrl(path);
|
|
290
|
+
videoElement.autoplay = false;
|
|
291
|
+
videoElement.loop = false;
|
|
292
|
+
setVideoElementVolume(videoElement, volume * this.globalVolume);
|
|
293
|
+
videoElement.preload = config.preload;
|
|
294
|
+
videoElement.addEventListener('playing', () => {
|
|
295
|
+
var _a, _b;
|
|
296
|
+
// If the clip is still the pending one when it actually start playing, then ensure it is in the correct state
|
|
297
|
+
if (((_a = this.pendingClip) === null || _a === void 0 ? void 0 : _a.path) === path) {
|
|
298
|
+
switch (this.pendingClip.actionOncePlaying) {
|
|
299
|
+
case 'play': {
|
|
300
|
+
// Continue playing, show the video element, and notify listeners
|
|
301
|
+
videoElement.style.display = 'block';
|
|
302
|
+
this.notifyClipStateListeners(this.pendingClip.playId, path, 'playing');
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
case 'pause': {
|
|
306
|
+
// Pause playback, show the video element, and notify listeners
|
|
307
|
+
videoElement.style.display = 'block';
|
|
308
|
+
videoElement.pause();
|
|
309
|
+
this.notifyClipStateListeners(this.pendingClip.playId, path, 'paused');
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
case 'stop': {
|
|
313
|
+
// Pause playback, leave the video element hidden, and notify listeners
|
|
314
|
+
videoElement.pause();
|
|
315
|
+
this.notifyClipStateListeners(this.pendingClip.playId, path, 'stopped');
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// If there was a previously active clip, then stop it
|
|
320
|
+
if (this.activeClip) {
|
|
321
|
+
this.handleStoppedClip(this.activeClip.path);
|
|
322
|
+
}
|
|
323
|
+
this.activeClip = this.pendingClip;
|
|
324
|
+
this.pendingClip = undefined;
|
|
325
|
+
}
|
|
326
|
+
else if (((_b = this.activeClip) === null || _b === void 0 ? void 0 : _b.path) === path) {
|
|
327
|
+
// If we were the active clip then just notify listeners that we are now playing
|
|
328
|
+
this.notifyClipStateListeners(this.activeClip.playId, path, 'playing');
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
// Otherwise it shouldn't be playing, like because another clip became pending before we loaded,
|
|
332
|
+
// so we pause and don't show or notify listeners
|
|
333
|
+
videoElement.pause();
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
videoElement.addEventListener('ended', () => {
|
|
337
|
+
// Ignore if there's a pending clip, as once that starts playing the active clip will be stopped
|
|
338
|
+
// Also ignore if the video is set to loop
|
|
339
|
+
if (!this.pendingClip && !videoElement.loop) {
|
|
340
|
+
this.handleStoppedClip(path);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
videoElement.style.position = 'absolute';
|
|
344
|
+
videoElement.style.top = '0';
|
|
345
|
+
videoElement.style.left = '0';
|
|
346
|
+
videoElement.style.width = '100%';
|
|
347
|
+
videoElement.style.height = '100%';
|
|
348
|
+
videoElement.style.objectFit = config.fit;
|
|
349
|
+
videoElement.style.display = 'none';
|
|
350
|
+
this.parentElement.appendChild(videoElement);
|
|
351
|
+
return videoElement;
|
|
352
|
+
}
|
|
353
|
+
createClipPlayer(path, config) {
|
|
354
|
+
const volume = 1;
|
|
355
|
+
const player = {
|
|
356
|
+
config,
|
|
357
|
+
videoElement: this.createVideoElement(path, config, { volume }),
|
|
358
|
+
volume,
|
|
359
|
+
};
|
|
360
|
+
setPlayerSinkId(player, this.sinkId);
|
|
361
|
+
return player;
|
|
362
|
+
}
|
|
363
|
+
unloadClip(path) {
|
|
364
|
+
var _a, _b;
|
|
365
|
+
if (((_a = this.activeClip) === null || _a === void 0 ? void 0 : _a.path) === path) {
|
|
366
|
+
const playId = this.activeClip.playId;
|
|
367
|
+
this.activeClip = undefined;
|
|
368
|
+
this.notifyClipStateListeners(playId, path, 'stopped');
|
|
369
|
+
}
|
|
370
|
+
(_b = this.videoClipPlayers[path]) === null || _b === void 0 ? void 0 : _b.videoElement.remove();
|
|
371
|
+
this.updateVideoClipPlayer(path, () => null);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
exports.default = VideoPlayer;
|
|
375
|
+
function preloadString(preload) {
|
|
376
|
+
return typeof preload === 'string' ? preload : preload ? 'auto' : 'none';
|
|
377
|
+
}
|
|
378
|
+
function setPlayerSinkId(player, sinkId) {
|
|
379
|
+
if (sinkId === undefined) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (typeof player.videoElement.setSinkId === 'function') {
|
|
383
|
+
player.videoElement.setSinkId(sinkId);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Set video volume
|
|
388
|
+
*
|
|
389
|
+
* This doesn't work on iOS (volume is read-only) so at least mute it if the volume is zero
|
|
390
|
+
*/
|
|
391
|
+
function setVideoElementVolume(videoElement, volume) {
|
|
392
|
+
videoElement.volume = volume;
|
|
393
|
+
videoElement.muted = volume === 0;
|
|
394
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.COGS_SERVER_PORT = void 0;
|
|
4
|
+
exports.assetUrl = assetUrl;
|
|
5
|
+
exports.preloadUrl = preloadUrl;
|
|
6
|
+
exports.COGS_SERVER_PORT = 12095;
|
|
7
|
+
/**
|
|
8
|
+
* Get the URL of an asset hosted by the COGS server.
|
|
9
|
+
*/
|
|
10
|
+
function assetUrl(file) {
|
|
11
|
+
const location = typeof window !== 'undefined' ? window.location : undefined;
|
|
12
|
+
const path = `/assets/${encodeURIComponent(file)}`;
|
|
13
|
+
return `${location === null || location === void 0 ? void 0 : location.protocol}//${location === null || location === void 0 ? void 0 : location.hostname}:${exports.COGS_SERVER_PORT}${path}`;
|
|
14
|
+
}
|
|
15
|
+
async function preloadUrl(url) {
|
|
16
|
+
const response = await fetch(url);
|
|
17
|
+
// We used arrayBuffer()` instead of `blob()` because the latter seems to fail on Pis when preloading some files
|
|
18
|
+
return URL.createObjectURL(new Blob([await response.arrayBuffer()]));
|
|
19
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ActiveVideoClipState = void 0;
|
|
4
|
+
var ActiveVideoClipState;
|
|
5
|
+
(function (ActiveVideoClipState) {
|
|
6
|
+
ActiveVideoClipState["Paused"] = "paused";
|
|
7
|
+
ActiveVideoClipState["Playing"] = "playing";
|
|
8
|
+
})(ActiveVideoClipState || (exports.ActiveVideoClipState = ActiveVideoClipState = {}));
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Adapted from https://stackoverflow.com/a/58325977/244640
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.setDate = setDate;
|
|
5
|
+
const OriginalDateConstructor = globalThis.Date;
|
|
6
|
+
/**
|
|
7
|
+
* Patch `Date.now()` and `new Date()` given the current time is @param now
|
|
8
|
+
*/
|
|
9
|
+
function setDate(now) {
|
|
10
|
+
const nowDelta = now - OriginalDateConstructor.now();
|
|
11
|
+
function Date(...args) {
|
|
12
|
+
if (args.length === 0) {
|
|
13
|
+
return new OriginalDateConstructor(Date.now()); // Date.now() is implemented below
|
|
14
|
+
}
|
|
15
|
+
// Specific date constructor
|
|
16
|
+
return new OriginalDateConstructor(...args);
|
|
17
|
+
}
|
|
18
|
+
// copy all properties from the original date, this includes the prototype
|
|
19
|
+
const propertyDescriptors = Object.getOwnPropertyDescriptors(OriginalDateConstructor);
|
|
20
|
+
Object.defineProperties(Date, propertyDescriptors);
|
|
21
|
+
// override Date.now to return the adjusted time
|
|
22
|
+
Date.now = function () {
|
|
23
|
+
return OriginalDateConstructor.now() + nowDelta;
|
|
24
|
+
};
|
|
25
|
+
globalThis.Date = Date;
|
|
26
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface TimeSyncRequestData {
|
|
2
|
+
timesync: {
|
|
3
|
+
id: number;
|
|
4
|
+
};
|
|
5
|
+
}
|
|
6
|
+
export interface TimeSyncResponseData {
|
|
7
|
+
timesync: {
|
|
8
|
+
id: number;
|
|
9
|
+
now: number;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export interface TimeSyncClient {
|
|
13
|
+
receive(data: TimeSyncResponseData): void;
|
|
14
|
+
destroy(): void;
|
|
15
|
+
}
|
|
16
|
+
export interface TimeSyncServer {
|
|
17
|
+
receive(data: TimeSyncRequestData): void;
|
|
18
|
+
}
|
|
19
|
+
export declare function createTimeSyncClient({ interval, send, onChange, syncSampleSize, syncRequestTimeout, }: {
|
|
20
|
+
interval: number;
|
|
21
|
+
send: (data: TimeSyncRequestData) => void | Promise<void>;
|
|
22
|
+
onChange?: (now: number) => void;
|
|
23
|
+
syncSampleSize?: number;
|
|
24
|
+
syncRequestTimeout?: number;
|
|
25
|
+
}): TimeSyncClient;
|
|
26
|
+
export declare function createTimeSyncServer({ send }: {
|
|
27
|
+
send: (data: TimeSyncResponseData) => void | Promise<void>;
|
|
28
|
+
}): TimeSyncServer;
|