@checksub_team/peaks_timeline 1.4.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/src/main.js ADDED
@@ -0,0 +1,663 @@
1
+ /**
2
+ * @file
3
+ *
4
+ * Defines the {@link Peaks} class.
5
+ *
6
+ * @module main
7
+ */
8
+
9
+ define([
10
+ 'colors.css',
11
+ 'eventemitter2',
12
+ './timeline-segments',
13
+ './timeline-sources',
14
+ './keyboard-handler',
15
+ './player',
16
+ './marker-factories',
17
+ './timeline-zoomview',
18
+ './utils'
19
+ ], function(
20
+ Colors,
21
+ EventEmitter,
22
+ TimelineSegments,
23
+ TimelineSources,
24
+ KeyboardHandler,
25
+ Player,
26
+ MarkerFactories,
27
+ TimelineZoomView,
28
+ Utils) {
29
+ 'use strict';
30
+
31
+ /**
32
+ * Initialises a new Peaks instance with default option settings.
33
+ *
34
+ * @class
35
+ * @alias Peaks
36
+ *
37
+ * @param {Object} opts Configuration options
38
+ */
39
+
40
+ function Peaks() {
41
+ EventEmitter.call(this, { wildcard: true });
42
+
43
+ this.options = {
44
+
45
+ /**
46
+ * Array of scale factors (samples per pixel) for the zoom levels
47
+ * (big >> small)
48
+ */
49
+ zoomRange: [5, 2000],
50
+
51
+ initialZoomLevel: 100,
52
+
53
+ minScale: 40,
54
+
55
+ /**
56
+ * Data URI where to get the waveform data.
57
+ *
58
+ * If a string, we assume that `this.dataUriDefaultFormat` is the default
59
+ * `xhr.responseType` value.
60
+ *
61
+ * @since 0.0.1
62
+ *
63
+ * ```js
64
+ * dataUri: 'url/to/data.json?waveformId=1337'
65
+ * ```
66
+ *
67
+ * If an object, each key is an `xhr.responseType` which will contain its
68
+ * associated source URI.
69
+ *
70
+ * @since 0.3.0
71
+ *
72
+ * ```js
73
+ * dataUri: {
74
+ * arraybuffer: 'url/to/data.dat',
75
+ * json: 'url/to/data.json'
76
+ * }
77
+ * ```
78
+ */
79
+ dataUri: null,
80
+
81
+ objectUrl: null,
82
+
83
+ /**
84
+ * Will be used as a `xhr.responseType` if `dataUri` is a string, and not
85
+ * an object. Here for backward compatibility purpose only.
86
+ *
87
+ * @since 0.3.0
88
+ */
89
+ dataUriDefaultFormat: 'json',
90
+
91
+ /**
92
+ * If true, all ajax requests (e.g. to fetch waveform data) will be made
93
+ * with credentials (i.e. browser-controlled cookies).
94
+ *
95
+ * @type {Boolean}
96
+ */
97
+ withCredentials: false,
98
+
99
+ /**
100
+ * Will report errors to that function
101
+ *
102
+ * @type {Function=}
103
+ * @since 0.4.4
104
+ */
105
+ logger: null,
106
+
107
+ /**
108
+ * Deprecation messages logger.
109
+ *
110
+ * @type {Function}
111
+ * @since 0.4.8
112
+ */
113
+ // eslint-disable-next-line no-console
114
+ deprecationLogger: console.log.bind(console),
115
+
116
+ /**
117
+ * Bind keyboard controls
118
+ */
119
+ keyboard: false,
120
+
121
+ /**
122
+ * Keyboard nudge increment in seconds (left arrow/right arrow)
123
+ */
124
+ nudgeIncrement: 1.0,
125
+
126
+ /**
127
+ * Colour for the zoomed in waveform
128
+ */
129
+ zoomWaveformColor: 'rgba(180, 180, 180, 1)',
130
+
131
+ /**
132
+ * Random colour per segment (overrides segmentColor)
133
+ */
134
+ randomizeSegmentColor: true,
135
+
136
+ /**
137
+ * Block mouse clicks if a meta key is pressed
138
+ */
139
+ blockUpdatingOnMouseClickWithMetaKey: false,
140
+
141
+ /**
142
+ * Default height of the waveform canvases in pixels.
143
+ * The height of the canvas might grow depending on the number of line inside.
144
+ */
145
+ height: 200,
146
+
147
+ /**
148
+ * Colour for segments on the waveform
149
+ */
150
+ segmentColor: Colors.orange,
151
+
152
+ /**
153
+ * Colour of the play head
154
+ */
155
+ playheadColor: Colors.red,
156
+
157
+ /**
158
+ * Colour of the play head text
159
+ */
160
+ playheadTextColor: Colors.red,
161
+
162
+ /**
163
+ * Show current time position by the play head marker
164
+ * (zoom view only)
165
+ */
166
+ showPlayheadTime: true,
167
+
168
+ /**
169
+ * Show segment markers, allowing to resize a segment by dragging its handles
170
+ */
171
+ showSegmentMarkers: true,
172
+
173
+ /**
174
+ * Colour of the axis gridlines
175
+ */
176
+ axisGridlineColor: '#ccc',
177
+
178
+ /**
179
+ * Colour of the axis labels
180
+ */
181
+ axisLabelColor: Colors.gray,
182
+
183
+ /**
184
+ *
185
+ */
186
+ template: [
187
+ '<div class="timeline">',
188
+ '<div class="zoom-container"></div>',
189
+ '</div>'
190
+ ].join(''),
191
+
192
+ /**
193
+ * An object containing an AudioContext, used when creating waveform data
194
+ * using the Web Audio API
195
+ */
196
+
197
+ webAudio: null,
198
+
199
+ /**
200
+ * Point/Segment marker customisation.
201
+ *
202
+ * @todo This part of the API is not stable.
203
+ */
204
+ createSegmentMarker: MarkerFactories.createSegmentMarker,
205
+ createSegmentLabel: MarkerFactories.createSegmentLabel,
206
+
207
+ /**
208
+ * External sources information.
209
+ *
210
+ * ```js
211
+ * sources: {
212
+ * id: 'unique-identifier-for-this-source',
213
+ * title: 'Source #1',
214
+ * url: 'https://my-website/my-resource.mp3',
215
+ * dataUri: {
216
+ * arraybuffer: 'url/to/data.dat',
217
+ * json: 'url/to/data.json'
218
+ * },
219
+ * start: 1.03,
220
+ * end: 5.06,
221
+ * color: #0000ff,
222
+ * wrapped: false, //show only a line or peaks/preview
223
+ * position: 0 //position in the timeline (here on the first line)
224
+ * }
225
+ * ```
226
+ */
227
+ sources: null,
228
+
229
+ /**
230
+ * Height of a line, in pixels.
231
+ * This height will correspond to the height of the background when the element is unwrapped.
232
+ */
233
+ lineHeight: 80,
234
+
235
+ /**
236
+ * Ratio by which lineHeight must be multiplied to obtain the segment's line height.
237
+ */
238
+ segmentHeight: 32,
239
+
240
+ /**
241
+ * Height of a line, in pixels.
242
+ * This height will correspond to the height of the group
243
+ * containing the line and title of the wrapped source.
244
+ */
245
+ wrappedLineHeight: 20,
246
+
247
+ /**
248
+ * Height of an empty line, in pixels.
249
+ */
250
+ emptyLineHeight: 10,
251
+
252
+ /**
253
+ * Empty space between 2 sources, in pixels.
254
+ */
255
+ interline: 10,
256
+
257
+ /**
258
+ * Empty space before the first source and after the last source, in pixels.
259
+ */
260
+ padding: 30,
261
+
262
+ /**
263
+ * Horizontal empty space after the longest source, in pixels.
264
+ */
265
+ horizontalPadding: 0,
266
+
267
+ /**
268
+ * Threshold around a segment where the dragged segment is magnetized
269
+ * toward the former, in pixels.
270
+ */
271
+ segmentMagnetThreshold: 15,
272
+
273
+ /**
274
+ * Enable or disable the timeline vertical scrolling
275
+ */
276
+ enableVerticalScrolling: true,
277
+
278
+ /**
279
+ * Width of the left line indicator, in pixels
280
+ */
281
+ lineIndicatorWidth: 20,
282
+
283
+ /**
284
+ * Color of the line indicators
285
+ */
286
+ lineIndicatorColor: 'gray',
287
+
288
+ /**
289
+ * Color for the indicator when selected
290
+ */
291
+ lineIndicatorSelected: '#ccc',
292
+
293
+ /**
294
+ * Threshold size on the left and right border of the view,
295
+ * where auto scrolling is activated, between 0 and 1.
296
+ */
297
+ autoScrollThreshold: 0.05,
298
+
299
+ /**
300
+ * Indicates whether or not the context menu should be displayed
301
+ * on right click in the line indicator.
302
+ */
303
+ enableLineIndicatorContextMenu: true,
304
+
305
+ /**
306
+ * The minimal size of a source, in seconds
307
+ */
308
+ minSourceSize: 0.05,
309
+
310
+ /**
311
+ * The minimal size of a segment, in seconds
312
+ */
313
+ minSegmentSize: 0.2
314
+ };
315
+
316
+ /**
317
+ * Asynchronous errors logger.
318
+ *
319
+ * @type {Function}
320
+ */
321
+ // eslint-disable-next-line no-console
322
+ this.logger = console.error.bind(console);
323
+
324
+ return this;
325
+ }
326
+
327
+ Peaks.prototype = Object.create(EventEmitter.prototype);
328
+
329
+ /**
330
+ * Creates and initialises a new Peaks instance with the given options.
331
+ *
332
+ * @param {Object} opts Configuration options
333
+ *
334
+ * @return {Peaks}
335
+ */
336
+
337
+ Peaks.init = function(opts, callback) {
338
+ var instance = new Peaks();
339
+
340
+ opts = opts || {};
341
+
342
+ var err = instance._setOptions(opts);
343
+
344
+ if (err) {
345
+ callback(err);
346
+ return;
347
+ }
348
+
349
+ /*
350
+ Setup the fonts
351
+ */
352
+ /* eslint-disable max-len */
353
+ var fonts = [
354
+ 'https://fonts.gstatic.com/s/opensans/v27/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4kaVIGxA.woff2',
355
+ 'https://fonts.gstatic.com/s/opensans/v27/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4jaVIGxA.woff2',
356
+ 'https://fonts.gstatic.com/s/opensans/v27/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4iaVIGxA.woff2',
357
+ 'https://fonts.gstatic.com/s/opensans/v27/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4vaVIGxA.woff2',
358
+ 'https://fonts.gstatic.com/s/opensans/v27/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4gaVI.woff2'
359
+ ];
360
+ /* eslint-enable max-len */
361
+
362
+ fonts.forEach(function(fontUrl) {
363
+ var fontFace = new FontFace(
364
+ 'Open Sans',
365
+ 'url(' + fontUrl + ')'
366
+ );
367
+
368
+ document.fonts.add(fontFace);
369
+ fontFace.load();
370
+ });
371
+
372
+ /*
373
+ Setup the layout
374
+ */
375
+ if (!instance.options.containers) {
376
+ callback(new TypeError('Peaks.init(): The containers option must be a valid DOM object'));
377
+ return;
378
+ }
379
+
380
+ var zoomviewContainer = instance.options.containers.zoomview;
381
+
382
+ if (!Utils.isHTMLElement(zoomviewContainer)) {
383
+ // eslint-disable-next-line max-len
384
+ callback(new TypeError('Peaks.init(): The containers options must be valid HTML elements'));
385
+ return;
386
+ }
387
+
388
+ if (zoomviewContainer && zoomviewContainer.clientWidth <= 0) {
389
+ // eslint-disable-next-line max-len
390
+ callback(new TypeError('Peaks.init(): Please ensure that the container is visible and has non-zero width'));
391
+ return;
392
+ }
393
+
394
+ if (instance.options.keyboard) {
395
+ instance.keyboardHandler = new KeyboardHandler(instance);
396
+ }
397
+
398
+ instance.player = new Player(instance);
399
+ instance.segments = new TimelineSegments(instance);
400
+ instance.sources = new TimelineSources(instance);
401
+
402
+ // Setup the UI components
403
+ instance.view = new TimelineZoomView(
404
+ zoomviewContainer,
405
+ instance
406
+ );
407
+
408
+ instance._addWindowResizeHandler();
409
+
410
+ if (instance.options.sources) {
411
+ instance.sources.add(instance.options.sources);
412
+ }
413
+
414
+ document.fonts.ready.then(function() {
415
+ setTimeout(function() {
416
+ instance.emit('peaks.ready');
417
+ }, 0);
418
+
419
+ if (callback) {
420
+ callback(null, instance);
421
+ }
422
+ });
423
+ return instance;
424
+ };
425
+
426
+ Peaks.prototype._setOptions = function(opts) {
427
+ // eslint-disable-next-line no-console
428
+ opts.deprecationLogger = opts.deprecationLogger || console.log.bind(console);
429
+
430
+ if (opts.overviewHighlightRectangleColor) {
431
+ opts.overviewHighlightColor = opts.overviewHighlightRectangleColor;
432
+ // eslint-disable-next-line max-len
433
+ opts.deprecationLogger('Peaks.init(): The overviewHighlightRectangleColor option is deprecated, please use overviewHighlightColor instead');
434
+ }
435
+
436
+ if (opts.inMarkerColor) {
437
+ opts.segmentStartMarkerColor = opts.inMarkerColor;
438
+ // eslint-disable-next-line max-len
439
+ opts.deprecationLogger('Peaks.init(): The inMarkerColor option is deprecated, please use segmentStartMarkerColor instead');
440
+ }
441
+
442
+ if (opts.outMarkerColor) {
443
+ opts.segmentEndMarkerColor = opts.outMarkerColor;
444
+ // eslint-disable-next-line max-len
445
+ opts.deprecationLogger('Peaks.init(): The outMarkerColor option is deprecated, please use segmentEndMarkerColor instead');
446
+ }
447
+
448
+ if (!opts.container && !opts.containers) {
449
+ // eslint-disable-next-line max-len
450
+ return new Error('Peaks.init(): Please specify either a container or containers option');
451
+ }
452
+ else if (Boolean(opts.container) === Boolean(opts.containers)) {
453
+ // eslint-disable-next-line max-len
454
+ return new Error('Peaks.init(): Please specify either a container or containers option, but not both');
455
+ }
456
+
457
+ if (opts.template && opts.containers) {
458
+ // eslint-disable-next-line max-len
459
+ return new Error('Peaks.init(): Please specify either a template or a containers option, but not both');
460
+ }
461
+
462
+ // The 'containers' option overrides 'template'.
463
+ if (opts.containers) {
464
+ opts.template = null;
465
+ }
466
+
467
+ if (opts.logger && !Utils.isFunction(opts.logger)) {
468
+ // eslint-disable-next-line max-len
469
+ return new TypeError('Peaks.init(): The logger option should be a function');
470
+ }
471
+
472
+ if (opts.segments && !Array.isArray(opts.segments)) {
473
+ // eslint-disable-next-line max-len
474
+ return new TypeError('Peaks.init(): options.segments must be an array of segment objects');
475
+ }
476
+
477
+ if (opts.points && !Array.isArray(opts.points)) {
478
+ // eslint-disable-next-line max-len
479
+ return new TypeError('Peaks.init(): options.points must be an array of point objects');
480
+ }
481
+
482
+ Utils.extend(this.options, opts);
483
+
484
+ if (!Array.isArray(this.options.zoomRange)) {
485
+ return new TypeError('Peaks.init(): The zoomRange option should be an array');
486
+ }
487
+ else if (this.options.zoomRange.length === 0) {
488
+ return new Error('Peaks.init(): The zoomRange array must not be empty');
489
+ }
490
+ else {
491
+ if (!Utils.isInAscendingOrder(this.options.zoomRange)) {
492
+ return new Error('Peaks.init(): The zoomRange array must be sorted in ascending order');
493
+ }
494
+ }
495
+
496
+ if (opts.logger) {
497
+ this.logger = opts.logger;
498
+ }
499
+
500
+ return null;
501
+ };
502
+
503
+ /**
504
+ * Remote waveform data options for [Peaks.setSource]{@link Peaks#setSource}.
505
+ *
506
+ * @typedef {Object} RemoteWaveformDataOptions
507
+ * @global
508
+ * @property {String=} arraybuffer
509
+ * @property {String=} json
510
+ */
511
+
512
+ /**
513
+ * Local waveform data options for [Peaks.setSource]{@link Peaks#setSource}.
514
+ *
515
+ * @typedef {Object} LocalWaveformDataOptions
516
+ * @global
517
+ * @property {ArrayBuffer=} arraybuffer
518
+ * @property {Object=} json
519
+ */
520
+
521
+ /**
522
+ * Web Audio options for [Peaks.setSource]{@link Peaks#setSource}.
523
+ *
524
+ * @typedef {Object} WebAudioOptions
525
+ * @global
526
+ * @property {AudioContext} audioContext
527
+ * @property {AudioBuffer=} audioBuffer
528
+ * @property {Boolean=} multiChannel
529
+ */
530
+
531
+ /**
532
+ * Options for [Peaks.setSource]{@link Peaks#setSource}.
533
+ *
534
+ * @typedef {Object} PeaksSetSourceOptions
535
+ * @global
536
+ * @property {String} mediaUrl
537
+ * @property {RemoteWaveformDataOptions=} dataUri
538
+ * @property {LocalWaveformDataOptions=} waveformData
539
+ * @property {WebAudioOptions=} webAudio
540
+ * @property {Boolean=} withCredentials
541
+ * @property {Array<Number>=} zoomRange
542
+ */
543
+
544
+ /**
545
+ * Source for [Peaks.setSource]{@link Peaks#setSource}.
546
+ *
547
+ * @typedef {Object} RemoteWaveformDataOptions
548
+ * @global
549
+ * @property {String=} arraybuffer
550
+ * @property {String=} json
551
+ */
552
+
553
+ /**
554
+ * Add a new source to the {@link Peaks} instance.
555
+ *
556
+ * @param {Object} source
557
+ */
558
+
559
+ Peaks.prototype.addSource = function(source) {
560
+ this.sources.add(source);
561
+ };
562
+
563
+ /**
564
+ * Destroy a source from the {@link Peaks} instance.
565
+ *
566
+ * @param {String} sourceId
567
+ */
568
+
569
+ Peaks.prototype.destroySource = function(sourceId) {
570
+ this.sources.destroyById(sourceId);
571
+ };
572
+
573
+ /**
574
+ * Show a source from the {@link Peaks} instance.
575
+ *
576
+ * @param {String} sourceId
577
+ */
578
+
579
+ Peaks.prototype.showSource = function(sourceId) {
580
+ this.sources.showById(sourceId);
581
+ };
582
+
583
+ /**
584
+ * Hide a source from the {@link Peaks} instance.
585
+ *
586
+ * @param {String} sourceId
587
+ */
588
+
589
+ Peaks.prototype.hideSource = function(sourceId) {
590
+ this.sources.hideById(sourceId);
591
+ };
592
+
593
+ Peaks.prototype.showSegments = function(position) {
594
+ this.segments.addSegmentsToPosition(position);
595
+ };
596
+
597
+ Peaks.prototype.setDefaultMode = function() {
598
+ this.emit('default_mode');
599
+ };
600
+
601
+ Peaks.prototype.setCutMode = function() {
602
+ this.emit('cut_mode');
603
+ };
604
+
605
+ Peaks.prototype.getVisibleSegments = function() {
606
+ return this.view
607
+ .getSegmentsGroup()
608
+ .getVisibleSegments();
609
+ };
610
+
611
+ Peaks.prototype.getDuration = function() {
612
+ return this.view.pixelsToTime(this.view.getTimelineLength());
613
+ };
614
+
615
+ Peaks.prototype.overrideInteractions = function(bool, areInteractionsAllowed) {
616
+ return this.view
617
+ .overrideInteractions(bool, areInteractionsAllowed);
618
+ };
619
+
620
+ Peaks.prototype.allowInteractions = function(forSources, forSegments) {
621
+ return this.view
622
+ .allowInteractions(forSources, forSegments);
623
+ };
624
+
625
+ Peaks.prototype._addWindowResizeHandler = function() {
626
+ this._onResize = this._onResize.bind(this);
627
+ window.addEventListener('resize', this._onResize);
628
+ };
629
+
630
+ Peaks.prototype._onResize = function() {
631
+ this.emit('window_resize');
632
+ };
633
+
634
+ Peaks.prototype._removeWindowResizeHandler = function() {
635
+ window.removeEventListener('resize', this._onResize);
636
+ };
637
+
638
+ /**
639
+ * Cleans up a Peaks instance after use.
640
+ */
641
+
642
+ Peaks.prototype.destroy = function() {
643
+ this._removeWindowResizeHandler();
644
+
645
+ if (this.keyboardHandler) {
646
+ this.keyboardHandler.destroy();
647
+ }
648
+
649
+ if (this.view) {
650
+ this.view.destroy();
651
+ }
652
+
653
+ if (this.player) {
654
+ this.player.destroy();
655
+ }
656
+
657
+ if (this._cueEmitter) {
658
+ this._cueEmitter.destroy();
659
+ }
660
+ };
661
+
662
+ return Peaks;
663
+ });