@checksub_team/peaks_timeline 2.2.1 → 2.3.0-alpha.1
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/package.json +1 -1
- package/peaks.js +780 -175
- package/src/components/axis.js +24 -13
- package/src/components/invoker.js +218 -2
- package/src/components/line-group.js +6 -6
- package/src/components/line-groups.js +8 -3
- package/src/components/line-indicator.js +11 -2
- package/src/components/mode-layer.js +50 -7
- package/src/components/playhead-layer.js +13 -3
- package/src/components/source-group.js +303 -65
- package/src/components/sources-layer.js +305 -42
- package/src/components/waveform-builder.js +27 -9
- package/src/utils.js +90 -0
- package/src/view.js +152 -44
|
@@ -43,12 +43,29 @@ define([
|
|
|
43
43
|
this._lineGroups = new LineGroups(peaks, view, this);
|
|
44
44
|
this._lineGroups.addToLayer(this);
|
|
45
45
|
|
|
46
|
+
// Drag overlay container.
|
|
47
|
+
// Use a Group inside the main sources layer to avoid adding an extra Stage layer.
|
|
48
|
+
this._dragGroup = new Konva.Group({
|
|
49
|
+
listening: false
|
|
50
|
+
});
|
|
51
|
+
this._layer.add(this._dragGroup);
|
|
52
|
+
|
|
46
53
|
this._loadedData = {};
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
// Create invoker for performance optimizations
|
|
56
|
+
this._invoker = new Invoker();
|
|
57
|
+
|
|
58
|
+
// Version counter for cancellable rescale operations
|
|
59
|
+
this._rescaleVersion = 0;
|
|
60
|
+
|
|
61
|
+
// Throttled batch draw to prevent excessive redraws
|
|
62
|
+
this._throttledBatchDraw = this._invoker.throttleTrailing(
|
|
63
|
+
this._layer.batchDraw.bind(this._layer)
|
|
50
64
|
);
|
|
51
65
|
|
|
66
|
+
// Pending draw flag to coalesce multiple draw requests
|
|
67
|
+
this._drawPending = false;
|
|
68
|
+
|
|
52
69
|
this._peaks.on('handler.sources.add', this._onSourcesAdd.bind(this));
|
|
53
70
|
this._peaks.on('handler.sources.destroy', this._onSourcesDestroy.bind(this));
|
|
54
71
|
this._peaks.on('handler.sources.show', this._onSourcesShow.bind(this));
|
|
@@ -59,8 +76,16 @@ define([
|
|
|
59
76
|
this._peaks.on('handler.segments.show', this._onSegmentsShow.bind(this));
|
|
60
77
|
this._peaks.on('model.source.setIndicators', this.setIndicators.bind(this));
|
|
61
78
|
this._peaks.on('handler.view.mouseup', this._stopDrag.bind(this));
|
|
79
|
+
this._peaks.on('sources.delayedLineChange', this._onSourcesDelayedLineChanged.bind(this));
|
|
62
80
|
}
|
|
63
81
|
|
|
82
|
+
SourcesLayer.prototype._onSourcesDelayedLineChanged = function() {
|
|
83
|
+
// Update dragged source groups when sources change line after a delay (e.g., after automatic line creation)
|
|
84
|
+
if (this._draggedElements && this._draggedElements.length > 0) {
|
|
85
|
+
this._dragSourcesGroup();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
64
89
|
SourcesLayer.prototype._stopDrag = function() {
|
|
65
90
|
const draggedSourceGroup = this._sourcesGroup[this._draggedElementId];
|
|
66
91
|
|
|
@@ -87,6 +112,11 @@ define([
|
|
|
87
112
|
|
|
88
113
|
SourcesLayer.prototype.add = function(element) {
|
|
89
114
|
this._layer.add(element);
|
|
115
|
+
|
|
116
|
+
// Keep drag group on top even if other callers add elements later.
|
|
117
|
+
if (this._dragGroup) {
|
|
118
|
+
this._dragGroup.moveToTop();
|
|
119
|
+
}
|
|
90
120
|
};
|
|
91
121
|
|
|
92
122
|
/**
|
|
@@ -190,7 +220,7 @@ define([
|
|
|
190
220
|
|
|
191
221
|
this._view.updateTimelineLength();
|
|
192
222
|
|
|
193
|
-
this.
|
|
223
|
+
this.batchDraw();
|
|
194
224
|
};
|
|
195
225
|
|
|
196
226
|
SourcesLayer.prototype._onSourcesShow = function(sources) {
|
|
@@ -200,7 +230,7 @@ define([
|
|
|
200
230
|
self._sourcesGroup[source.id].setWrapping(false, true);
|
|
201
231
|
});
|
|
202
232
|
|
|
203
|
-
this.
|
|
233
|
+
this.batchDraw();
|
|
204
234
|
};
|
|
205
235
|
|
|
206
236
|
SourcesLayer.prototype._onSourcesHide = function(sources) {
|
|
@@ -210,7 +240,7 @@ define([
|
|
|
210
240
|
self._sourcesGroup[source.id].setWrapping(true, true);
|
|
211
241
|
});
|
|
212
242
|
|
|
213
|
-
this.
|
|
243
|
+
this.batchDraw();
|
|
214
244
|
};
|
|
215
245
|
|
|
216
246
|
SourcesLayer.prototype._onDataRetrieved = function(data, source, url) {
|
|
@@ -243,7 +273,7 @@ define([
|
|
|
243
273
|
SourcesLayer.prototype._onSegmentsShow = function(segmentsGroupId, lineId) {
|
|
244
274
|
this._lineGroups.addSegments(segmentsGroupId, lineId);
|
|
245
275
|
this._view.updateTimelineLength();
|
|
246
|
-
this.
|
|
276
|
+
this.batchDraw();
|
|
247
277
|
};
|
|
248
278
|
|
|
249
279
|
/**
|
|
@@ -285,12 +315,13 @@ define([
|
|
|
285
315
|
|
|
286
316
|
if (sourceGroup) {
|
|
287
317
|
sourceGroup.createIndicators();
|
|
288
|
-
this.
|
|
318
|
+
this.batchDraw();
|
|
289
319
|
}
|
|
290
320
|
};
|
|
291
321
|
|
|
292
322
|
/**
|
|
293
323
|
* Updates the positions of all displayed sources in the view.
|
|
324
|
+
* Uses optimized batching to reduce draw calls.
|
|
294
325
|
*
|
|
295
326
|
* @param {Number} startTime The start of the visible range in the view,
|
|
296
327
|
* in seconds.
|
|
@@ -307,12 +338,16 @@ define([
|
|
|
307
338
|
|
|
308
339
|
var count = sources.length;
|
|
309
340
|
|
|
310
|
-
sources
|
|
341
|
+
// Batch update all sources
|
|
342
|
+
for (var i = 0; i < sources.length; i++) {
|
|
343
|
+
this._updateSource(sources[i]);
|
|
344
|
+
}
|
|
311
345
|
|
|
312
346
|
count += this._removeInvisibleSources(startTime, endTime);
|
|
313
347
|
|
|
314
348
|
if (count > 0) {
|
|
315
|
-
|
|
349
|
+
// Use throttled batch draw for consistent performance
|
|
350
|
+
this.batchDraw();
|
|
316
351
|
}
|
|
317
352
|
};
|
|
318
353
|
|
|
@@ -348,23 +383,67 @@ define([
|
|
|
348
383
|
orderable: true,
|
|
349
384
|
draggable: true
|
|
350
385
|
});
|
|
386
|
+
|
|
387
|
+
var self = this;
|
|
388
|
+
|
|
389
|
+
this._initialSourcePositions = {};
|
|
390
|
+
|
|
391
|
+
this._draggedElements.forEach(function(source) {
|
|
392
|
+
// Store initial position for ALL dragged sources (even those outside view)
|
|
393
|
+
// This is needed for time calculations during drag
|
|
394
|
+
self._initialSourcePositions[source.id] = {
|
|
395
|
+
startTime: source.startTime,
|
|
396
|
+
endTime: source.endTime,
|
|
397
|
+
lineId: source.lineId
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
var sourceGroup = self._sourcesGroup[source.id];
|
|
401
|
+
|
|
402
|
+
if (sourceGroup) {
|
|
403
|
+
// Mark as dragging (for all sources, not just the clicked one)
|
|
404
|
+
sourceGroup.setDragging(true);
|
|
405
|
+
|
|
406
|
+
// Get absolute Y position before moving (relative to line group)
|
|
407
|
+
var absoluteY = sourceGroup.getAbsoluteY();
|
|
408
|
+
|
|
409
|
+
// Move source to the drag group so it draws above ALL other sources/segments
|
|
410
|
+
// without introducing an additional Konva.Layer on the Stage.
|
|
411
|
+
sourceGroup.moveTo(self._dragGroup);
|
|
412
|
+
// Restore the Y position (now relative to drag layer, which is at y=0)
|
|
413
|
+
sourceGroup.y(absoluteY);
|
|
414
|
+
}
|
|
415
|
+
});
|
|
351
416
|
};
|
|
352
417
|
|
|
353
418
|
SourcesLayer.prototype.onSourcesGroupDragEnd = function() {
|
|
419
|
+
var self = this;
|
|
420
|
+
|
|
421
|
+
this._initialSourcePositions = null;
|
|
422
|
+
this._dragOffsetX = undefined;
|
|
423
|
+
this._dragOffsetY = undefined;
|
|
424
|
+
|
|
354
425
|
const updatedSources = this._draggedElements.map(
|
|
355
426
|
function(source) {
|
|
356
|
-
const sourceGroup =
|
|
427
|
+
const sourceGroup = self._sourcesGroup[source.id];
|
|
357
428
|
|
|
358
429
|
if (sourceGroup) {
|
|
430
|
+
// Clear dragging state before moving back to line group
|
|
431
|
+
sourceGroup.setDragging(false);
|
|
359
432
|
sourceGroup.prepareDragEnd();
|
|
433
|
+
// Move source back to its line group (it was moved to layer during drag)
|
|
434
|
+
self._lineGroups.addSource(source, sourceGroup);
|
|
435
|
+
// Reset Y position to 0 relative to parent line group
|
|
436
|
+
// (source followed cursor during drag, now snap to line position)
|
|
437
|
+
sourceGroup.y(0);
|
|
360
438
|
}
|
|
361
439
|
|
|
362
440
|
return source;
|
|
363
|
-
}
|
|
441
|
+
}
|
|
364
442
|
);
|
|
365
443
|
|
|
366
444
|
this._draggedElementId = null;
|
|
367
445
|
|
|
446
|
+
this.refresh();
|
|
368
447
|
this._view.batchDrawSourcesLayer();
|
|
369
448
|
this._view.updateTimelineLength();
|
|
370
449
|
|
|
@@ -372,7 +451,70 @@ define([
|
|
|
372
451
|
};
|
|
373
452
|
|
|
374
453
|
SourcesLayer.prototype.onSourcesGroupDrag = function(draggedElement) {
|
|
375
|
-
this._view.
|
|
454
|
+
var pointerPos = this._view.getPointerPosition();
|
|
455
|
+
|
|
456
|
+
this._view.updateWithAutoScroll(this._dragSourcesGroup.bind(this), null, true);
|
|
457
|
+
|
|
458
|
+
// Return position that follows the mouse cursor exactly
|
|
459
|
+
var clickedSourceGroup = this._sourcesGroup[this._draggedElementId];
|
|
460
|
+
|
|
461
|
+
if (clickedSourceGroup) {
|
|
462
|
+
var mouseX = pointerPos.x;
|
|
463
|
+
var mouseY = pointerPos.y;
|
|
464
|
+
var offsetX = this._dragOffsetX || 0;
|
|
465
|
+
var offsetY = this._dragOffsetY || 0;
|
|
466
|
+
|
|
467
|
+
// Calculate offset on first drag if not set
|
|
468
|
+
if (this._dragOffsetX === undefined) {
|
|
469
|
+
var currentPos = draggedElement.absolutePosition();
|
|
470
|
+
|
|
471
|
+
this._dragOffsetX = currentPos.x - mouseX;
|
|
472
|
+
this._dragOffsetY = currentPos.y - mouseY;
|
|
473
|
+
offsetX = this._dragOffsetX;
|
|
474
|
+
offsetY = this._dragOffsetY;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
var clickedSourceX = mouseX + offsetX;
|
|
478
|
+
var clickedSourceY = mouseY + offsetY;
|
|
479
|
+
|
|
480
|
+
// Position all other dragged sources relative to the clicked source
|
|
481
|
+
// They should maintain their initial relative positions
|
|
482
|
+
if (this._draggedElements && this._draggedElements.length > 1 && this._initialSourcePositions) {
|
|
483
|
+
var self = this;
|
|
484
|
+
var clickedInitialPos = this._initialSourcePositions[this._draggedElementId];
|
|
485
|
+
|
|
486
|
+
if (clickedInitialPos) {
|
|
487
|
+
var clickedInitialPixelX = this._view.timeToPixels(clickedInitialPos.startTime);
|
|
488
|
+
|
|
489
|
+
this._draggedElements.forEach(function(source) {
|
|
490
|
+
if (source.id !== self._draggedElementId) {
|
|
491
|
+
var sourceGroup = self._sourcesGroup[source.id];
|
|
492
|
+
|
|
493
|
+
if (sourceGroup) {
|
|
494
|
+
var initialPos = self._initialSourcePositions[source.id];
|
|
495
|
+
|
|
496
|
+
if (initialPos) {
|
|
497
|
+
// Calculate pixel offset from clicked source's initial position
|
|
498
|
+
var initialPixelX = self._view.timeToPixels(initialPos.startTime);
|
|
499
|
+
var pixelOffset = initialPixelX - clickedInitialPixelX;
|
|
500
|
+
|
|
501
|
+
// Position this source relative to clicked source's current position
|
|
502
|
+
sourceGroup.absolutePosition({
|
|
503
|
+
x: clickedSourceX + pixelOffset,
|
|
504
|
+
y: clickedSourceY
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
x: clickedSourceX,
|
|
515
|
+
y: clickedSourceY
|
|
516
|
+
};
|
|
517
|
+
}
|
|
376
518
|
|
|
377
519
|
return {
|
|
378
520
|
x: draggedElement.absolutePosition().x,
|
|
@@ -399,10 +541,13 @@ define([
|
|
|
399
541
|
return;
|
|
400
542
|
}
|
|
401
543
|
|
|
544
|
+
var newStartTime = Utils.roundTime(initialStartTime + timeOffsetDiff + timeDiff);
|
|
545
|
+
var newEndTime = Utils.roundTime(initialEndTime + timeOffsetDiff + timeDiff);
|
|
546
|
+
|
|
402
547
|
const shouldRedraw = this.manageSourceMovements(
|
|
403
548
|
this._draggedElements,
|
|
404
|
-
|
|
405
|
-
|
|
549
|
+
newStartTime,
|
|
550
|
+
newEndTime,
|
|
406
551
|
orderable,
|
|
407
552
|
mousePosX,
|
|
408
553
|
mousePosY
|
|
@@ -424,8 +569,40 @@ define([
|
|
|
424
569
|
);
|
|
425
570
|
};
|
|
426
571
|
|
|
427
|
-
|
|
428
|
-
|
|
572
|
+
/**
|
|
573
|
+
* Updates source times during drag using initial positions.
|
|
574
|
+
*
|
|
575
|
+
* @private
|
|
576
|
+
* @param {Number} newStartTime The new start time for the first source
|
|
577
|
+
*/
|
|
578
|
+
SourcesLayer.prototype._updateSourceTimesDuringDrag = function(newStartTime) {
|
|
579
|
+
if (!this._initialSourcePositions || !this._draggedElements) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
var self = this;
|
|
584
|
+
var firstSourceInitial = this._initialSourcePositions[this._draggedElements[0].id];
|
|
585
|
+
|
|
586
|
+
if (!firstSourceInitial) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Calculate time diff from INITIAL position, not current position
|
|
591
|
+
var timeDiff = Utils.roundTime(newStartTime - firstSourceInitial.startTime);
|
|
592
|
+
|
|
593
|
+
this._draggedElements.forEach(function(source) {
|
|
594
|
+
var initialPos = self._initialSourcePositions[source.id];
|
|
595
|
+
|
|
596
|
+
if (initialPos) {
|
|
597
|
+
source.updateTimes(
|
|
598
|
+
Utils.roundTime(initialPos.startTime + timeDiff),
|
|
599
|
+
Utils.roundTime(initialPos.endTime + timeDiff)
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
SourcesLayer.prototype._applyTimeChangesToSources = function(sources, initialStartTime, newStartTime, newEndTime
|
|
429
606
|
) {
|
|
430
607
|
if (sources.length === 1) {
|
|
431
608
|
sources[0].updateTimes(
|
|
@@ -434,18 +611,49 @@ define([
|
|
|
434
611
|
);
|
|
435
612
|
}
|
|
436
613
|
else {
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
if (
|
|
443
|
-
sources.
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
614
|
+
// When moving multiple sources via block-drag, compute time diff from the
|
|
615
|
+
// drag-start positions to avoid cumulative drift as times are updated.
|
|
616
|
+
var canUseInitialPositions = Boolean(this._initialSourcePositions
|
|
617
|
+
&& this._initialSourcePositions[sources[0].id]);
|
|
618
|
+
|
|
619
|
+
if (canUseInitialPositions) {
|
|
620
|
+
for (var i = 0; i < sources.length; i++) {
|
|
621
|
+
if (!this._initialSourcePositions[sources[i].id]) {
|
|
622
|
+
canUseInitialPositions = false;
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (canUseInitialPositions) {
|
|
629
|
+
var firstInitial = this._initialSourcePositions[sources[0].id];
|
|
630
|
+
var timeDiffFromInitial = Utils.roundTime(newStartTime - firstInitial.startTime);
|
|
631
|
+
|
|
632
|
+
if (timeDiffFromInitial !== 0) {
|
|
633
|
+
var self = this;
|
|
634
|
+
|
|
635
|
+
sources.forEach(function(source) {
|
|
636
|
+
var initialPos = self._initialSourcePositions[source.id];
|
|
637
|
+
|
|
638
|
+
source.updateTimes(
|
|
639
|
+
Utils.roundTime(initialPos.startTime + timeDiffFromInitial),
|
|
640
|
+
Utils.roundTime(initialPos.endTime + timeDiffFromInitial)
|
|
641
|
+
);
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
// Fallback for non-drag multi-updates.
|
|
647
|
+
const timeDiff = Utils.roundTime(newStartTime - initialStartTime);
|
|
648
|
+
|
|
649
|
+
if (timeDiff !== 0) {
|
|
650
|
+
sources.forEach(function(source) {
|
|
651
|
+
source.updateTimes(
|
|
652
|
+
Utils.roundTime(source.startTime + timeDiff),
|
|
653
|
+
Utils.roundTime(source.endTime + timeDiff)
|
|
654
|
+
);
|
|
655
|
+
});
|
|
656
|
+
}
|
|
449
657
|
}
|
|
450
658
|
}
|
|
451
659
|
|
|
@@ -501,9 +709,31 @@ define([
|
|
|
501
709
|
|
|
502
710
|
SourcesLayer.prototype._findOrAddSourceGroup = function(source) {
|
|
503
711
|
var sourceGroup = this._sourcesGroup[source.id];
|
|
712
|
+
var isNewlyCreated = false;
|
|
504
713
|
|
|
505
714
|
if (!sourceGroup) {
|
|
506
715
|
sourceGroup = this._addSourceGroup(source);
|
|
716
|
+
isNewlyCreated = true;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// If this source is being dragged and was just recreated (came back into view),
|
|
720
|
+
// set it up properly for dragging
|
|
721
|
+
if (isNewlyCreated && this._draggedElements && this._initialSourcePositions) {
|
|
722
|
+
var isDraggedSource = this._draggedElements.some(function(s) {
|
|
723
|
+
return s.id === source.id;
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
if (isDraggedSource) {
|
|
727
|
+
// Mark as dragging
|
|
728
|
+
sourceGroup.setDragging(true);
|
|
729
|
+
|
|
730
|
+
// Get Y position from line group before moving
|
|
731
|
+
var absoluteY = sourceGroup.getAbsoluteY();
|
|
732
|
+
|
|
733
|
+
// Move to drag group
|
|
734
|
+
sourceGroup.moveTo(this._dragGroup);
|
|
735
|
+
sourceGroup.y(absoluteY);
|
|
736
|
+
}
|
|
507
737
|
}
|
|
508
738
|
|
|
509
739
|
return sourceGroup;
|
|
@@ -625,8 +855,12 @@ define([
|
|
|
625
855
|
this._layer.setVisible(visible);
|
|
626
856
|
};
|
|
627
857
|
|
|
858
|
+
/**
|
|
859
|
+
* Schedules a batch draw using RAF throttling for better performance.
|
|
860
|
+
* Multiple calls within the same frame are coalesced into one draw.
|
|
861
|
+
*/
|
|
628
862
|
SourcesLayer.prototype.batchDraw = function() {
|
|
629
|
-
this.
|
|
863
|
+
this._throttledBatchDraw();
|
|
630
864
|
};
|
|
631
865
|
|
|
632
866
|
SourcesLayer.prototype.listening = function(bool) {
|
|
@@ -654,21 +888,36 @@ define([
|
|
|
654
888
|
return endsLater || startsEarlier;
|
|
655
889
|
};
|
|
656
890
|
|
|
657
|
-
SourcesLayer.prototype.rescale = function(
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
else {
|
|
662
|
-
this._rescale();
|
|
663
|
-
}
|
|
891
|
+
SourcesLayer.prototype.rescale = function() {
|
|
892
|
+
// Increment the rescale version to cancel any in-progress rescale
|
|
893
|
+
this._rescaleVersion = (this._rescaleVersion || 0) + 1;
|
|
894
|
+
this._rescale(this._rescaleVersion);
|
|
664
895
|
};
|
|
665
896
|
|
|
666
|
-
SourcesLayer.prototype._rescale = function() {
|
|
667
|
-
var
|
|
897
|
+
SourcesLayer.prototype._rescale = function(version) {
|
|
898
|
+
var self = this;
|
|
899
|
+
var ids = Object.keys(this._sourcesGroup);
|
|
900
|
+
var urls = [];
|
|
901
|
+
var index = 0;
|
|
902
|
+
|
|
903
|
+
function processNext() {
|
|
904
|
+
// Check if this rescale was cancelled (a newer one started)
|
|
905
|
+
if (self._rescaleVersion !== version) {
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (index >= ids.length) {
|
|
910
|
+
// Done processing all sources
|
|
911
|
+
self.batchDraw();
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
var id = ids[index];
|
|
916
|
+
var sourceGroup = self._sourcesGroup[id];
|
|
668
917
|
|
|
669
|
-
|
|
670
|
-
if (
|
|
671
|
-
audioPreviews =
|
|
918
|
+
// Skip if source group was removed during async processing
|
|
919
|
+
if (sourceGroup) {
|
|
920
|
+
var audioPreviews = sourceGroup.getAudioPreview();
|
|
672
921
|
|
|
673
922
|
audioPreviews.forEach(function(audioPreview) {
|
|
674
923
|
if (self._shouldResampleAudio(audioPreview.url, urls)) {
|
|
@@ -681,8 +930,14 @@ define([
|
|
|
681
930
|
}
|
|
682
931
|
});
|
|
683
932
|
}
|
|
933
|
+
|
|
934
|
+
index++;
|
|
935
|
+
|
|
936
|
+
// Yield to allow cancellation and UI updates
|
|
937
|
+
Utils.scheduleIdle(processNext, { timeout: 16 });
|
|
684
938
|
}
|
|
685
|
-
|
|
939
|
+
|
|
940
|
+
processNext();
|
|
686
941
|
};
|
|
687
942
|
|
|
688
943
|
SourcesLayer.prototype._shouldResampleAudio = function(audioUrl, urls) {
|
|
@@ -708,6 +963,14 @@ define([
|
|
|
708
963
|
this._peaks.off('handler.segments.show', this._onSegmentsShow);
|
|
709
964
|
this._peaks.off('model.source.setIndicators', this.setIndicators);
|
|
710
965
|
this._peaks.off('handler.view.mouseup', this._stopDrag);
|
|
966
|
+
|
|
967
|
+
// Cancel any in-progress rescale
|
|
968
|
+
this._rescaleVersion++;
|
|
969
|
+
|
|
970
|
+
// Clean up invoker resources
|
|
971
|
+
if (this._invoker) {
|
|
972
|
+
this._invoker.destroy();
|
|
973
|
+
}
|
|
711
974
|
};
|
|
712
975
|
|
|
713
976
|
SourcesLayer.prototype.getHeight = function() {
|
|
@@ -16,13 +16,8 @@ define([
|
|
|
16
16
|
|
|
17
17
|
var isXhr2 = ('withCredentials' in new XMLHttpRequest());
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
if (typeof window !== 'undefined' && window.requestIdleCallback) {
|
|
22
|
-
return window.requestIdleCallback(fn, { timeout: 80 });
|
|
23
|
-
}
|
|
24
|
-
return setTimeout(fn, 0);
|
|
25
|
-
}
|
|
19
|
+
// Waveform data cache for better performance on repeated requests
|
|
20
|
+
var waveformCache = Utils.createLRUCache(50);
|
|
26
21
|
|
|
27
22
|
/**
|
|
28
23
|
* Creates and returns a WaveformData object, either by requesting the
|
|
@@ -39,6 +34,14 @@ define([
|
|
|
39
34
|
this._peaks = peaks;
|
|
40
35
|
}
|
|
41
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Clears the waveform data cache.
|
|
39
|
+
* Useful when memory needs to be freed.
|
|
40
|
+
*/
|
|
41
|
+
WaveformBuilder.clearCache = function() {
|
|
42
|
+
waveformCache.clear();
|
|
43
|
+
};
|
|
44
|
+
|
|
42
45
|
/**
|
|
43
46
|
* Options for requesting remote waveform data.
|
|
44
47
|
*
|
|
@@ -192,6 +195,18 @@ define([
|
|
|
192
195
|
return;
|
|
193
196
|
}
|
|
194
197
|
|
|
198
|
+
// Check cache first for better performance
|
|
199
|
+
var cacheKey = url + ':' + requestType;
|
|
200
|
+
var cachedData = waveformCache.get(cacheKey);
|
|
201
|
+
|
|
202
|
+
if (cachedData) {
|
|
203
|
+
// Return cached waveform data asynchronously to maintain consistent API
|
|
204
|
+
Utils.scheduleIdle(function() {
|
|
205
|
+
callback(null, cachedData);
|
|
206
|
+
});
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
195
210
|
var xhr = self._createXHR(url, requestType, options.withCredentials, function(event) {
|
|
196
211
|
if (this.readyState !== 4) {
|
|
197
212
|
return;
|
|
@@ -205,7 +220,7 @@ define([
|
|
|
205
220
|
return;
|
|
206
221
|
}
|
|
207
222
|
|
|
208
|
-
scheduleIdle(function() {
|
|
223
|
+
Utils.scheduleIdle(function() {
|
|
209
224
|
try {
|
|
210
225
|
var waveformData = WaveformData.create(event.target.response);
|
|
211
226
|
|
|
@@ -214,6 +229,9 @@ define([
|
|
|
214
229
|
return;
|
|
215
230
|
}
|
|
216
231
|
|
|
232
|
+
// Cache the waveform data for future use
|
|
233
|
+
waveformCache.set(cacheKey, waveformData);
|
|
234
|
+
|
|
217
235
|
callback(null, waveformData);
|
|
218
236
|
}
|
|
219
237
|
catch (err) {
|
|
@@ -270,7 +288,7 @@ define([
|
|
|
270
288
|
return;
|
|
271
289
|
}
|
|
272
290
|
|
|
273
|
-
scheduleIdle(function() {
|
|
291
|
+
Utils.scheduleIdle(function() {
|
|
274
292
|
try {
|
|
275
293
|
var createdWaveformData = WaveformData.create(data);
|
|
276
294
|
|
package/src/utils.js
CHANGED
|
@@ -338,6 +338,96 @@ define([
|
|
|
338
338
|
var BB = ((B.toString(16).length === 1) ? '0' + B.toString(16) : B.toString(16));
|
|
339
339
|
|
|
340
340
|
return '#' + RR + GG + BB;
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
// ==================== Performance Utilities ====================
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Schedules work during browser idle time.
|
|
347
|
+
* Falls back to setTimeout if requestIdleCallback is not available.
|
|
348
|
+
*
|
|
349
|
+
* @param {Function} callback Function to execute
|
|
350
|
+
* @param {Object} options Options object with timeout property
|
|
351
|
+
* @returns {Number} ID that can be used to cancel
|
|
352
|
+
*/
|
|
353
|
+
scheduleIdle: function(callback, options) {
|
|
354
|
+
options = options || { timeout: 50 };
|
|
355
|
+
|
|
356
|
+
if (typeof window !== 'undefined' && window.requestIdleCallback) {
|
|
357
|
+
return window.requestIdleCallback(callback, options);
|
|
358
|
+
}
|
|
359
|
+
return setTimeout(function() {
|
|
360
|
+
callback({
|
|
361
|
+
didTimeout: true,
|
|
362
|
+
timeRemaining: function() {
|
|
363
|
+
return 0;
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}, 0);
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Cancels a scheduled idle callback.
|
|
371
|
+
*
|
|
372
|
+
* @param {Number} id The ID returned by scheduleIdle
|
|
373
|
+
*/
|
|
374
|
+
cancelIdle: function(id) {
|
|
375
|
+
if (typeof window !== 'undefined' && window.cancelIdleCallback) {
|
|
376
|
+
window.cancelIdleCallback(id);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
clearTimeout(id);
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Creates a simple LRU (Least Recently Used) cache.
|
|
385
|
+
*
|
|
386
|
+
* @param {Number} maxSize Maximum number of items to cache
|
|
387
|
+
* @returns {Object} Cache with get(), set(), has(), delete(), clear() methods
|
|
388
|
+
*/
|
|
389
|
+
createLRUCache: function(maxSize) {
|
|
390
|
+
var cache = new Map();
|
|
391
|
+
|
|
392
|
+
maxSize = maxSize || 100;
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
get: function(key) {
|
|
396
|
+
if (!cache.has(key)) {
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
// Move to end (most recently used)
|
|
400
|
+
var value = cache.get(key);
|
|
401
|
+
|
|
402
|
+
cache.delete(key);
|
|
403
|
+
cache.set(key, value);
|
|
404
|
+
return value;
|
|
405
|
+
},
|
|
406
|
+
set: function(key, value) {
|
|
407
|
+
if (cache.has(key)) {
|
|
408
|
+
cache.delete(key);
|
|
409
|
+
}
|
|
410
|
+
else if (cache.size >= maxSize) {
|
|
411
|
+
// Delete oldest (first) entry
|
|
412
|
+
var firstKey = cache.keys().next().value;
|
|
413
|
+
|
|
414
|
+
cache.delete(firstKey);
|
|
415
|
+
}
|
|
416
|
+
cache.set(key, value);
|
|
417
|
+
},
|
|
418
|
+
has: function(key) {
|
|
419
|
+
return cache.has(key);
|
|
420
|
+
},
|
|
421
|
+
delete: function(key) {
|
|
422
|
+
return cache.delete(key);
|
|
423
|
+
},
|
|
424
|
+
clear: function() {
|
|
425
|
+
cache.clear();
|
|
426
|
+
},
|
|
427
|
+
size: function() {
|
|
428
|
+
return cache.size;
|
|
429
|
+
}
|
|
430
|
+
};
|
|
341
431
|
}
|
|
342
432
|
};
|
|
343
433
|
});
|