Files
easystream-main/f_scripts/shared/videojs/videojs.wavesurfer.js
SamiAhmed7777 0b7e2d0a5b feat: Add comprehensive documentation suite and reorganize project structure
- Created complete documentation in docs/ directory
- Added PROJECT_OVERVIEW.md with feature highlights and getting started guide
- Added ARCHITECTURE.md with system design and technical details
- Added SECURITY.md with comprehensive security implementation guide
- Added DEVELOPMENT.md with development workflows and best practices
- Added DEPLOYMENT.md with production deployment instructions
- Added API.md with complete REST API documentation
- Added CONTRIBUTING.md with contribution guidelines
- Added CHANGELOG.md with version history and migration notes
- Reorganized all documentation files into docs/ directory for better organization
- Updated README.md with proper documentation links and quick navigation
- Enhanced project structure with professional documentation standards
2025-10-21 00:39:45 -07:00

828 lines
25 KiB
JavaScript

(function (root, factory)
{
if (typeof define === 'function' && define.amd)
{
// AMD. Register as an anonymous module.
define(['videojs', 'wavesurfer'], factory);
}
else if (typeof module === 'object' && module.exports)
{
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory(require('video.js'), require('wavesurfer.js'));
}
else
{
// Browser globals (root is window)
root.returnExports = factory(root.videojs, root.WaveSurfer);
}
}(this, function (videojs, WaveSurfer)
{
var ERROR = 'error';
var WARN = 'warn';
var VjsComponent = videojs.getComponent('Component');
/**
* Draw a waveform for audio and video files in a video.js player.
* @class
* @augments videojs.Component
*/
videojs.Waveform = videojs.extend(VjsComponent,
{
/**
* The constructor function for the class.
*
* @param {(videojs.Player|Object)} player - Video.js player instance.
* @param {Object} options - Player options.
*/
constructor: function(player, options)
{
// run base component initializing with new options
VjsComponent.call(this, player, options);
// parse options
this.waveReady = false;
this.waveFinished = false;
this.liveMode = false;
this.debug = (this.options_.options.debug.toString() === 'true');
this.msDisplayMax = parseFloat(this.options_.options.msDisplayMax);
if (this.options_.options.src === 'live')
{
// check if the wavesurfer.js microphone plugin can be enabled
try
{
this.microphone = Object.create(WaveSurfer.Microphone);
// listen for events
this.microphone.on('deviceError', this.onWaveError.bind(this));
// enable audio input from a microphone
this.liveMode = true;
this.waveReady = true;
this.log('wavesurfer.js microphone plugin enabled.');
}
catch (TypeError)
{
this.onWaveError('Could not find wavesurfer.js ' +
'microphone plugin!');
}
}
// wait until player ui is ready
this.player().one('ready', this.setupUI.bind(this));
},
/**
* Player UI is ready.
* @private
*/
setupUI: function()
{
// customize controls
this.player().bigPlayButton.hide();
// the native controls don't work for this UI so disable
// them no matter what
if (this.player().usingNativeControls_ === true)
{
if (this.player().tech_.el_ !== undefined)
{
this.player().tech_.el_.controls = false;
}
}
// since video.js 5.11.0 the player won't start if no src is
// set (see PR 3378), so fake it
this.player().currentSrc = function()
{
return 'videojs-wavesurfer';
};
if (this.player().options_.controls)
{
// make sure controlBar is showing
this.player().controlBar.show();
this.player().controlBar.el().style.display = 'flex';
// progress control isn't used by this plugin
this.player().controlBar.progressControl.hide();
// prevent controlbar fadeout
this.player().on('userinactive', function(event)
{
this.player().userActive(true);
});
// make sure time display is visible
var uiElements = [this.player().controlBar.currentTimeDisplay,
this.player().controlBar.timeDivider,
this.player().controlBar.durationDisplay];
for (var element in uiElements)
{
// ignore when elements have been disabled by user
if (uiElements[element] !== undefined)
{
uiElements[element].el().style.display = 'block';
uiElements[element].show();
}
}
if (this.player().controlBar.remainingTimeDisplay !== undefined)
{
this.player().controlBar.remainingTimeDisplay.hide();
}
if (this.player().controlBar.timeDivider !== undefined)
{
this.player().controlBar.timeDivider.el().style.textAlign = 'center';
this.player().controlBar.timeDivider.el().style.width = '2em';
}
// disable play button until waveform is ready
// (except when in live mode)
if (!this.liveMode)
{
this.player().controlBar.playToggle.hide();
}
}
// waveform events
this.surfer = Object.create(WaveSurfer);
this.surfer.on('error', this.onWaveError.bind(this));
this.surfer.on('finish', this.onWaveFinish.bind(this));
this.surferReady = this.onWaveReady.bind(this);
this.surferProgress = this.onWaveProgress.bind(this);
this.surferSeek = this.onWaveSeek.bind(this);
// only listen to these events when we're not in live mode
if (!this.liveMode)
{
this.setupPlaybackEvents(true);
}
// player events
this.player().on('play', this.onPlay.bind(this));
this.player().on('pause', this.onPause.bind(this));
this.player().on('volumechange', this.onVolumeChange.bind(this));
this.player().on('fullscreenchange', this.onScreenChange.bind(this));
// kick things off
this.startPlayers();
},
/**
* Starts or stops listening to events related to audio-playback.
*
* @param {boolean} enable - Start or stop listening to playback
* related events.
* @private
*/
setupPlaybackEvents: function(enable)
{
if (enable === false)
{
this.surfer.un('ready', this.surferReady);
this.surfer.un('audioprocess', this.surferProgress);
this.surfer.un('seek', this.surferSeek);
}
else if (enable === true)
{
this.surfer.on('ready', this.surferReady);
this.surfer.on('audioprocess', this.surferProgress);
this.surfer.on('seek', this.surferSeek);
}
},
/**
* Start the players.
* @private
*/
startPlayers: function()
{
var options = this.options_.options;
// init waveform
this.initialize(options);
if (options.src !== undefined)
{
if (this.microphone === undefined)
{
// show loading spinner
this.player().loadingSpinner.show();
// start loading file
this.load(options.src);
}
else
{
// hide loading spinner
this.player().loadingSpinner.hide();
// connect microphone input to our waveform
options.wavesurfer = this.surfer;
this.microphone.init(options);
}
}
else
{
// no valid src found, hide loading spinner
this.player().loadingSpinner.hide();
}
},
/**
* Initializes the waveform.
*
* @param {Object} opts - Plugin options.
* @private
*/
initialize: function(opts)
{
this.originalHeight = this.player().options_.height;
var controlBarHeight = this.player().controlBar.height();
if (this.player().options_.controls === true && controlBarHeight === 0)
{
// the dimensions of the controlbar are not known yet, but we
// need it now, so we can calculate the height of the waveform.
// The default height is 30px, so use that instead.
controlBarHeight = 30;
}
// set waveform element and dimensions
// Set the container to player's container if "container" option is
// not provided. If a waveform needs to be appended to your custom
// element, then use below option. For example:
// container: document.querySelector("#vjs-waveform")
if (opts.container === undefined)
{
opts.container = this.el();
}
// set the height of generated waveform if user has provided height
// from options. If height of waveform need to be customized then use
// option below. For example: waveformHeight: 30
if (opts.waveformHeight === undefined)
{
opts.height = this.player().height() - controlBarHeight;
}
else
{
opts.height = opts.waveformHeight;
}
// split channels
if (opts.splitChannels && opts.splitChannels === true)
{
opts.height /= 2;
}
// customize waveform appearance
this.surfer.init(opts);
},
/**
* Start loading waveform data.
*
* @param {string|blob|file} url - Either the URL of the audio file,
* a Blob or a File object.
*/
load: function(url)
{
if (url instanceof Blob || url instanceof File)
{
this.log('Loading object: ' + url);
this.surfer.loadBlob(url);
}
else
{
this.log('Loading URL: ' + url);
this.surfer.load(url);
}
},
/**
* Start/resume playback or microphone.
*/
play: function()
{
if (this.liveMode)
{
// start/resume microphone visualization
if (!this.microphone.active)
{
this.log('Start microphone');
this.microphone.start();
}
else
{
this.log('Resume microphone');
this.microphone.play();
}
}
else
{
this.log('Start playback');
// put video.js player UI in playback mode
this.player().play();
// start surfer playback
this.surfer.play();
}
},
/**
* Pauses playback or microphone visualization.
*/
pause: function()
{
if (this.liveMode)
{
// pause microphone visualization
this.log('Pause microphone');
this.microphone.pause();
}
else
{
// pause playback
this.log('Pause playback');
if (!this.waveFinished)
{
this.surfer.pause();
}
else
{
this.waveFinished = false;
}
this.setCurrentTime();
}
},
/**
* Remove the player and waveform.
*/
destroy: function()
{
this.log('Destroying plugin');
if (this.liveMode && this.microphone)
{
// destroy microphone plugin
this.log('Destroying microphone');
this.microphone.destroy();
}
this.surfer.destroy();
this.player().dispose();
},
/**
* Set the current volume.
*
* @param {number} volume - The new volume level.
*/
setVolume: function(volume)
{
if (volume !== undefined)
{
this.log('Changing volume to: ' + volume);
this.surfer.setVolume(volume);
}
},
/**
* Save waveform image as data URI.
*
* The default format is 'image/png'. Other supported types are
* 'image/jpeg' and 'image/webp'.
*
* @param {string} [format=image/png] - String indicating the image format.
* @param {number} [quality=1] - Number between 0 and 1 indicating image
* quality if the requested type is 'image/jpeg' or 'image/webp'.
* @returns {string} The data URI of the image data.
*/
exportImage: function(format, quality)
{
return this.surfer.exportImage(format, quality);
},
/**
* Log message (if the debug option is enabled).
* @private
*/
log: function(args, logType)
{
if (this.debug === true)
{
if (logType === ERROR)
{
videojs.log.error(args);
}
else if (logType === WARN)
{
videojs.log.warn(args);
}
else
{
videojs.log(args);
}
}
},
/**
* Get the current time (in seconds) of the stream during playback.
*
* Returns 0 if no stream is available (yet).
*/
getCurrentTime: function()
{
var currentTime = this.surfer.getCurrentTime();
currentTime = isNaN(currentTime) ? 0 : currentTime;
return currentTime;
},
/**
* Updates the player's element displaying the current time.
*
* @param {number} [currentTime] - Current position of the playhead
* (in seconds).
* @param {number} [duration] - Duration of the waveform (in seconds).
* @private
*/
setCurrentTime: function(currentTime, duration)
{
if (currentTime === undefined)
{
currentTime = this.surfer.getCurrentTime();
}
if (duration === undefined)
{
duration = this.surfer.getDuration();
}
currentTime = isNaN(currentTime) ? 0 : currentTime;
duration = isNaN(duration) ? 0 : duration;
var time = Math.min(currentTime, duration);
// update control
this.player().controlBar.currentTimeDisplay.contentEl(
).innerHTML = this.formatTime(time, duration);
},
/**
* Get the duration of the stream in seconds.
*
* Returns 0 if no stream is available (yet).
*/
getDuration: function()
{
var duration = this.surfer.getDuration();
duration = isNaN(duration) ? 0 : duration;
return duration;
},
/**
* Updates the player's element displaying the duration time.
*
* @param {number} [duration] - Duration of the waveform (in seconds).
* @private
*/
setDuration: function(duration)
{
if (duration === undefined)
{
duration = this.surfer.getDuration();
}
duration = isNaN(duration) ? 0 : duration;
// update control
this.player().controlBar.durationDisplay.contentEl(
).innerHTML = this.formatTime(duration, duration);
},
/**
* Wave ready event.
*
* Fired when audio is loaded, decoded and the waveform is drawn.
*
* @event waveReady
*/
/**
* Audio is loaded, decoded and the waveform is drawn.
*
* @fires waveReady
* @private
*/
onWaveReady: function()
{
this.waveReady = true;
this.waveFinished = false;
this.liveMode = false;
this.log('Waveform is ready');
this.player().trigger('waveReady');
// update time display
this.setCurrentTime();
this.setDuration();
// enable and show play button
this.player().controlBar.playToggle.show();
// hide loading spinner
this.player().loadingSpinner.hide();
// auto-play when ready (if enabled)
if (this.player().options_.autoplay)
{
this.play();
}
},
/**
* Playback finish event.
*
* Fired when audio playback finished.
*
* @event playbackFinish
*/
/**
* Fires when audio playback completed.
* @private
*/
onWaveFinish: function()
{
this.log('Finished playback');
this.player().trigger('playbackFinish');
// check if player isn't paused already
if (!this.player().paused())
{
// check if loop is enabled
if (this.player().options_.loop)
{
// reset waveform
this.surfer.stop();
this.play();
}
else
{
// finished
this.waveFinished = true;
// pause player
this.player().pause();
}
}
},
/**
* Fires continuously during audio playback.
*
* @param {number} time - Current time/location of the playhead.
* @private
*/
onWaveProgress: function(time)
{
this.setCurrentTime();
},
/**
* Fires during seeking of the waveform.
* @private
*/
onWaveSeek: function()
{
this.setCurrentTime();
},
/**
* Fired whenever the media in the player begins or resumes playback.
* @private
*/
onPlay: function()
{
// don't start playing until waveform's ready
if (this.waveReady)
{
this.play();
}
},
/**
* Fired whenever the media in the player has been paused.
* @private
*/
onPause: function()
{
this.pause();
},
/**
* Fired when the volume in the player changes.
* @private
*/
onVolumeChange: function()
{
var volume = this.player().volume();
if (this.player().muted())
{
// muted volume
volume = 0;
}
this.setVolume(volume);
},
/**
* Fired when the player switches in or out of fullscreen mode.
* @private
*/
onScreenChange: function()
{
var isFullscreen = this.player().isFullscreen();
var newHeight;
if (!isFullscreen)
{
// restore original height
newHeight = this.originalHeight;
}
else
{
// fullscreen height
newHeight = window.outerHeight;
}
if (this.waveReady)
{
if (this.liveMode && !this.microphone.active)
{
// we're in live mode but the microphone hasn't been
// started yet
return;
}
// destroy old drawing
this.surfer.drawer.destroy();
// set new height
this.surfer.params.height = newHeight - this.player().controlBar.height();
this.surfer.createDrawer();
// redraw
this.surfer.drawBuffer();
// make sure playhead is restored at right position
this.surfer.drawer.progress(this.surfer.backend.getPlayedPercents());
}
},
/**
* Waveform error.
*
* @param {string} error - The wavesurfer error.
* @private
*/
onWaveError: function(error)
{
this.log(error, ERROR);
this.player().trigger('error', error);
},
/**
* Format seconds as a time string, H:MM:SS, M:SS or M:SS:MMM.
*
* Supplying a guide (in seconds) will force a number of leading zeros
* to cover the length of the guide.
*
* @param {number} seconds - Number of seconds to be turned into a
* string.
* @param {number} guide - Number (in seconds) to model the string
* after.
* @return {string} Time formatted as H:MM:SS, M:SS or M:SS:MMM, e.g.
* 0:00:12.
* @private
*/
formatTime: function(seconds, guide)
{
// Default to using seconds as guide
seconds = seconds < 0 ? 0 : seconds;
guide = guide || seconds;
var s = Math.floor(seconds % 60),
m = Math.floor(seconds / 60 % 60),
h = Math.floor(seconds / 3600),
gm = Math.floor(guide / 60 % 60),
gh = Math.floor(guide / 3600),
ms = Math.floor((seconds - s) * 1000);
// handle invalid times
if (isNaN(seconds) || seconds === Infinity)
{
// '-' is false for all relational operators (e.g. <, >=) so this
// setting will add the minimum number of fields specified by the
// guide
h = m = s = ms = '-';
}
// Check if we need to show milliseconds
if (guide > 0 && guide < this.msDisplayMax)
{
if (ms < 100)
{
if (ms < 10)
{
ms = '00' + ms;
}
else
{
ms = '0' + ms;
}
}
ms = ':' + ms;
}
else
{
ms = '';
}
// Check if we need to show hours
h = (h > 0 || gh > 0) ? h + ':' : '';
// If hours are showing, we may need to add a leading zero.
// Always show at least one digit of minutes.
m = (((h || gm >= 10) && m < 10) ? '0' + m : m) + ':';
// Check if leading zero is need for seconds
s = ((s < 10) ? '0' + s : s);
return h + m + s + ms;
}
});
var createWaveform = function()
{
var props = {
className: 'vjs-waveform',
tabIndex: 0
};
return VjsComponent.prototype.createEl('div', props);
};
// plugin defaults
var defaults = {
// Display console log messages.
debug: false,
// msDisplayMax indicates the number of seconds that is
// considered the boundary value for displaying milliseconds
// in the time controls. An audio clip with a total length of
// 2 seconds and a msDisplayMax of 3 will use the format
// M:SS:MMM. Clips longer than msDisplayMax will be displayed
// as M:SS or HH:MM:SS.
msDisplayMax: 3
};
/**
* Initialize the plugin.
*
* @param {Object} [options] - Configuration for the plugin.
* @private
*/
var wavesurferPlugin = function(options)
{
var settings = videojs.mergeOptions(defaults, options);
var player = this;
// create new waveform
player.waveform = new videojs.Waveform(player,
{
'el': createWaveform(),
'options': settings
});
// add waveform to dom
player.el().appendChild(player.waveform.el());
};
// register the plugin
videojs.plugin('wavesurfer', wavesurferPlugin);
// return a function to define the module export
return wavesurferPlugin;
}));