@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.
@@ -0,0 +1,458 @@
1
+ /**
2
+ * @file
3
+ *
4
+ * Defines the {@link WaveformBuilder} class.
5
+ *
6
+ * @module waveform-builder
7
+ */
8
+
9
+ define([
10
+ 'waveform-data',
11
+ './utils'
12
+ ], function(
13
+ WaveformData,
14
+ Utils) {
15
+ 'use strict';
16
+
17
+ var isXhr2 = ('withCredentials' in new XMLHttpRequest());
18
+
19
+ /**
20
+ * Creates and returns a WaveformData object, either by requesting the
21
+ * waveform data from the server, or by creating the waveform data using the
22
+ * Web Audio API.
23
+ *
24
+ * @class
25
+ * @alias WaveformBuilder
26
+ *
27
+ * @param {Peaks} peaks
28
+ */
29
+
30
+ function WaveformBuilder(peaks) {
31
+ this._peaks = peaks;
32
+ }
33
+
34
+ /**
35
+ * Options for requesting remote waveform data.
36
+ *
37
+ * @typedef {Object} RemoteWaveformDataOptions
38
+ * @global
39
+ * @property {String=} arraybuffer
40
+ * @property {String=} json
41
+ */
42
+
43
+ /**
44
+ * Options for supplying local waveform data.
45
+ *
46
+ * @typedef {Object} LocalWaveformDataOptions
47
+ * @global
48
+ * @property {ArrayBuffer=} arraybuffer
49
+ * @property {Object=} json
50
+ */
51
+
52
+ /**
53
+ * Options for the Web Audio waveform builder.
54
+ *
55
+ * @typedef {Object} WaveformBuilderWebAudioOptions
56
+ * @global
57
+ * @property {AudioContext} audioContext
58
+ * @property {AudioBuffer=} audioBuffer
59
+ * @property {Number=} scale
60
+ * @property {Boolean=} multiChannel
61
+ */
62
+
63
+ /**
64
+ * Options for [WaveformBuilder.init]{@link WaveformBuilder#init}.
65
+ *
66
+ * @typedef {Object} WaveformBuilderInitOptions
67
+ * @global
68
+ * @property {RemoteWaveformDataOptions=} dataUri
69
+ * @property {LocalWaveformDataOptions=} waveformData
70
+ * @property {WaveformBuilderWebAudioOptions=} webAudio
71
+ * @property {Boolean=} withCredentials
72
+ * @property {Array<Number>=} zoomLevels
73
+ */
74
+
75
+ /**
76
+ * Callback for receiving the waveform data.
77
+ *
78
+ * @callback WaveformBuilderInitCallback
79
+ * @global
80
+ * @param {Error} error
81
+ * @param {WaveformData} waveformData
82
+ */
83
+
84
+ /**
85
+ * Loads or creates the waveform data.
86
+ *
87
+ * @private
88
+ * @param {WaveformBuilderInitOptions} options
89
+ * @param {WaveformBuilderInitCallback} callback
90
+ */
91
+
92
+ WaveformBuilder.prototype.init = function(options, callback) {
93
+ if ((options.dataUri && (options.webAudio || options.audioContext)) ||
94
+ (options.waveformData && (options.webAudio || options.audioContext)) ||
95
+ (options.dataUri && options.waveformData)) {
96
+ // eslint-disable-next-line max-len
97
+ callback(new TypeError('Peaks.init(): You may only pass one source (webAudio, dataUri, or waveformData) to render waveform data.'));
98
+ return;
99
+ }
100
+
101
+ if (options.audioContext) {
102
+ // eslint-disable-next-line max-len
103
+ this._peaks.options.deprecationLogger('Peaks.init(): The audioContext option is deprecated, please pass a webAudio object instead');
104
+
105
+ options.webAudio = {
106
+ audioContext: options.audioContext
107
+ };
108
+ }
109
+
110
+ if (options.objectUrl) {
111
+ return this._buildWaveformDataFromObjectUrl(options, callback);
112
+ }
113
+ else if (options.dataUri) {
114
+ return this._getRemoteWaveformData(options, callback);
115
+ }
116
+ else if (options.waveformData) {
117
+ return this._buildWaveformFromLocalData(options, callback);
118
+ }
119
+ else if (options.webAudio) {
120
+ if (options.webAudio.audioBuffer) {
121
+ return this._buildWaveformDataFromAudioBuffer(options, callback);
122
+ }
123
+ else {
124
+ return this._buildWaveformDataUsingWebAudio(options, callback);
125
+ }
126
+ }
127
+ else {
128
+ // eslint-disable-next-line max-len
129
+ callback(new Error('Peaks.init(): You must pass an audioContext, or dataUri, or waveformData to render waveform data'));
130
+ }
131
+ };
132
+
133
+ /* eslint-disable max-len */
134
+
135
+ /**
136
+ * Fetches waveform data, based on the given options.
137
+ *
138
+ * @private
139
+ * @param {Object} options
140
+ * @param {String|Object} options.dataUri
141
+ * @param {String} options.dataUri.arraybuffer Waveform data URL
142
+ * (binary format)
143
+ * @param {String} options.dataUri.json Waveform data URL (JSON format)
144
+ * @param {String} options.defaultUriFormat Either 'arraybuffer' (for binary
145
+ * data) or 'json'
146
+ * @param {WaveformBuilderInitCallback} callback
147
+ *
148
+ * @see Refer to the <a href="https://github.com/bbc/audiowaveform/blob/master/doc/DataFormat.md">data format documentation</a>
149
+ * for details of the binary and JSON waveform data formats.
150
+ */
151
+
152
+ /* eslint-enable max-len */
153
+
154
+ WaveformBuilder.prototype._getRemoteWaveformData = function(options, callback) {
155
+ var self = this;
156
+ var dataUri = null;
157
+ var requestType = null;
158
+ var url;
159
+
160
+ if (Utils.isObject(options.dataUri)) {
161
+ dataUri = options.dataUri;
162
+ }
163
+ else if (Utils.isString(options.dataUri)) {
164
+ // Backward compatibility
165
+ dataUri = {};
166
+ dataUri[options.dataUriDefaultFormat || 'json'] = options.dataUri;
167
+ }
168
+ else {
169
+ callback(new TypeError('Peaks.init(): The dataUri option must be an object'));
170
+ return;
171
+ }
172
+
173
+ ['ArrayBuffer', 'JSON'].some(function(connector) {
174
+ if (window[connector]) {
175
+ requestType = connector.toLowerCase();
176
+ url = dataUri[requestType];
177
+
178
+ return Boolean(url);
179
+ }
180
+ });
181
+
182
+ if (!url) {
183
+ // eslint-disable-next-line max-len
184
+ callback(new Error('Peaks.init(): Unable to determine a compatible dataUri format for this browser'));
185
+ return;
186
+ }
187
+
188
+ var xhr = self._createXHR(url, requestType, options.withCredentials, function(event) {
189
+ if (this.readyState !== 4) {
190
+ return;
191
+ }
192
+
193
+ if (this.status !== 200) {
194
+ callback(
195
+ new Error('Unable to fetch remote data. HTTP status ' + this.status)
196
+ );
197
+
198
+ return;
199
+ }
200
+
201
+ var waveformData = WaveformData.create(event.target.response);
202
+
203
+ if (waveformData.channels !== 1 && waveformData.channels !== 2) {
204
+ callback(new Error('Peaks.init(): Only mono or stereo waveforms are currently supported'));
205
+ return;
206
+ }
207
+
208
+ callback(null, waveformData);
209
+ },
210
+ function() {
211
+ callback(new Error('XHR Failed'));
212
+ });
213
+
214
+ xhr.send();
215
+ };
216
+
217
+ /* eslint-disable max-len */
218
+
219
+ /**
220
+ * Creates a waveform from given data, based on the given options.
221
+ *
222
+ * @private
223
+ * @param {Object} options
224
+ * @param {Object} options.waveformData
225
+ * @param {ArrayBuffer} options.waveformData.arraybuffer Waveform data (binary format)
226
+ * @param {Object} options.waveformData.json Waveform data (JSON format)
227
+ * @param {WaveformBuilderInitCallback} callback
228
+ *
229
+ * @see Refer to the <a href="https://github.com/bbc/audiowaveform/blob/master/doc/DataFormat.md">data format documentation</a>
230
+ * for details of the binary and JSON waveform data formats.
231
+ */
232
+
233
+ /* eslint-enable max-len */
234
+
235
+ WaveformBuilder.prototype._buildWaveformFromLocalData = function(options, callback) {
236
+ var waveformData = null;
237
+ var data = null;
238
+
239
+ if (Utils.isObject(options.waveformData)) {
240
+ waveformData = options.waveformData;
241
+ }
242
+ else {
243
+ callback(new Error('Peaks.init(): The waveformData option must be an object'));
244
+ return;
245
+ }
246
+
247
+ if (Utils.isObject(waveformData.json)) {
248
+ data = waveformData.json;
249
+ }
250
+ else if (Utils.isArrayBuffer(waveformData.arraybuffer)) {
251
+ data = waveformData.arraybuffer;
252
+ }
253
+
254
+ if (!data) {
255
+ // eslint-disable-next-line max-len
256
+ callback(new Error('Peaks.init(): Unable to determine a compatible waveformData format'));
257
+ return;
258
+ }
259
+
260
+ try {
261
+ var createdWaveformData = WaveformData.create(data);
262
+
263
+ if (createdWaveformData.channels !== 1 && createdWaveformData.channels !== 2) {
264
+ callback(new Error('Peaks.init(): Only mono or stereo waveforms are currently supported'));
265
+ return;
266
+ }
267
+
268
+ callback(null, createdWaveformData);
269
+ }
270
+ catch (err) {
271
+ callback(err);
272
+ }
273
+ };
274
+
275
+ /**
276
+ * Creates waveform data using the Web Audio API.
277
+ *
278
+ * @private
279
+ * @param {Object} options
280
+ * @param {AudioContext} options.webAudio.audioContext
281
+ * @param {WaveformBuilderInitCallback} callback
282
+ */
283
+
284
+ WaveformBuilder.prototype._buildWaveformDataUsingWebAudio = function(options, callback) {
285
+ var self = this;
286
+
287
+ var audioContext = window.AudioContext || window.webkitAudioContext;
288
+
289
+ if (!(options.webAudio.audioContext instanceof audioContext)) {
290
+ // eslint-disable-next-line max-len
291
+ callback(new TypeError('Peaks.init(): The webAudio.audioContext option must be a valid AudioContext'));
292
+ return;
293
+ }
294
+
295
+ var webAudioOptions = options.webAudio;
296
+
297
+ if (webAudioOptions.scale !== options.zoomLevels[0]) {
298
+ webAudioOptions.scale = options.zoomLevels[0];
299
+ }
300
+
301
+ // If the media element has already selected which source to play, its
302
+ // currentSrc attribute will contain the source media URL. Otherwise,
303
+ // we wait for a canplay event to tell us when the media is ready.
304
+
305
+ var mediaSourceUrl = self._peaks.player.getCurrentSource();
306
+
307
+ if (mediaSourceUrl) {
308
+ self._requestAudioAndBuildWaveformData(
309
+ mediaSourceUrl,
310
+ webAudioOptions,
311
+ options.withCredentials,
312
+ callback
313
+ );
314
+ }
315
+ else {
316
+ self._peaks.once('player_canplay', function(player) {
317
+ self._requestAudioAndBuildWaveformData(
318
+ player.getCurrentSource(),
319
+ webAudioOptions,
320
+ options.withCredentials,
321
+ callback
322
+ );
323
+ });
324
+ }
325
+ };
326
+
327
+ /**
328
+ * Creates waveform data using the Web Audio API.
329
+ *
330
+ * @private
331
+ * @param {Object} options
332
+ * @param {WaveformBuilderInitCallback} callback
333
+ */
334
+
335
+ WaveformBuilder.prototype._buildWaveformDataFromObjectUrl = function(options, callback) {
336
+ var self = this;
337
+
338
+ var webAudioOptions = {
339
+ scale: options.minScale
340
+ };
341
+
342
+ webAudioOptions.audioContext = new window.AudioContext();
343
+
344
+ if (options.objectUrl) {
345
+ self._requestAudioAndBuildWaveformData(
346
+ options.objectUrl,
347
+ webAudioOptions,
348
+ options.withCredentials,
349
+ callback
350
+ );
351
+ }
352
+ };
353
+
354
+ WaveformBuilder.prototype._buildWaveformDataFromAudioBuffer = function(options, callback) {
355
+ var webAudioOptions = options.webAudio;
356
+
357
+ if (webAudioOptions.scale !== options.zoomLevels[0]) {
358
+ webAudioOptions.scale = options.zoomLevels[0];
359
+ }
360
+
361
+ var webAudioBuilderOptions = {
362
+ audio_buffer: webAudioOptions.audioBuffer,
363
+ split_channels: webAudioOptions.multiChannel,
364
+ scale: webAudioOptions.scale
365
+ };
366
+
367
+ WaveformData.createFromAudio(webAudioBuilderOptions, callback);
368
+ };
369
+
370
+ /**
371
+ * Fetches the audio content, based on the given options, and creates waveform
372
+ * data using the Web Audio API.
373
+ *
374
+ * @private
375
+ * @param {url} The media source URL
376
+ * @param {WaveformBuilderWebAudioOptions} webAudio
377
+ * @param {Boolean} withCredentials
378
+ * @param {WaveformBuilderInitCallback} callback
379
+ */
380
+
381
+ WaveformBuilder.prototype._requestAudioAndBuildWaveformData = function(url,
382
+ webAudio, withCredentials, callback) {
383
+ var self = this;
384
+
385
+ if (!url) {
386
+ self._peaks.logger('Peaks.init(): The mediaElement src is invalid');
387
+ return;
388
+ }
389
+
390
+ var xhr = self._createXHR(url, 'arraybuffer', withCredentials, function(event) {
391
+ if (this.readyState !== 4) {
392
+ return;
393
+ }
394
+
395
+ if (this.status !== 200) {
396
+ callback(
397
+ new Error('Unable to fetch remote data. HTTP status ' + this.status)
398
+ );
399
+
400
+ return;
401
+ }
402
+
403
+ var webAudioBuilderOptions = {
404
+ audio_context: webAudio.audioContext,
405
+ array_buffer: event.target.response,
406
+ split_channels: webAudio.multiChannel,
407
+ scale: webAudio.scale
408
+ };
409
+
410
+ WaveformData.createFromAudio(webAudioBuilderOptions, callback);
411
+ },
412
+ function() {
413
+ callback(new Error('XHR Failed'));
414
+ });
415
+
416
+ xhr.send();
417
+ };
418
+
419
+ /**
420
+ * @private
421
+ * @param {String} url
422
+ * @param {String} requestType
423
+ * @param {Boolean} withCredentials
424
+ * @param {Function} onLoad
425
+ * @param {Function} onError
426
+ *
427
+ * @returns {XMLHttpRequest}
428
+ */
429
+
430
+ WaveformBuilder.prototype._createXHR = function(url, requestType,
431
+ withCredentials, onLoad, onError) {
432
+ var xhr = new XMLHttpRequest();
433
+
434
+ // open an XHR request to the data source file
435
+ xhr.open('GET', url, true);
436
+
437
+ if (isXhr2) {
438
+ try {
439
+ xhr.responseType = requestType;
440
+ }
441
+ catch (e) {
442
+ // Some browsers like Safari 6 do handle XHR2 but not the json
443
+ // response type, doing only a try/catch fails in IE9
444
+ }
445
+ }
446
+
447
+ xhr.onload = onLoad;
448
+ xhr.onerror = onError;
449
+
450
+ if (isXhr2 && withCredentials) {
451
+ xhr.withCredentials = true;
452
+ }
453
+
454
+ return xhr;
455
+ };
456
+
457
+ return WaveformBuilder;
458
+ });
@@ -0,0 +1,223 @@
1
+ /**
2
+ * @file
3
+ *
4
+ * Defines the {@link WaveformShape} class.
5
+ *
6
+ * @module waveform-shape
7
+ */
8
+
9
+ define(['./utils', 'konva'], function(Utils, Konva) {
10
+ 'use strict';
11
+
12
+ /**
13
+ * Scales the waveform data for drawing on a canvas context.
14
+ *
15
+ * @param {Number} amplitude The waveform data point amplitude.
16
+ * @param {Number} height The height of the waveform, in pixels.
17
+ * @param {Number} scale Amplitude scaling factor.
18
+ * @returns {Number} The scaled waveform data point.
19
+ */
20
+
21
+ function scaleY(amplitude, height, scale) {
22
+ var range = 256;
23
+ var offset = 128;
24
+
25
+ var scaledAmplitude = (amplitude * scale + offset) * height / range;
26
+
27
+ return height - Utils.clamp(height - scaledAmplitude, 0, height);
28
+ }
29
+
30
+ /**
31
+ * Waveform shape options.
32
+ *
33
+ * @typedef {Object} WaveformShapeOptions
34
+ * @global
35
+ * @property {String} color Waveform color.
36
+ * @property {WaveformOverview|WaveformZoomView} view The view object
37
+ * that contains the waveform shape.
38
+ * @property {Segment?} segment If given, render a waveform image
39
+ * covering the segment's time range. Otherwise, render the entire
40
+ * waveform duration.
41
+ */
42
+
43
+ /**
44
+ * Creates a Konva.Shape object that renders a waveform image.
45
+ *
46
+ * @class
47
+ * @alias WaveformShape
48
+ *
49
+ * @param {WaveformShapeOptions} options
50
+ */
51
+
52
+ function WaveformShape(options) {
53
+ Konva.Shape.call(this, {
54
+ fill: options.color
55
+ });
56
+
57
+ this._view = options.view;
58
+ this._source = options.source;
59
+ this._originalWaveformData = options.waveformData;
60
+ this._waveformData = options.waveformData;
61
+ this._height = options.height;
62
+
63
+ this.rescale();
64
+
65
+ this.sceneFunc(this._sceneFunc);
66
+
67
+ this.hitFunc(this._waveformShapeHitFunc);
68
+ }
69
+
70
+ WaveformShape.prototype = Object.create(Konva.Shape.prototype);
71
+
72
+ WaveformShape.prototype.rescale = function() {
73
+ this._waveformData = this._originalWaveformData.resample({
74
+ scale: this._waveformData.sample_rate / this._view.getTimeToPixelsScale()
75
+ });
76
+ };
77
+
78
+ WaveformShape.prototype.setWaveformColor = function(color) {
79
+ this.fill(color);
80
+ };
81
+
82
+ WaveformShape.prototype._sceneFunc = function(context) {
83
+ var width = this._view.getWidth();
84
+
85
+ var startPixels = 0, startOffset = 0;
86
+
87
+ if (this._source) {
88
+ startPixels = this._view.timeToPixels(this._source.mediaStartTime) + Math.max(
89
+ this._view.getFrameOffset() - this._view.timeToPixels(this._source.startTime),
90
+ 0
91
+ );
92
+
93
+ startOffset = this._view.timeToPixels(this._source.mediaStartTime);
94
+ }
95
+
96
+ var endPixels = width;
97
+
98
+ if (this._source) {
99
+ endPixels = Math.min(
100
+ this._view.timeToPixels(this._source.mediaEndTime) - Math.max(
101
+ this._view.timeToPixels(this._source.endTime)
102
+ - this._view.getFrameOffset()
103
+ - this._view.getWidth(),
104
+ 0
105
+ ),
106
+ this._waveformData.length
107
+ );
108
+ }
109
+
110
+ this._drawWaveform(
111
+ context,
112
+ this._waveformData,
113
+ startPixels,
114
+ startOffset,
115
+ endPixels,
116
+ this._height
117
+ );
118
+ };
119
+
120
+ /**
121
+ * Draws a waveform on a canvas context.
122
+ *
123
+ * @param {Konva.Context} context The canvas context to draw on.
124
+ * @param {WaveformData} waveformData The waveform data to draw.
125
+ * @param {Number} frameOffset The start position of the waveform shown
126
+ * in the view, in pixels.
127
+ * @param {Number} startPixels The start position of the waveform to draw,
128
+ * in pixels.
129
+ * @param {Number} endPixels The end position of the waveform to draw,
130
+ * in pixels.
131
+ * @param {Number} width The width of the waveform area, in pixels.
132
+ * @param {Number} height The height of the waveform area, in pixels.
133
+ */
134
+
135
+ WaveformShape.prototype._drawWaveform = function(context, waveformData,
136
+ startPixels, startOffset, endPixels, height) {
137
+ var channels = waveformData.channels;
138
+
139
+ var waveformTop = 0;
140
+ var waveformHeight = Math.floor(height / channels);
141
+
142
+ for (var i = 0; i < channels; i++) {
143
+ if (i === channels - 1) {
144
+ waveformHeight = height - (channels - 1) * waveformHeight;
145
+ }
146
+
147
+ this._drawChannel(
148
+ context,
149
+ waveformData.channel(i),
150
+ startPixels,
151
+ startOffset,
152
+ endPixels,
153
+ waveformTop,
154
+ waveformHeight
155
+ );
156
+
157
+ waveformTop += waveformHeight;
158
+ }
159
+ };
160
+
161
+ WaveformShape.prototype._drawChannel = function(context, channel,
162
+ startPixels, startOffset, endPixels, top, height) {
163
+ var x, val;
164
+
165
+ var amplitudeScale = this._view.getAmplitudeScale();
166
+
167
+ context.beginPath();
168
+
169
+ for (x = startPixels; x < endPixels; x++) {
170
+ val = channel.min_sample(x);
171
+
172
+ context.lineTo(x - startOffset + 0.5, top + scaleY(val, height, amplitudeScale) + 0.5);
173
+ }
174
+
175
+ for (x = endPixels - 1; x >= startPixels; x--) {
176
+ val = channel.max_sample(x);
177
+
178
+ context.lineTo(x - startOffset + 0.5, top + scaleY(val, height, amplitudeScale) + 0.5);
179
+ }
180
+
181
+ context.closePath();
182
+
183
+ context.fillShape(this);
184
+ };
185
+
186
+ WaveformShape.prototype._waveformShapeHitFunc = function(context) {
187
+ if (!this._source) {
188
+ return;
189
+ }
190
+
191
+ var frameOffset = this._view.getFrameOffset();
192
+ var viewWidth = this._view.getWidth();
193
+
194
+ var startPixels = this._view.timeToPixels(this._source.startTime);
195
+ var endPixels = this._view.timeToPixels(this._source.endTime);
196
+
197
+ var offsetY = 10;
198
+ var hitRectHeight = this._height;
199
+
200
+ if (hitRectHeight < 0) {
201
+ hitRectHeight = 0;
202
+ }
203
+
204
+ var hitRectLeft = startPixels - frameOffset;
205
+ var hitRectWidth = endPixels - startPixels;
206
+
207
+ if (hitRectLeft < 0) {
208
+ hitRectWidth -= -hitRectLeft;
209
+ hitRectLeft = 0;
210
+ }
211
+
212
+ if (hitRectLeft + hitRectWidth > viewWidth) {
213
+ hitRectWidth -= hitRectLeft + hitRectWidth - viewWidth;
214
+ }
215
+
216
+ context.beginPath();
217
+ context.rect(hitRectLeft, offsetY, hitRectWidth, hitRectHeight);
218
+ context.closePath();
219
+ context.fillStrokeShape(this);
220
+ };
221
+
222
+ return WaveformShape;
223
+ });