@checksub_team/peaks_timeline
Version:
JavaScript UI component for displaying audio waveforms
829 lines (679 loc) • 21.2 kB
JavaScript
/**
* @file
*
* Defines the {@link Peaks} class.
*
* @module main
*/
define([
'colors.css',
'eventemitter2',
'./timeline-segments',
'./timeline-sources',
'./keyboard-handler',
'./player',
'./marker-factories',
'./timeline-zoomview',
'./utils'
], function(
Colors,
EventEmitter,
TimelineSegments,
TimelineSources,
KeyboardHandler,
Player,
MarkerFactories,
TimelineZoomView,
Utils) {
'use strict';
/**
* Initialises a new Peaks instance with default option settings.
*
* @class
* @alias Peaks
*
* @param {Object} opts Configuration options
*/
function Peaks() {
EventEmitter.call(this, { wildcard: true });
this.options = {
/**
* Array of scale factors (samples per pixel) for the zoom levels
* (big >> small)
*/
zoomRange: [5, 2000],
initialZoomLevel: 100,
minScale: 40,
/**
* Data URI where to get the waveform data.
*
* If a string, we assume that `this.dataUriDefaultFormat` is the default
* `xhr.responseType` value.
*
* @since 0.0.1
*
* ```js
* dataUri: 'url/to/data.json?waveformId=1337'
* ```
*
* If an object, each key is an `xhr.responseType` which will contain its
* associated source URI.
*
* @since 0.3.0
*
* ```js
* dataUri: {
* arraybuffer: 'url/to/data.dat',
* json: 'url/to/data.json'
* }
* ```
*/
dataUri: null,
objectUrl: null,
/**
* Will be used as a `xhr.responseType` if `dataUri` is a string, and not
* an object. Here for backward compatibility purpose only.
*
* @since 0.3.0
*/
dataUriDefaultFormat: 'json',
/**
* If true, all ajax requests (e.g. to fetch waveform data) will be made
* with credentials (i.e. browser-controlled cookies).
*
* @type {Boolean}
*/
withCredentials: false,
/**
* Will report errors to that function
*
* @type {Function=}
* @since 0.4.4
*/
logger: null,
/**
* Deprecation messages logger.
*
* @type {Function}
* @since 0.4.8
*/
// eslint-disable-next-line no-console
deprecationLogger: console.log.bind(console),
/**
* Bind keyboard controls
*/
keyboard: false,
/**
* Keyboard nudge increment in seconds (left arrow/right arrow)
*/
nudgeIncrement: 1.0,
/**
* Colour for the zoomed in waveform
*/
zoomWaveformColor: 'rgba(180, 180, 180, 1)',
/**
* Random colour per segment (overrides segmentColor)
*/
randomizeSegmentColor: true,
/**
* Block mouse clicks if a meta key is pressed
*/
blockUpdatingOnMouseClickWithMetaKey: false,
/**
* Default height of the waveform canvases in pixels.
* The height of the canvas might grow depending on the number of line inside.
*/
height: 200,
/**
* Colour for segments on the waveform
*/
segmentColor: Colors.orange,
/**
* Colour of the play head
*/
playheadColor: Colors.red,
/**
* Colour of the play head text
*/
playheadTextColor: Colors.red,
/**
* Show current time position by the play head marker
* (zoom view only)
*/
showPlayheadTime: true,
/**
* Show segment markers, allowing to resize a segment by dragging its handles
*/
showSegmentMarkers: true,
/**
* Colour of the axis gridlines
*/
axisGridlineColor: '#ccc',
/**
* Colour of the axis labels
*/
axisLabelColor: Colors.gray,
/**
*
*/
template: [
'<div class="timeline">',
'<div class="zoom-container"></div>',
'</div>'
].join(''),
/**
* An object containing an AudioContext, used when creating waveform data
* using the Web Audio API
*/
webAudio: null,
/**
* Point/Segment marker customisation.
*
* @todo This part of the API is not stable.
*/
createSegmentMarker: MarkerFactories.createSegmentMarker,
createSegmentLabel: MarkerFactories.createSegmentLabel,
/**
* External sources information.
*
* ```js
* sources: {
* id: 'unique-identifier-for-this-source',
* title: 'Source #1',
* url: 'https://my-website/my-resource.mp3',
* dataUri: {
* arraybuffer: 'url/to/data.dat',
* json: 'url/to/data.json'
* },
* start: 1.03,
* end: 5.06,
* color: #0000ff,
* wrapped: false, //show only a line or peaks/preview
* position: 0 //position in the timeline (here on the first line)
* }
* ```
*/
sources: null,
/**
* Height of a line, in pixels.
* This height will correspond to the height of the background when the element is unwrapped.
*/
lineHeight: 80,
/**
* Height of a segment, in pixels.
*/
segmentHeight: 32,
/**
* Height of a line, in pixels.
* This height will correspond to the height of the group
* containing the line and title of the wrapped source.
*/
wrappedLineHeight: 30,
/**
* Height of an empty line, in pixels.
*/
emptyLineHeight: 10,
/**
* Empty space between 2 sources, in pixels.
*/
interline: 10,
/**
* Empty space before the first source and after the last source, in pixels.
*/
padding: 30,
/**
* Horizontal empty space after the longest source, in pixels.
*/
horizontalPadding: 0,
/**
* Threshold around a segment where the dragged segment is magnetized
* toward the former, in pixels.
*/
segmentMagnetThreshold: 15,
/**
* Enable or disable the timeline vertical scrolling
*/
enableVerticalScrolling: true,
/**
* Width of the left line indicator, in pixels
*/
lineIndicatorWidth: 20,
/**
* Size of the line indicators' icon, in pixels
*/
lineIndicatorDefaultIconSize: 19,
/**
* Size of the line indicators' volume icon, in pixels
*/
lineIndicatorVolumeIconSize: 24,
/**
* Size of the line indicators' no volume, in pixels
*/
lineIndicatorNoVolumeIconSize: 24,
/**
* Size of the line indicators' visibility, in pixels
*/
lineIndicatorVisibilityIconSize: 24,
/**
* Size of the line indicators' no visibility icon, in pixels
*/
lineIndicatorNoVisibilityIconSize: 24,
/**
* Font of the line indicators' text
*/
lineIndicatorFont: 'Arial',
/**
* Font size of the line indicators' text, in pixels
*/
lineIndicatorFontSize: 10,
/**
* Color of the line indicators' text
*/
lineIndicatorTextColor: '#8A8F98',
/**
* Color of the line indicators' icon
*/
lineIndicatorIconColor: '#8A8F98',
/**
* Color for the indicator's text when selected
*/
lineIndicatorSelectedTextColor: '#ccc',
/**
* Color for the indicator's icon when selected
*/
lineIndicatorSelectedIconColor: '#ccc',
/**
* Border width of a source when selected
*/
sourceSelectedBorderWidth: 3,
/**
* Handle width for a source, in pixels
*/
sourceHandleWidth: 12,
/**
* X Offset of the text in the source, in pixels
*/
sourceTextXOffset: 10,
/**
* Y Offset of the text in the source, in pixels
*/
sourceTextYOffset: 10,
/**
* X Offset of the source indicators, in pixels
* This is the offset of the source indicators
* from the left side of the source
*/
sourceIndicatorsXOffset: 8,
/**
* Y Offset of the source indicators, in pixels
* This is the offset of the source indicators
* from the top of the source
*/
sourceIndicatorsYOffset: 12,
/**
* Spacing between the source buttons, in pixels
*/
sourceButtonsGap: 2,
/**
* Padding around the source buttons, in pixels
*/
sourceButtonsPadding: 4,
/**
* X Offset of the source buttons, in pixels
*/
sourceButtonsXOffset: 8,
/**
* Y Offset of the source buttons, in pixels
*/
sourceButtonsYOffset: 0,
/**
* Threshold size on the left and right border of the view,
* where auto scrolling is activated, between 0 and 1.
*/
autoScrollThreshold: 0.05,
/**
* Indicates whether or not the context menu should be displayed
* on right click in the line indicator.
*/
enableLineIndicatorContextMenu: true,
/**
* The minimal size of a source, in seconds
*/
minSourceSize: 0.05,
/**
* The minimal size of a segment, in seconds
*/
minSegmentSize: 0.2,
/**
* Indicates whether or not sources can be dragged
* from one line to another
*/
canMoveSourcesBetweenLines: true
};
/**
* Asynchronous errors logger.
*
* @type {Function}
*/
// eslint-disable-next-line no-console
this.logger = console.error.bind(console);
return this;
}
Peaks.prototype = Object.create(EventEmitter.prototype);
/**
* Creates and initialises a new Peaks instance with the given options.
*
* @param {Object} opts Configuration options
*
* @return {Peaks}
*/
Peaks.init = function(opts, callback) {
var instance = new Peaks();
opts = opts || {};
var err = instance._setOptions(opts);
if (err) {
callback(err);
return;
}
/*
Setup the fonts
*/
var fonts = [
'https://fonts.gstatic.com/s/opensans/v27/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4kaVIGxA.woff2',
'https://fonts.gstatic.com/s/opensans/v27/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4jaVIGxA.woff2',
'https://fonts.gstatic.com/s/opensans/v27/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4iaVIGxA.woff2',
'https://fonts.gstatic.com/s/opensans/v27/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4vaVIGxA.woff2',
'https://fonts.gstatic.com/s/opensans/v27/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4gaVI.woff2'
];
fonts.forEach(function(fontUrl) {
var fontFace = new FontFace(
'Open Sans',
'url(' + fontUrl + ')'
);
document.fonts.add(fontFace);
fontFace.load();
});
/*
Setup the layout
*/
if (!instance.options.containers) {
callback(new TypeError('Peaks.init(): The containers option must be a valid DOM object'));
return;
}
var zoomviewContainer = instance.options.containers.zoomview;
if (!Utils.isHTMLElement(zoomviewContainer)) {
callback(new TypeError('Peaks.init(): The containers options must be valid HTML elements'));
return;
}
if (zoomviewContainer && zoomviewContainer.clientWidth <= 0) {
callback(new TypeError('Peaks.init(): Please ensure that the container is visible and has non-zero width'));
return;
}
instance.player = new Player(instance);
instance.segments = new TimelineSegments(instance);
instance.sources = new TimelineSources(instance);
// Setup the UI components
instance.view = new TimelineZoomView(
zoomviewContainer,
instance
);
if (instance.options.keyboard) {
instance.keyboardHandler = new KeyboardHandler(instance);
}
instance._addWindowResizeHandler();
if (instance.options.sources) {
instance.sources.add(instance.options.sources);
}
document.fonts.ready.then(function() {
setTimeout(function() {
instance.emit('peaks.ready');
}, 0);
if (callback) {
callback(null, instance);
}
});
return instance;
};
Peaks.prototype._setOptions = function(opts) {
// eslint-disable-next-line no-console
opts.deprecationLogger = opts.deprecationLogger || console.log.bind(console);
if (opts.overviewHighlightRectangleColor) {
opts.overviewHighlightColor = opts.overviewHighlightRectangleColor;
// eslint-disable-next-line max-len
opts.deprecationLogger('Peaks.init(): The overviewHighlightRectangleColor option is deprecated, please use overviewHighlightColor instead');
}
if (opts.inMarkerColor) {
opts.segmentStartMarkerColor = opts.inMarkerColor;
// eslint-disable-next-line max-len
opts.deprecationLogger('Peaks.init(): The inMarkerColor option is deprecated, please use segmentStartMarkerColor instead');
}
if (opts.outMarkerColor) {
opts.segmentEndMarkerColor = opts.outMarkerColor;
// eslint-disable-next-line max-len
opts.deprecationLogger('Peaks.init(): The outMarkerColor option is deprecated, please use segmentEndMarkerColor instead');
}
if (!opts.container && !opts.containers) {
return new Error('Peaks.init(): Please specify either a container or containers option');
}
else if (Boolean(opts.container) === Boolean(opts.containers)) {
return new Error('Peaks.init(): Please specify either a container or containers option, but not both');
}
if (opts.template && opts.containers) {
return new Error('Peaks.init(): Please specify either a template or a containers option, but not both');
}
// The 'containers' option overrides 'template'.
if (opts.containers) {
opts.template = null;
}
if (opts.logger && !Utils.isFunction(opts.logger)) {
return new TypeError('Peaks.init(): The logger option should be a function');
}
if (opts.segments && !Array.isArray(opts.segments)) {
return new TypeError('Peaks.init(): options.segments must be an array of segment objects');
}
if (opts.points && !Array.isArray(opts.points)) {
return new TypeError('Peaks.init(): options.points must be an array of point objects');
}
Utils.extend(this.options, opts);
if (!Array.isArray(this.options.zoomRange)) {
return new TypeError('Peaks.init(): The zoomRange option should be an array');
}
else if (this.options.zoomRange.length === 0) {
return new Error('Peaks.init(): The zoomRange array must not be empty');
}
else {
if (!Utils.isInAscendingOrder(this.options.zoomRange)) {
return new Error('Peaks.init(): The zoomRange array must be sorted in ascending order');
}
}
if (opts.logger) {
this.logger = opts.logger;
}
return null;
};
/**
* Remote waveform data options for [Peaks.setSource]{@link Peaks#setSource}.
*
* @typedef {Object} RemoteWaveformDataOptions
* @global
* @property {String=} arraybuffer
* @property {String=} json
*/
/**
* Local waveform data options for [Peaks.setSource]{@link Peaks#setSource}.
*
* @typedef {Object} LocalWaveformDataOptions
* @global
* @property {ArrayBuffer=} arraybuffer
* @property {Object=} json
*/
/**
* Web Audio options for [Peaks.setSource]{@link Peaks#setSource}.
*
* @typedef {Object} WebAudioOptions
* @global
* @property {AudioContext} audioContext
* @property {AudioBuffer=} audioBuffer
* @property {Boolean=} multiChannel
*/
/**
* Options for [Peaks.setSource]{@link Peaks#setSource}.
*
* @typedef {Object} PeaksSetSourceOptions
* @global
* @property {String} mediaUrl
* @property {RemoteWaveformDataOptions=} dataUri
* @property {LocalWaveformDataOptions=} waveformData
* @property {WebAudioOptions=} webAudio
* @property {Boolean=} withCredentials
* @property {Array<Number>=} zoomRange
*/
/**
* Source for [Peaks.setSource]{@link Peaks#setSource}.
*
* @typedef {Object} RemoteWaveformDataOptions
* @global
* @property {String=} arraybuffer
* @property {String=} json
*/
/**
* Add a new source to the {@link Peaks} instance.
*
* @param {Object} source
*/
Peaks.prototype.addSource = function(source) {
this.sources.add(source);
};
/**
* Destroy a source from the {@link Peaks} instance.
*
* @param {String} sourceId
*/
Peaks.prototype.destroySource = function(sourceId, notify) {
this.sources.destroyById(sourceId, !notify);
};
/**
* Show a source from the {@link Peaks} instance.
*
* @param {String} sourceId
*/
Peaks.prototype.showSource = function(sourceId) {
this.sources.showById(sourceId);
};
/**
* Hide a source from the {@link Peaks} instance.
*
* @param {String} sourceId
*/
Peaks.prototype.hideSource = function(sourceId) {
this.sources.hideById(sourceId);
};
Peaks.prototype.showSegments = function(lineId, position) {
this.segments.addSegmentsToPosition(lineId, position);
};
/**
* Destroy a segment from the {@link Peaks} instance.
*
* @param {String} segmentId
*/
Peaks.prototype.destroySegment = function(segmentId) {
this.segments.removeById(segmentId);
};
Peaks.prototype.setDefaultMode = function() {
this.emit('default_mode');
};
Peaks.prototype.setCutMode = function() {
this.emit('cut_mode');
};
Peaks.prototype.setIndicatorType = function(linePosition, type) {
var lineId = this.view.getLineByPosition(linePosition).getId();
this.emit('lineIndicator.setType', lineId, type);
};
Peaks.prototype.setIndicatorText = function(linePosition, text) {
var lineId = this.view.getLineByPosition(linePosition).getId();
this.emit('lineIndicator.setText', lineId, text);
};
Peaks.prototype.getVisibleSegments = function() {
return this.view
.getSegmentsGroup()
.getVisibleSegments();
};
Peaks.prototype.getDuration = function() {
return this.view.pixelsToTime(this.view.getTimelineLength());
};
Peaks.prototype.overrideInteractions = function(bool, areInteractionsAllowed) {
return this.view
.overrideInteractions(bool, areInteractionsAllowed);
};
Peaks.prototype.allowInteractions = function(forSources, forSegments) {
return this.view
.allowInteractions(forSources, forSegments);
};
Peaks.prototype.getSelectedElements = function() {
return this.view
.getSelectedElements();
};
Peaks.prototype.getSourceGroupById = function(id) {
return this.view
.getSourceGroupById(id);
};
Peaks.prototype.selectSourceById = function(sourceId) {
return this.view
.selectSourceById(sourceId);
};
Peaks.prototype.selectSourcesOnLineAfter = function(lineId, time) {
return this.view
.selectSourcesOnLineAfter(lineId, time);
};
Peaks.prototype.deselectAll = function(notify) {
return this.view
.deselectAll(notify);
};
Peaks.prototype._addWindowResizeHandler = function() {
this._onResize = this._onResize.bind(this);
window.addEventListener('resize', this._onResize);
};
Peaks.prototype._onResize = function() {
this.emit('window_resize');
};
Peaks.prototype._removeWindowResizeHandler = function() {
window.removeEventListener('resize', this._onResize);
};
Peaks.prototype.setLineHeight = function(newLineHeight) {
var oldHeight = this.options.lineHeight;
this.options.lineHeight = newLineHeight;
this.emit('options.set.line_height', oldHeight);
};
Peaks.prototype.zoomIn = function() {
this.view.setZoom(
this.view.getTimeToPixelsScale() + Math.floor(this.view.getTimeToPixelsScale() / 10) + 1
);
};
Peaks.prototype.zoomOut = function() {
this.view.setZoom(
this.view.getTimeToPixelsScale() - Math.floor(this.view.getTimeToPixelsScale() / 10) + 1
);
};
Peaks.prototype.getFullHeight = function() {
return this.view.getFullHeight();
};
/**
* Cleans up a Peaks instance after use.
*/
Peaks.prototype.destroy = function() {
this._removeWindowResizeHandler();
if (this.keyboardHandler) {
this.keyboardHandler.destroy();
}
if (this.view) {
this.view.destroy();
}
if (this.player) {
this.player.destroy();
}
if (this._cueEmitter) {
this._cueEmitter.destroy();
}
};
return Peaks;
});