@eluvio/elv-client-js 4.2.14 → 4.2.15

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.
@@ -0,0 +1,431 @@
1
+ // copied from
2
+ // https://github.com/eluv-io/elv-video-editor/blob/4408dd5a98536a13d16f80d2bca943077289765c/src/utils/FrameAccurateVideo.js
3
+ // (no changes made)
4
+
5
+ // import Fraction from "fraction.js";
6
+ const Fraction = require("fraction.js");
7
+
8
+ const FrameRateNumerator = {
9
+ NTSC: 30000,
10
+ NTSCFilm: 24000,
11
+ NTSCHD: 60000,
12
+ PAL: 25,
13
+ PALHD: 50,
14
+ Film: 24,
15
+ Web: 30,
16
+ High: 60
17
+ };
18
+
19
+ const FrameRateDenominator = {
20
+ NTSC: 1001,
21
+ NTSCFilm: 1001,
22
+ NTSCHD: 1001,
23
+ PAL: 1,
24
+ PALHD: 1,
25
+ Film: 1,
26
+ Web: 1,
27
+ High: 1
28
+ };
29
+
30
+ const FrameRates = {
31
+ NTSC: Fraction(FrameRateNumerator["NTSC"]).div(FrameRateDenominator["NTSC"]),
32
+ NTSCFilm: Fraction(FrameRateNumerator["NTSCFilm"]).div(FrameRateDenominator["NTSC"]),
33
+ NTSCHD: Fraction(FrameRateNumerator["NTSCHD"]).div(FrameRateDenominator["NTSCHD"]),
34
+ PAL: Fraction(25),
35
+ PALHD: Fraction(50),
36
+ Film: Fraction(24),
37
+ Web: Fraction(30),
38
+ High: Fraction(60)
39
+ };
40
+
41
+ /**
42
+ * FrameAccurateVideo constructor.
43
+ *
44
+ * Initializes a video wrapper that supports frame-accurate timing based on a
45
+ * specified frame rate. Handles drop-frame logic automatically for NTSC-based
46
+ * frame rates and optionally registers a time-update callback on the video.
47
+ *
48
+ * @param {Object} params
49
+ * @param {HTMLVideoElement} params.video - Video element to track.
50
+ * @param {number} params.frameRate - Nominal frame rate (e.g. 29.97, 30).
51
+ * @param {Object} params.frameRateRat - Rational frame rate representation.
52
+ * @param {boolean} [params.dropFrame=false] - Enable drop-frame timecode (only valid for NTSC rates).
53
+ * @param {Function} params.callback - Callback invoked on frame/time updates.
54
+ */
55
+ class FrameAccurateVideo {
56
+ constructor({video, frameRate, frameRateRat, dropFrame=false, callback}) {
57
+ this.video = video;
58
+ this.SetFrameRate({rate: frameRate, rateRat: frameRateRat});
59
+ this.callback = callback;
60
+
61
+ // Only set drop frame if appropriate based on frame rate
62
+ const frameRateKey = FrameAccurateVideo.FractionToRateKey(`${this.frameRateNumerator}/${this.frameRateDenominator}`);
63
+ this.dropFrame = dropFrame && ["NTSC", "NTSCFilm", "NTSCHD"].includes(frameRateKey);
64
+
65
+ if(this.video && callback) {
66
+ this.RegisterCallback();
67
+ }
68
+
69
+ this.TimeToString = FrameAccurateVideo.TimeToString.bind(this);
70
+ this.Update = this.Update.bind(this);
71
+ }
72
+
73
+ static FractionToRateKey(input) {
74
+ let rate = input;
75
+ if(typeof input === "string") {
76
+ if(input.includes("/")) {
77
+ rate = input.split("/");
78
+ rate = rate[0] / rate[1];
79
+ } else {
80
+ rate = parseFloat(input);
81
+ }
82
+ }
83
+
84
+ switch(rate) {
85
+ case 24:
86
+ return "Film";
87
+ case 25:
88
+ return "PAL";
89
+ case 30:
90
+ return "Web";
91
+ case 50:
92
+ return "PALHD";
93
+ case 60:
94
+ return "High";
95
+ default:
96
+ if(Math.abs(24 - rate) < 0.1) {
97
+ return "NTSCFilm";
98
+ } else if(Math.abs(30 - rate) < 0.1) {
99
+ return "NTSC";
100
+ } else if(Math.abs(60 - rate) < 0.1) {
101
+ return "NTSCHD";
102
+ }
103
+
104
+ console.error(`Unknown playback rate: ${input}`);
105
+
106
+ FrameRateNumerator.Unknown = parseInt(input.split("/")[0]);
107
+ FrameRateDenominator.Unknown = parseInt(input.split("/")[1]);
108
+ FrameRates.Unknown = rate;
109
+ return "Unknown";
110
+ }
111
+ }
112
+
113
+ static TimeToString({time, includeFractionalSeconds, format="description"}) {
114
+ let seconds = Fraction(time);
115
+ const hours = seconds.div(3600).floor().valueOf();
116
+ const minutes = seconds.div(60).mod(60).floor(0).valueOf();
117
+ seconds = seconds.mod(60);
118
+
119
+ let string = "";
120
+ if(format === "smpte") {
121
+ string = `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
122
+ string += `:${seconds.floor().valueOf().toString().padStart(2, "0")}`;
123
+
124
+ if(includeFractionalSeconds && seconds.floor().valueOf() !== seconds.valueOf()) {
125
+ string += `.${parseFloat(seconds.valueOf().toFixed(4).toString().split(".")[1])}`;
126
+ }
127
+
128
+ return string;
129
+ }
130
+
131
+ if(hours > 0) {
132
+ string += `${hours}h `;
133
+ }
134
+
135
+ if(minutes > 0) {
136
+ string += `${minutes}m `;
137
+ }
138
+
139
+ if(!string || seconds > 0) {
140
+ if(includeFractionalSeconds) {
141
+ string += `${parseFloat(seconds.valueOf().toFixed(4))}s`;
142
+ } else {
143
+ string += `${seconds.floor().valueOf()}s`;
144
+ }
145
+ }
146
+
147
+ return string.trim();
148
+ }
149
+
150
+ /* Conversion utility methods */
151
+
152
+ static ParseRat(str) {
153
+ if(str.includes("/")) {
154
+ const num = parseInt(str.split("/")[0]);
155
+ const denom = parseInt(str.split("/")[1]);
156
+
157
+ return Number((num / denom).toFixed(3));
158
+ } else {
159
+ return parseInt(str);
160
+ }
161
+ }
162
+
163
+ SetFrameRate({rate, rateRat}) {
164
+ let num, denom;
165
+ if(rateRat) {
166
+ if(rateRat.includes("/")) {
167
+ num = parseInt(rateRat.split("/")[0]);
168
+ denom = parseInt(rateRat.split("/")[1]);
169
+ } else {
170
+ num = parseInt(rateRat);
171
+ denom = 1;
172
+ }
173
+
174
+ rate = Fraction(num).div(denom);
175
+ } else {
176
+ const rateKey = FrameAccurateVideo.FractionToRateKey(this.frameRate);
177
+ num = FrameRateNumerator[rateKey];
178
+ denom = FrameRateDenominator[rateKey];
179
+ }
180
+
181
+ this.frameRate = Fraction(rate);
182
+ this.frameRateNumerator = num;
183
+ this.frameRateDenominator = denom;
184
+ }
185
+
186
+ RatToFrame(rat) {
187
+ const [numerator, denominator] = rat.split("/");
188
+ const time = denominator ? Fraction(numerator).div(denominator) : numerator;
189
+
190
+ return this.TimeToFrame(time);
191
+ }
192
+
193
+ FrameToRat(frame) {
194
+ return `${frame * this.frameRateDenominator}/${this.frameRateNumerator}`;
195
+ }
196
+
197
+ FrameToTime(frame) {
198
+ if(frame === 0) { return 0; }
199
+
200
+ return Fraction(frame).div(this.frameRate).valueOf();
201
+ }
202
+
203
+ FrameToSMPTE(frame) {
204
+ return this.SMPTE(frame);
205
+ }
206
+
207
+ ProgressToTime(progress, duration=0) {
208
+ return Fraction(progress).mul(duration).valueOf();
209
+ }
210
+
211
+ ProgressToSMPTE(progress, duration=0) {
212
+ return this.TimeToSMPTE(Fraction(progress).mul(duration));
213
+ }
214
+
215
+ TimeToFrame(time, round=false) {
216
+ const frame = Fraction(time || 0).mul(this.frameRate);
217
+
218
+ if(round) {
219
+ return frame.round().valueOf();
220
+ } else {
221
+ return frame.floor().valueOf();
222
+ }
223
+ }
224
+
225
+ TimeToSMPTE(time) {
226
+ return this.SMPTE(this.TimeToFrame(time));
227
+ }
228
+
229
+ SMPTEToFrame(smpte) {
230
+ const components = smpte.split(/[:;]/).reverse();
231
+
232
+ const frames = Fraction(components[0]);
233
+ const seconds = Fraction(components[1]);
234
+ const minutes = Fraction(components[2]);
235
+ const hours = Fraction(components[3] || 0);
236
+
237
+ let skippedFrames = 0;
238
+ if(this.dropFrame) {
239
+ const skippedFramesPerMinute = this.frameRate.equals(FrameRates.NTSCHD) ? Fraction(4) : Fraction(2);
240
+ const totalMinutes = minutes.add(hours.mul(60));
241
+ const tenMinutes = totalMinutes.div(10).floor();
242
+ skippedFrames = totalMinutes.mul(skippedFramesPerMinute)
243
+ .sub(tenMinutes.mul(skippedFramesPerMinute));
244
+ }
245
+
246
+ return frames
247
+ .add(seconds.mul(this.frameRate.round()))
248
+ .add(minutes.mul(this.frameRate.round()).mul(60))
249
+ .add(hours.mul(this.frameRate.round()).mul(60).mul(60))
250
+ .sub(skippedFrames)
251
+ .valueOf();
252
+ }
253
+
254
+ SMPTEToTime(smpte) {
255
+ const frame = this.SMPTEToFrame(smpte);
256
+
257
+ return Fraction(frame).div(this.frameRate).valueOf();
258
+ }
259
+
260
+ /* Time Calculations */
261
+
262
+ Frame() {
263
+ return this.TimeToFrame(this.video?.currentTime || 0);
264
+ }
265
+
266
+ Pad(fraction) {
267
+ fraction = fraction.valueOf();
268
+ return fraction < 10 ? `0${fraction}` : fraction;
269
+ }
270
+
271
+ SMPTE(f) {
272
+ let frame = (typeof f !== "undefined" ? Fraction(f) : Fraction(this.Frame())).floor();
273
+ const frameRate = this.frameRate.round();
274
+
275
+ if(this.dropFrame) {
276
+ const framesPerMinute = this.frameRate.equals(FrameRates.NTSCHD) ? Fraction(4) : Fraction(2);
277
+ const tenMinutes = Fraction("17982").mul(framesPerMinute).div(2);
278
+ const oneMinute = Fraction("1798").mul(framesPerMinute).div(2);
279
+
280
+ const tenMinuteIntervals = frame.div(tenMinutes).floor();
281
+ let framesSinceLastInterval = frame.mod(tenMinutes);
282
+
283
+ // If framesSinceLastInterval < framesPerMinute
284
+ if(framesSinceLastInterval.compare(framesPerMinute) < 0) {
285
+ // This is where the jump from :59:29 -> :00:02 or :59:59 -> :00:04 happens
286
+ framesSinceLastInterval = framesSinceLastInterval.add(framesPerMinute);
287
+ }
288
+
289
+ frame = frame.add(
290
+ framesPerMinute.mul(tenMinuteIntervals).mul("9").add(
291
+ framesPerMinute.mul((framesSinceLastInterval.sub(framesPerMinute)).div(oneMinute).floor())
292
+ )
293
+ );
294
+ }
295
+
296
+ const hours = frame.div(frameRate.mul(3600)).mod(24).floor();
297
+ const minutes = frame.div(frameRate.mul(60)).mod(60).floor();
298
+ const seconds = frame.div(frameRate).mod(60).floor();
299
+ const frames = frame.mod(frameRate).floor();
300
+
301
+ const lastColon = this.dropFrame ? ";" : ":";
302
+
303
+ return `${this.Pad(hours)}:${this.Pad(minutes)}:${this.Pad(seconds)}${lastColon}${this.Pad(frames)}`;
304
+ }
305
+
306
+ TotalFrames(duration=0) {
307
+ return Math.floor(Fraction(duration).mul(this.frameRate).valueOf());
308
+ }
309
+
310
+ FrameToString({frame, includeFractionalSeconds}) {
311
+ let [hours, minutes, seconds, frames] = this.SMPTE(frame).split(":").map(e => parseInt(e));
312
+
313
+ if(includeFractionalSeconds) {
314
+ seconds = Fraction(frames).div(this.frameRate).add(seconds).round(4);
315
+ }
316
+
317
+ let string = "";
318
+ if(hours > 0) {
319
+ string += `${hours}h `;
320
+ }
321
+
322
+ if(minutes > 0) {
323
+ string += `${minutes}m `;
324
+ }
325
+
326
+ if(seconds > 0 || !string) {
327
+ string += `${seconds}s`;
328
+ }
329
+
330
+ return string.trim();
331
+ }
332
+
333
+ /* Controls */
334
+
335
+ SeekForward(frames=1) {
336
+ const frame = this.Frame();
337
+ this.Seek(frame.add(frames));
338
+ }
339
+
340
+ SeekBackward(frames=1) {
341
+ const frame = Fraction(this.Frame());
342
+ this.Seek(frame.sub(frames));
343
+ }
344
+
345
+ SeekPercentage(percent, duration) {
346
+ this.Seek(Fraction(this.TotalFrames(duration)).mul(percent));
347
+ }
348
+
349
+ Seek(frame) {
350
+ if(!this.video) { return; }
351
+
352
+ // Whenever seeking, stop comfortably in the middle of a frame
353
+ frame = Fraction(frame).floor().add(0.5);
354
+
355
+ this.video.currentTime = frame.div(this.frameRate).valueOf().toFixed(3);
356
+ }
357
+
358
+ /* Callbacks */
359
+
360
+ Update() {
361
+ if(this.callback) {
362
+ this.callback({
363
+ frame: this.Frame().valueOf(),
364
+ smpte: this.SMPTE()
365
+ });
366
+ }
367
+ }
368
+
369
+ RegisterCallback() {
370
+ if(!this.video) { return; }
371
+
372
+ this.Update();
373
+
374
+ this.video.onseeked = (event) => this.Update(event);
375
+ this.video.onseeking = (event) => this.Update(event);
376
+ this.video.onplay = () => this.AddListener();
377
+ this.video.onpause = () => {
378
+ // On pause, seek to the nearest frame
379
+ this.Seek(this.Frame() + (this.frameRate.valueOf() > 30 ? 2 : 1));
380
+ this.RemoveListener();
381
+ };
382
+
383
+ this.video.onended = () => this.RemoveListener();
384
+ this.video.onratechange = () => {
385
+ // Update listener rate
386
+ if(this.listener) {
387
+ this.RemoveListener();
388
+ this.AddListener();
389
+ }
390
+ };
391
+ }
392
+
393
+ RemoveCallback() {
394
+ if(!this.video) { return; }
395
+
396
+ this.video.onseeked = undefined;
397
+ this.video.onseeking = undefined;
398
+ this.video.onplay = undefined;
399
+ this.video.onpause = undefined;
400
+ this.video.onended = undefined;
401
+ this.video.onratechange = undefined;
402
+ }
403
+
404
+ AddListener() {
405
+ if(!this.video) { return; }
406
+
407
+ // Call once per frame - possible range 10hz - 50hz Prevent division by zero
408
+ const fps = Fraction(this.video.playbackRate).mul(this.frameRate).add(Fraction("0.00001"));
409
+ const interval = Math.min(Math.max(Fraction(1000).div(fps).valueOf(), 20), 100);
410
+
411
+ this.listener = setInterval(() => {
412
+ if(this.video.paused || this.video.ended) {
413
+ return;
414
+ }
415
+
416
+ this.Update();
417
+ }, interval);
418
+ }
419
+
420
+ RemoveListener() {
421
+ if(this.listener) {
422
+ clearInterval(this.listener);
423
+ }
424
+
425
+ this.Update();
426
+ }
427
+ }
428
+
429
+ module.exports = {
430
+ FrameAccurateVideo
431
+ };
@@ -119,9 +119,10 @@ const New = context => {
119
119
  });
120
120
  };
121
121
 
122
- const write = async ({libraryId, metadata, noWait, objectId, writeToken}) => {
122
+ const write = async ({libraryId, metadataSubtree, metadata, noWait, objectId, writeToken}) => {
123
123
  return await context.concerns.Edit.writeMetadata({
124
124
  libraryId,
125
+ metadataSubtree,
125
126
  metadata,
126
127
  noWait,
127
128
  objectId,