UNPKG

@checksub_team/peaks_timeline

Version:

JavaScript UI component for displaying audio waveforms

829 lines (679 loc) 21.2 kB
/** * @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; });