/**
* Browser sound library which blended HTMLVideo and HTMLAudio and WebAudio
*
* The MIT License (MIT)
*
* Copyright (c) 2014 CyberAgent, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*
* @license MIT
* @author Kei Funagayama <funagayama_kei@cyberagent.co.jp>
*/
(function (w) {
'use strict';
var none = function () {};
var isRequire = !!(typeof define === 'function' && define.amd);
var SPRITE_SEPARATOR = '-';
var LOGGER_DEFAULT_SEPARATOR = '-';
var getSpriteName = function (name) {
return {
prefix: name.substring(0, name.indexOf(SPRITE_SEPARATOR)),
suffix: name.substring(name.indexOf(SPRITE_SEPARATOR) + 1),
name: name
};
};
/**
* trace: 1
* debug: 2
* info: 3
* warn: 4
* error: 5
*
*/
var LOG_LEVEL = 3; // default: info
var Logger = (function () {
var ArrayProto = Array.prototype;
var slice = ArrayProto.slice;
function Logger(prefix) {
this.prefix = prefix || LOGGER_DEFAULT_SEPARATOR;
this.prefix = '[' + this.prefix + ']';
}
/**
* Log output (trace)
* @memberof Logger
* @name trace
*/
Logger.prototype.trace = function () {
if (LOG_LEVEL <= 1) {
if (!w.console) {
}
if (w.console.trace) {
w.console.trace('[TRACE]', this.prefix, slice.call(arguments).join(' '));
} else if (w.console.debug) {
w.console.debug('[TRACE]', this.prefix, slice.call(arguments).join(' '));
} else {
w.console.log('[TRACE]', this.prefix, slice.call(arguments).join(' '));
}
}
};
/**
* Log output (debug)
* @memberof Logger
* @name debug
*/
Logger.prototype.debug = function () {
if (LOG_LEVEL <= 2) {
if (!w.console) {
}
if (w.console.debug) {
w.console.debug('[DEBUG]', this.prefix, slice.call(arguments).join(' '));
} else {
w.console.log('[DEBUG]', this.prefix, slice.call(arguments).join(' '));
}
}
};
/**
* Log output (info)
* @memberof Logger
* @name info
*/
Logger.prototype.info = function () {
if (LOG_LEVEL <= 3) {
w.console && w.console.info('[INFO]', this.prefix, slice.call(arguments).join(' '));
}
};
/**
* Log output (warn)
* @memberof Logger
* @name warn
*/
Logger.prototype.warn = function () {
if (LOG_LEVEL <= 4) {
w.console && w.console.warn('[WARN]', this.prefix, slice.call(arguments).join(' '));
}
};
/**
* Log output (error)
* @memberof Logger
* @name error
*/
Logger.prototype.error = function () {
if (LOG_LEVEL <= 5) {
w.console && w.console.error('[ERROR]', this.prefix, slice.call(arguments).join(' '));
}
};
return Logger;
})();
//////////////////////////////////
// Boombox Class
var Boombox = (function () {
function Boombox() {
/**
* Version
* @memberof Boombox
* @name VERSION
*/
this.VERSION = '1.0.7';
/**
* Loop off
*
* @memberof Boombox
* @name LOOP_NOT
* @constant
* @type {Interger}
*/
this.LOOP_NOT = 0;
/**
* orignal loop
*
* @memberof Boombox
* @name LOOP_ORIGINAL
* @constant
* @type {Interger}
*/
this.LOOP_ORIGINAL = 1;
/**
* Native loop
*
* @memberof Boombox
* @name LOOP_NATIVE
* @constant
* @type {Interger}
*/
this.LOOP_NATIVE = 2;
/**
* Turn off the power.
*
* @memberof Boombox
* @name POWER_OFF
* @constant
* @type {Boolean}
*/
this.POWER_OFF = false;
/**
* Turn on the power.
*
* @memberof Boombox
* @name POWER_ON
* @constant
* @type {Boolean}
*/
this.POWER_ON = true;
/**
* It does not support the media type.
*
* @memberof Boombox
* @name ERROR_MEDIA_TYPE
* @constant
* @type {Interger}
*/
this.ERROR_MEDIA_TYPE = 0;
/**
* Hit the filter
*
* @memberof Boombox
* @name ERROR_HIT_FILTER
* @constant
* @type {Interger}
*/
this.ERROR_HIT_FILTER = 1;
/**
* Threshold to determine whether sound source is finished or not
*
* @memberof Boombox
* @name THRESHOLD
* @type {Interger}
*/
this.THRESHOLD = 0.2;
/**
* flag setup
* @memberof Boombox
* @name setuped
* @type {Boolean}
*/
this.setuped = false;
/**
* AudioContext
* @memberof Boombox
* @name AudioContext
* @type {AudioContext}
*/
this.AudioContext = w.AudioContext || w.webkitAudioContext;
/**
* Environmental support information
*
* @memberof Boombox
* @name support
* @type {Object}
*/
this.support = {
mimes: [],
webaudio: {
use: !!this.AudioContext
},
htmlaudio: {
use: false
},
htmlvideo: {
use: false
}
};
if (this.support.webaudio.use) {
/**
* WebAudioContext instance (singleton)
*
* @memberof Boombox
* @name WEB_AUDIO_CONTEXT
* @type {AudioContext}
*/
this.WEB_AUDIO_CONTEXT = new this.AudioContext();
if (!this.WEB_AUDIO_CONTEXT.createGain) {
this.WEB_AUDIO_CONTEXT.createGain = this.WEB_AUDIO_CONTEXT.createGainNode;
}
}
// Check HTML Audio support.
try {
/**
* Test local HTMLAudio
* @memberof Boombox
* @name _audio
* @type {HTMLAudioElement}
*/
this._audio = new w.Audio();
if (this._audio.canPlayType) {
this.support.htmlaudio.use = true;
} else {
this.support.htmlaudio.use = false;
}
} catch (e) {
this.support.htmlaudio.use = false;
}
// Check HTML Video support.
try {
/**
* Test local HTMLVideo
* @memberof Boombox
* @name _video
* @type {HTMLVideoElement}
*/
this._video = document.createElement('video');
if (this._video.canPlayType) {
this.support.htmlvideo.use = true;
} else {
this.support.htmlvideo.use = false;
}
} catch (e) {
this.support.htmlvideo.use = false;
}
/**
* Audio instance pool
*
* @memberof Boombox
* @name pool
*/
this.pool = {};
/**
* Audio instance of waiting
*
* @memberof Boombox
* @name waits
*/
this.waits = [];
/**
* Visibility of browser
*
* @memberof Boombox
* @name visibility
*/
this.visibility = {
hidden: undefined,
visibilityChange: undefined
};
/**
* State of boombox
*
* @memberof Boombox
* @name state
* @type {Object}
*/
this.state = {
power: this.POWER_ON
};
/**
* Filtering function
*
* @memberof Boombox
* @name filter
* @type {Object}
*/
this.filter = {};
}
//// prototype
/**
* The availability of the WebLAudio
*
* @memberof Boombox
* @name isWebAudio
* @return {Boolean}
*/
Boombox.prototype.isWebAudio = function () {
return this.support.webaudio.use;
};
/**
* The availability of the HTMLAudio
*
* @memberof Boombox
* @name isHTMLAudio
* @return {Boolean}
*/
Boombox.prototype.isHTMLAudio = function () {
return this.support.htmlaudio.use;
};
/**
* The availability of the HTMLVideo
*
* @memberof Boombox
* @name isHTMLVideo
* @return {Boolean}
*/
Boombox.prototype.isHTMLVideo = function () {
return this.support.htmlvideo.use;
};
/**
* boombox to manage, Audio is playing
*
* @memberof Boombox
* @name isPlayback
* @return {Boolean}
*/
Boombox.prototype.isPlayback = function () {
var self = this;
var res = false;
for (var name in this.pool) {
if (this.pool[name].isPlayback()) {
res = true;
break;
}
}
return res;
};
/**
* Setup processing
*
* @memberof Boombox
* @name setup
* @param {Object} options
* @return {Boombox}
* @example
* var options = {
* webaudio: {use: Boolean},
* htmlaudio: {use: Boolean},
* htmlvideo: {use: Boolean},
* loglevel: Number, ) trace:1, debug:2, info:3, warn:4, error:5
* }
*
*/
Boombox.prototype.setup = function setup(options) {
var self = this;
options = options || {};
if (typeof options.threshold !== 'undefined') {
this.THRESHOLD = options.threshold;
}
if (typeof options.loglevel !== 'undefined') {
LOG_LEVEL = options.loglevel;
}
this.logger = new Logger('Boombox '); // log
if (this.setuped) {
this.logger.warn('"setup" already, are running.');
return this;
}
if (options.webaudio) {
if (typeof options.webaudio.use !== 'undefined') {
this.support.webaudio.use = options.webaudio.use;
this.logger.info('options.webaudio.use:', this.support.webaudio.use);
}
}
if (options.htmlaudio) {
if (typeof options.htmlaudio.use !== 'undefined') {
this.support.htmlaudio.use = options.htmlaudio.use;
this.logger.info('options.htmlaudio.use: ', this.support.htmlaudio.use);
}
}
if (options.htmlvideo) {
if (typeof options.htmlvideo.use !== 'undefined') {
this.support.htmlvideo.use = options.htmlvideo.use;
this.logger.info('options htmlvideo', this.support.htmlvideo);
}
}
// scan browser
this._browserControl();
// Log: WebAudio support.
if (this.support.webaudio.use) {
this.logger.debug('WebAudio use support.');
} else {
this.logger.debug('WebAudio use not support.');
}
// Log: HTMLAudio support.
if (this.support.htmlaudio.use) {
this.logger.debug('HTMLAudio use support.');
} else {
this.logger.debug('HTMLAudio use not support.');
}
// Log: HTMLVideo support.
if (this.support.htmlvideo.use) {
this.logger.debug('HTMLVideo use support.');
} else {
this.logger.debug('HTMLVideo use not support.');
}
//this.logger.debug('support:', JSON.stringify(this.support));
this.setuped = true;
return this;
};
/**
* Get Audio instance
*
* @memberof Boombox
* @name get
* @param {String} name audio name
* @return {WebAudio|HTMLAudio|HTMLVideo}
*/
Boombox.prototype.get = function (name) {
return this.pool[name];
};
/**
* Loading audio
*
* @memberof Boombox
* @name load
* @param {String} name audio name
* @param {Object} options Audio options
* @param {Boolean} useHTMLVideo forced use HTMLVideo
* @param {Function} callback
* @return {Boombox}
* @example
* var options = {
* src: [
* {
* media: 'audio/mp4',
* path: 'http://example.com/sample.m4a',
* }
* ]
* filter: ['android2', 'android4', 'ios5', 'ios6', 'ios7' ]
* }
*
*/
Boombox.prototype.load = function (name, options, useHTMLVideo, callback) {
if (typeof arguments[2] === 'function') {
callback = useHTMLVideo;
useHTMLVideo = null;
}
if (!this.setuped) {
callback && callback(new Error('setup incomplete boombox. run: boombox.setup(options)'));
return this;
}
if (this.pool[name]) {
this.logger.trace('audio pool cache hit!!', name);
return callback && callback(null, this.pool[name]);
}
// check support media type
var src = this.useMediaType(options.src);
if (src) {
options.media = src.media;
options.src = src.path;
}
// forced use HTMLVideo
if (useHTMLVideo && this.isHTMLVideo()) {
var htmlvideo = new boombox.HTMLVideo(name);
if (!src) {
htmlvideo.state.error = this.ERROR_MEDIA_TYPE;
}
if (typeof htmlvideo.state.error === 'undefined' && !this.runfilter(htmlvideo, options)) {
htmlvideo.load(options, callback);
} else {
return callback && callback(new Error(htmlvideo.state.error), htmlvideo);
}
this.setPool(name, htmlvideo, boombox.HTMLVideo);
return this;
}
//////////////////////
if (this.isWebAudio()) {
this.logger.debug("use web audio");
var webaudio = new boombox.WebAudio(name);
if (!src) {
webaudio.state.error = this.ERROR_MEDIA_TYPE;
}
if (typeof webaudio.state.error === 'undefined' && !this.runfilter(webaudio, options)) {
webaudio.load(options, callback);
} else {
return callback && callback(new Error(webaudio.state.error), webaudio);
}
this.setPool(name, webaudio, boombox.WebAudio);
} else if (this.isHTMLAudio()) {
this.logger.debug("use html audio");
var htmlaudio = new boombox.HTMLAudio(name);
if (!src) {
htmlaudio.state.error = this.ERROR_MEDIA_TYPE;
}
if (typeof htmlaudio.state.error === 'undefined' && !this.runfilter(htmlaudio, options)) {
htmlaudio.load(options, callback);
} else {
return callback && callback(new Error(htmlaudio.state.error), htmlaudio);
}
this.setPool(name, htmlaudio, boombox.HTMLAudio);
} else {
callback && callback(new Error('This environment does not support HTMLAudio, both WebAudio both.'), this);
}
return this;
};
/**
* remove audio
*
* @memberof Boombox
* @name remove
* @param {String} name
* @return {Boombox}
*/
Boombox.prototype.remove = function (name) {
if (this.pool[name]) { // change object
this.logger.trace('Remove Audio that is pooled. name', name);
this.pool[name].dispose && this.pool[name].dispose();
this.pool[name] = undefined;
delete this.pool[name];
}
return this;
};
/**
* Set pool
*
* @memberof Boombox
* @name setPool
* @param {String} name
* @param {AudioContext|HTMLAudioElement|HTMLVideoElement} obj Browser audio instance
* @param {WebAudio|HTMLAudio|HTMLVideo} Obj Boombox audio class
* @return {Boombox}
*/
Boombox.prototype.setPool = function (name, obj, Obj) {
if (obj.isParentSprite()) {
for (var r in this.pool) {
if (!!~r.indexOf(name + SPRITE_SEPARATOR)) {
delete this.pool[r];
}
}
for (var n in obj.sprite.options) {
var cname = name + SPRITE_SEPARATOR + n;
var audio = new Obj(cname, obj);
this.pool[audio.name] = audio;
}
}
this.remove(name);
this.pool[name] = obj;
return this;
};
/**
* Run filter
*
* @memberof Boombox
* @name runfilter
* @param {WebAudio|HTMLAudio|HTMLVideo} audio Boombox audio instance
* @param {Object} options
* @return {Boolean}
*/
Boombox.prototype.runfilter = function (audio, options) {
var hit;
var list = options.filter || [];
for (var i = 0; i < list.length; i++) {
var name = list[i];
var fn = this.filter[name];
if (!fn) {
this.logger.warn('filter not found. name:', name);
return;
}
this.logger.debug('filter run. name:', name);
if (fn(name, audio, options)) {
hit = name;
break;
}
}
if (hit) {
audio.state.error = this.ERROR_HIT_FILTER; // set error
this.logger.warn('Hit the filter of', hit);
return true;
}
return false;
};
/**
* check support media type
*
* @memberof Boombox
* @name useMediaType
* @param {Array} src audio file data
* @return {Object|undefined}
*/
Boombox.prototype.useMediaType = function (src) {
for (var i = 0; i < src.length; i++) {
var t = src[i];
if (this._audio.canPlayType(t.media)) {
return t;
} else {
this.logger.warn('skip audio type.', t.media);
}
}
return undefined;
};
/**
* pause sound playback in managing boombox
*
* @memberof Boombox
* @name pause
* @return {boombox}
*/
Boombox.prototype.pause = function () {
var self = this;
this.logger.trace('pause');
for (var name in this.pool) {
var audio = this.pool[name];
audio.pause();
self.waits.push(name);
}
return this;
};
/**
* resume the paused, to manage the boombox
*
* @memberof Boombox
* @name resume
* @return {Boombox}
*/
Boombox.prototype.resume = function () {
this.logger.trace('resume');
var name = this.waits.shift();
if (name && this.pool[name]) {
this.pool[name].resume();
}
if (0 < this.waits.length) {
this.resume();
}
return this;
};
/**
* Change all audio power on/off
*
* @memberof Boombox
* @name power
* @param {Boolean} p power on/off. boombox.(POWER_ON|POWER_OFF)
* @return {Boombox}
*/
Boombox.prototype.power = function (p) {
var self = this;
this.logger.trace('power:', this.name, 'flag:', p);
for (var name in this.pool) {
var audio = this.pool[name];
audio.power(p);
}
this.state.power = p;
return this;
};
/**
* audio change volume.
*
* @memberof Boombox
* @method
* @name volume
* @param {Interger} v volume
* @return {Boombox}
*/
Boombox.prototype.volume = function (v) {
var self = this;
this.logger.trace('volume:', this.name, 'volume:', v);
for (var name in this.pool) {
var audio = this.pool[name];
audio.volume(v);
}
return this;
};
/**
* Firing in the occurrence of events VisibilityChange
*
* @memberof Boombox
* @name onVisibilityChange
* @param {Event} e event
*/
Boombox.prototype.onVisibilityChange = function (e) {
this.logger.trace('onVisibilityChange');
if (document[this.visibility.hidden]) {
this.pause();
} else {
this.resume();
}
};
/**
* Firing in the occurrence of events window.onfocus
*
* @memberof Boombox
* @name onFocus
* @param {Event} e event
*/
Boombox.prototype.onFocus = function (e) {
this.logger.trace('onFocus');
this.resume();
};
/**
* Firing in the occurrence of events window.onblur
*
* @memberof Boombox
* @name onBlur
* @param {Event} e event
*/
Boombox.prototype.onBlur = function (e) {
this.logger.trace('onBlur');
this.pause();
};
/**
* Firing in the occurrence of events window.onpageshow
*
* @memberof Boombox
* @name onPageShow
* @param {Event} e event
*/
Boombox.prototype.onPageShow = function (e) {
this.logger.trace('onPageShow');
this.resume();
};
/**
* Firing in the occurrence of events window.onpagehide
*
* @memberof Boombox
* @name onPageHide
* @param {Event} e event
*/
Boombox.prototype.onPageHide = function (e) {
this.logger.trace('onPageHide');
this.pause();
};
/**
* Scan browser differences
*
* @memberof Boombox
* @name _browserControl
* @return {Boombox}
*/
Boombox.prototype._browserControl = function () {
var self = this;
if (typeof document.hidden !== "undefined") {
this.visibility.hidden = "hidden";
this.visibility.visibilityChange = "visibilitychange";
} else if (typeof document.webkitHidden !== "undefined") {
this.visibility.hidden = "webkitHidden";
this.visibility.visibilityChange = "webkitvisibilitychange";
} else if (typeof document.mozHidden !== "undefined") {
this.visibility.hidden = "mozHidden";
this.visibility.visibilityChange = "mozvisibilitychange";
} else if (typeof document.msHidden !== "undefined") {
this.visibility.hidden = "msHidden";
this.visibility.visibilityChange = "msvisibilitychange";
}
// Visibility.hidden
if (this.visibility.hidden) {
document.addEventListener(this.visibility.visibilityChange, function (e) {
self.onVisibilityChange(e);
}, false);
}
// focus/blur
if (typeof window.addEventListener !== "undefined") {
window.addEventListener('focus', function (e) {
self.onFocus(e);
}, false);
window.addEventListener('blur', function (e) {
self.onBlur(e);
}, false);
} else {
window.attachEvent('onfocusin', function (e) {
self.onFocus(e);
}, false);
window.attachEvent('onfocusout', function (e) {
self.onBlur(e);
}, false);
}
// onpage show/hide
window.onpageshow = function (e) {
self.onPageShow(e);
};
window.onpagehide = function (e) {
self.onPageHide(e);
};
//
return this;
};
/**
* Adding filtering
*
* @memberof Boombox
* @name addFilter
* @param {String} filter name
* @param {Function} filter function
* @return {Boombox}
*/
Boombox.prototype.addFilter = function (name, fn) {
this.filter[name] = fn;
return this;
};
/**
* dispose
*
* @memberof Boombox
* @name dispose
*/
Boombox.prototype.dispose = function () {
for (var name in this.pool) {
var audio = this.pool[name];
audio.dispose && audio.dispose();
}
delete this.VERSION;
delete this.setuped;
delete this.support.mimes;
delete this.support.webaudio.use;
this.WEB_AUDIO_CONTEXT && delete this.WEB_AUDIO_CONTEXT;
delete this.support.webaudio;
delete this.support.htmlaudio.use;
delete this.support.htmlaudio;
delete this.support.htmlvideo.use;
delete this.support.htmlvideo;
delete this.support;
delete this.pool;
delete this.waits;
delete this.visibility;
delete this.state.power;
delete this.state;
delete this._audio;
delete this._video;
delete this.filter;
};
return Boombox;
})();
//////////////////////////////////
// New!!!!
var boombox = new Boombox();
//////////////////////////////////
// HTMLAudio Class
var HTMLAudio = (function () {
function HTMLAudio(name, parent) {
/**
* logger
* @memberof HTMLAudio
* @name logger
*/
this.logger = new Logger('HTMLAudio');
/**
* unique name
* @memberof HTMLAudio
* @name name
*/
this.name = name;
/**
* SetTimeout#id pool running
* @memberof HTMLAudio
* @name _timer
*/
this._timer = {}; // setTimeout#id
/**
* AudioSprite option data
* @memberof HTMLAudio
* @name sprite
*/
this.sprite = undefined;
if (parent) {
var sprite_n = getSpriteName(name);
// change Sprite
var current = parent.sprite.options[sprite_n.suffix];
/**
* Reference of the parent instance HTMLAudio
* @memberof HTMLAudio
* @name parent
*/
this.parent = parent; // ref
/**
* Reference of the parent state instance HTMLAudio
* @memberof HTMLAudio
* @name state
*/
this.state = this.parent.state; // ref
/**
* Reference of the parent HTMLAudioElement instance HTMLAudio
* @memberof HTMLAudio
* @name state
*/
this.$el = this.parent.$el; // ref
//this.onEnded = this.parent.onEnded; // TODO: ref
this.sprite = new Sprite(undefined, current); // new
} else {
this.state = {
time: {
playback: undefined, // Playback start time (unixtime)
pause: undefined // // Seek time paused
},
loop: boombox.LOOP_NOT, // Loop playback flags
power: boombox.POWER_ON, // Power flag
loaded: false, // Audio file is loaded
error: undefined // error state
};
this.$el = new w.Audio();
}
}
////
/**
* Loading html audio
*
* @memberof HTMLAudio
* @name load
* @method
* @param {Object} options
* @param {Function} callback
* @return {HTMLAudio}
* @example
* .load({
* src: '', // required
* type: '', // optional
* media: '', // optional
* preload: 'auto', // optional
* autoplay: false, // optional
* mediagroup: 'boombox', // optional
* loop: false, // optional
* muted: false, // optional
* crossorigin: "anonymous", // optional
* controls: false // optional
* timeout: 15 * 1000 // optional default) 15s
* }, function callback() {});
*
*/
HTMLAudio.prototype.load = function (options, callback) {
var cb = callback || none;
if (this.parent) { // skip audiosprite children
cb(null, this);
return this;
}
options = options || {
//src: '',
//type: '',
//media: '',
preload: 'auto', // auto, metadata, none
autoplay: false, // no-auto
//mediagroup: 'boombox',
loop: false,
muted: false,
//crossorigin: "anonymous",
controls: false
};
var timeout = options.timeout || 15 * 1000;
delete options.timeout;
if (options.spritemap) { // Sprite ON
this.sprite = new Sprite(options.spritemap);
delete options.spritemap;
}
for (var k in options) {
var v = options[k];
this.logger.trace('HTMLAudioElement attribute:', k, v);
this.$el[k] = v;
}
var self = this;
// Debug log
/**
["loadstart",
"progress",
"suspend",
"load",
"abort",
"error",
"emptied",
"stalled",
"play",
"pause",
"loadedmetadata",
"loadeddata",
"waiting",
"playing",
"canplay",
"canplaythrough",
"seeking",
"seeked",
"timeupdate",
"ended",
"ratechange",
"durationchange",
"volumechange"].forEach(function (eventName) {
self.$el.addEventListener(eventName, function () {
console.log('audio: ' + eventName);
}, true);
});
*/
var hookEventName = 'canplay';
var ua_ios = window.navigator.userAgent.match(/(iPhone\sOS)\s([\d_]+)/);
if (ua_ios && 0 < ua_ios.length) { // IOS Safari
hookEventName = 'suspend';
}
this.logger.trace('hook event name:', hookEventName);
this.$el.addEventListener(hookEventName, function _canplay(e) {
self.logger.trace('processEvent ' + e.target.id + ' : ' + e.type, 'event');
self.state.loaded = true;
self.$el.removeEventListener(hookEventName, _canplay, false);
return cb(null, self);
});
this.$el.addEventListener(
'ended',
function (e) {
self._onEnded(e);
},
false);
// communication time-out
setTimeout(function () {
if (self.$el && self.$el.readyState !== 4) {
self.$el.src = '';
cb(new Error('load of html audio file has timed out. timeout:' + timeout), self);
cb = function () {};
}
}, timeout);
this.$el.load();
// setInterval(function () {
// console.error(self.name,
// 'playback:', self.isPlayback(),
// 'stop:', self.isStop(),
// 'pause:', self.isPause(),
// 'power:', self.state.power
// );
// }, 100);
return this;
};
//HTMLAudio.prototype.addTextTrack = function () { /**...*/ };
//HTMLAudio.prototype.canPlayType = function () { /**...*/ };
//////////
/**
* Is use.
*
* @memberof HTMLAudio
* @method
* @name isUse
* @return {Boolean}
*/
HTMLAudio.prototype.isUse = function () {
if (this.state.power === boombox.POWER_OFF || boombox.state.power === boombox.POWER_OFF) {
return false;
}
if (!this.state.loaded || typeof this.state.error !== 'undefined') {
return false;
}
return true;
};
/**
* Is playing.
*
* @memberof HTMLAudio
* @method
* @name isPlayback
* @return {Boolean}
*/
HTMLAudio.prototype.isPlayback = function () {
return !!this.state.time.playback;
};
/**
* Is stoped.
*
* @memberof HTMLAudio
* @method
* @name isStop
* @return {Boolean}
*/
HTMLAudio.prototype.isStop = function () {
return !this.state.time.playback;
};
/**
* Is paused.
*
* @memberof HTMLAudio
* @method
* @name isPause
* @return {boolean}
*/
HTMLAudio.prototype.isPause = function () {
return !!this.state.time.pause;
};
/**
* Loop flag
*
* @memberof HTMLAudio
* @method
* @name isLoop
* @return {Interger}
*/
HTMLAudio.prototype.isLoop = function () {
return (0 < this.state.loop);
};
/**
* Is sprite of the parent
*
* @memberof HTMLAudio
* @method
* @name isParentSprite
* @return {Boolean}
*/
HTMLAudio.prototype.isParentSprite = function () {
return !!(!this.parent && this.sprite && !this.sprite.current);
};
/**
* Is sprite
*
* @memberof HTMLAudio
* @method
* @name isSprite
* @return {Boolean}
*/
HTMLAudio.prototype.isSprite = function () {
return !!(this.parent && this.sprite && this.sprite.current);
};
/**
* Clear all the setTimeout
*
* @memberof HTMLAudio
* @method
* @name clearTimerAll
* @return {HTMLAudio}
*/
HTMLAudio.prototype.clearTimerAll = function () {
for (var k in this._timer) {
var id = this._timer[k];
this.clearTimer(k);
}
return this;
};
/**
* Clear specified setTimeout
*
* @memberof HTMLAudio
* @method
* @name clearTimer
* @param {String} name
* @return {Interger}
*/
HTMLAudio.prototype.clearTimer = function (name) {
var id = this._timer[name];
if (id) {
this.logger.debug('remove setTimetout:', id);
clearTimeout(id);
delete this._timer[name];
}
return id;
};
/**
* Save the specified setTimeout
*
* @memberof HTMLAudio
* @method
* @name setTimer
* @param {String} name
* @param {Interger} id setTimeout#id
* @return {Interger}
*/
HTMLAudio.prototype.setTimer = function (name, id) {
if (this._timer[name]) {
this.logger.warn('Access that is not expected:', name, id);
}
this._timer[name] = id;
return this._timer[name];
};
//////////
/**
* audio play.
*
* @memberof HTMLAudio
* @method
* @name play
* @param {Boolean} resume resume flag
* @return {HTMLAudio}
*/
HTMLAudio.prototype.play = function (resume) {
if (!this.isUse()) {
this.logger.debug('skip play:', this.name, 'state can not be used');
return this;
} // skip!!
if (this.isPlayback()) {
this.logger.debug('skip play:', this.name, 'is playing');
return this;
}
var self = this;
var type = 'play';
var fn = none;
this.state.time.playback = Date.now();
if (resume && this.state.time.pause) {
// resume
this.setCurrentTime(this.state.time.pause);
if (this.isSprite()) {
var _pause = this.state.time.pause;
fn = function () {
var interval = Math.ceil((self.sprite.current.end - _pause) * 1000); // (ms)
self.setTimer('play', setTimeout(function () {
self.stop();
self._onEnded(); // fire onended evnet
}, interval));
};
}
this.state.time.pause = undefined;
type = 'resume:';
} else {
// zero-play
this.setCurrentTime(0);
if (this.isSprite()) {
var start = this.sprite.current.start;
this.setCurrentTime(start);
fn = function () {
var interval = Math.ceil(self.sprite.current.term * 1000); // (ms)
self.setTimer('play', setTimeout(function () {
self.stop();
self._onEnded(); // fire onended evnet
}, interval));
};
}
}
this.logger.debug(type, this.name);
fn();
this.state.time.name = this.name;
this.$el.play();
return this;
};
/**
* audio stop.
*
* @memberof HTMLAudio
* @method
* @name stop
* @return {HTMLAudio}
*/
HTMLAudio.prototype.stop = function () {
if (!this.state.loaded || typeof this.state.error !== 'undefined') {
this.logger.debug('skip stop:', this.name, 'state can not be used');
return this;
} // skip!!
if (this.state.time.name && this.state.time.name !== this.name) {
this.logger.debug('skip stop: It is used in other sources', this.name, this.state.time.name);
return this;
} // skip!!
this.logger.debug('stop:', this.name);
this.clearTimer('play');
this.$el.pause();
this.setCurrentTime(0);
this.state.time.playback = undefined;
this.state.time.name = undefined;
return this;
};
/**
* audio pause.
*
* @memberof HTMLAudio
* @method
* @name pause
* @return {HTMLAudio}
*/
HTMLAudio.prototype.pause = function () {
if (!this.isUse()) {
this.logger.debug('skip pause:', this.name, 'state can not be used');
return this;
} // skip!!
if (this.state.time.name && this.state.time.name !== this.name) {
this.logger.debug('skip pause: It is used in other sources', this.name, this.state.time.name);
return this;
} // skip!!
this.logger.debug('pause:', this.name);
this.clearTimer('play');
this.$el.pause();
this.state.time.pause = this.$el.currentTime;
this.state.time.playback = undefined;
//this.state.time.name = undefined;
return this;
};
/**
* audio resume.
*
* @memberof HTMLAudio
* @method
* @name resume
* @return {HTMLAudio}
*/
HTMLAudio.prototype.resume = function () {
if (!this.isUse()) {
this.logger.debug('skip resume:', this.name, 'state can not be used');
return this;
} // skip!!
if (this.state.time.name && this.state.time.name !== this.name) {
this.logger.debug('skip resume: It is used in other sources', this.name, this.state.time.name);
return this;
} // skip!!
if (this.state.time.pause) {
this.play(true);
}
return this;
};
/**
* audio re-play.
*
* @memberof HTMLAudio
* @method
* @name replay
* @return {HTMLAudio}
*/
HTMLAudio.prototype.replay = function () {
if (!this.isUse()) {
this.logger.debug('skip replay:', this.name, 'state can not be used');
return this;
} // skip!!
this.logger.debug('replay:', this.name);
this.clearTimer('play');
this.pause();
this.setCurrentTime(0);
this.play();
return this;
};
/**
* audio change volume.
*
* @memberof HTMLAudio
* @method
* @name volume
* @param {Interger} v volume
* @return {HTMLAudio}
*/
HTMLAudio.prototype.volume = function (v) {
this.logger.trace('volume:', this.name, 'volume:', v);
this.$el.volume = Math.max(0, Math.min(1, v));
};
//////////
/**
* Audio.ended events
*
* @memberof HTMLAudio
* @method
* @name _onEnded
* @param {Event} e event
*/
HTMLAudio.prototype._onEnded = function (e) {
if (this.isDisposed()) { // check dispose
return;
}
this.logger.trace('onended fire! name:', this.name);
this.state.time.playback = undefined;
this.state.time.name = undefined;
this.onEnded(e); // fire user ended event!!
if (this.isDisposed()) { // check dispose
return;
}
if (this.state.loop === boombox.LOOP_ORIGINAL && typeof this.state.time.pause === 'undefined') {
this.logger.trace('onended original loop play.', this.name);
this.play();
}
};
/**
* Override Audio.ended events
*
* @memberof HTMLAudio
* @method
* @name onEnded
* @param {Event} e event
*/
HTMLAudio.prototype.onEnded = none;
/**
* Set loop flag
*
* @memberof HTMLAudio
* @method
* @name setLoop
* @param {Interger} loop loop flag (Boombox.LOOP_XXX)
* @return {HTMLAudio}
*/
HTMLAudio.prototype.setLoop = function (loop) {
if (!this.isUse()) { return this; } // skip!!
this.state.loop = loop;
if (loop === boombox.LOOP_NOT) {
this.$el.loop = boombox.LOOP_NOT;
//} else if (loop === boombox.LOOP_ORIGINAL) {
} else if (loop === boombox.LOOP_NATIVE) {
if (this.isSprite()) {
this.logger.warn('audiosprite does not support the native. please use the original loop.');
return this;
}
if (this.$el) {
this.$el.loop = loop;
}
}
return this;
};
/**
* Change power on/off
*
* @memberof HTMLAudio
* @method
* @name power
* @param {Boolean} p power on/off. Boombox.(POWER_ON|POWER_OFF)
* @return {HTMLAudio}
*/
HTMLAudio.prototype.power = function (p) {
this.logger.trace('power:', this.name, 'flag:', p);
if (p === boombox.POWER_OFF) {
this.stop(); // force pause
}
this.state.power = p;
return this;
};
/**
* Set audio.currentTime
*
* @memberof HTMLAudio
* @method
* @name setCurrentTime
* @param {Interger} t set value(HTMLAudioElement.currentTime)
* @return {HTMLAudio}
*/
HTMLAudio.prototype.setCurrentTime = function (t) {
try {
this.$el.currentTime = t;
} catch (e) {
this.logger.error('Set currentTime.', e.message);
}
return this;
};
/**
* Check disposed
*
* @memberof HTMLAudio
* @method
* @name isDisposed
*/
HTMLAudio.prototype.isDisposed = function () {
return WebAudio.prototype.isDisposed.apply(this, arguments);
};
//////////
/**
* Dispose
*
* @memberof HTMLAudio
* @method
* @name dispose
*/
HTMLAudio.prototype.dispose = function () {
delete this.name;
delete this.state.time.playback;
delete this.state.time.pause;
delete this.state.time.name;
delete this.state.time;
delete this.state.loop;
delete this.state.power;
delete this.state.loaded;
delete this.state.error;
delete this.state;
this.$el.src = undefined;
delete this.$el;
this.clearTimerAll();
delete this._timer;
delete this.parent;
if (this.sprite && this.sprite.dispose) {
this.sprite.dispose();
}
delete this.sprite;
};
return HTMLAudio;
})();
//////////////////////////////////
// HTMLVideo Class
var HTMLVideo = (function () {
function HTMLVideo(name, parent) {
/**
* logger
* @memberof HTMLVideo
* @name logger
*/
this.logger = new Logger('HTMLVideo');
/**
* unique name
* @memberof HTMLVideo
* @name name
*/
this.name = name;
/**
* SetTimeout#id pool running
* @memberof HTMLVideo
* @name _timer
*/
this._timer = {}; // setTimeout#id
/**
* AudioSprite option data
* @memberof HTMLVideo
* @name sprite
*/
this.sprite = undefined;
if (parent) {
var sprite_n = getSpriteName(name);
// change Sprite
var current = parent.sprite.options[sprite_n.suffix];
/**
* Reference of the parent instance HTMLVideo
* @memberof HTMLVideo
* @name parent
*/
this.parent = parent; // ref
/**
* Reference of the parent state instance HTMLVideo
* @memberof HTMLVideo
* @name state
*/
this.state = this.parent.state; // ref
/**
* Reference of the parent HTMLVideoElement instance HTMLVideo
* @memberof HTMLAudio
* @name state
*/
this.$el = this.parent.$el; // ref
//this.onEnded = this.parent.onEnded; // TODO: ref
this.sprite = new Sprite(undefined, current); // new
} else {
this.state = {
time: {
playback: undefined, // Playback start time (unixtime)
pause: undefined // // Seek time paused
},
loop: boombox.LOOP_NOT, // Loop playback flags
power: boombox.POWER_ON, // Power flag
loaded: false, // Video file is loaded
error: undefined // error state
};
this.$el = document.createElement('video');
}
}
///////
/**
* Loading html video
*
* @memberof HTMLVideo
* @name load
* @method
* @param {Object} options
* @param {Function} callback
* @return {HTMLVideo}
* @example
* .load({
* src: '', // required
* type: '', // optional
* media: '', // optional
* preload: 'auto', // optional
* autoplay: false, // optional
* mediagroup: 'boombox', // optional
* loop: false, // optional
* muted: false, // optional
* crossorigin: "anonymous", // optional
* controls: false // optional
* timeout: 15 * 1000 // optional default) 15s
* }, function callback() {});
*
*/
HTMLVideo.prototype.load = function (options, callback) {
var self = this;
var cb = callback || none;
if (this.parent) { // skip audiosprite children
cb(null, this);
return this;
}
options = options || {
//src: '',
//type: '',
//media: '',
preload: 'auto', // auto, metadata, none
autoplay: false, // no-auto
//mediagroup: 'boombox',
loop: false,
muted: false,
//crossorigin: "anonymous",
controls: false
};
var timeout = options.timeout || 15 * 1000;
delete options.timeout;
if (options.spritemap) { // Sprite ON
this.sprite = new Sprite(options.spritemap);
delete options.spritemap;
}
for (var k in options) {
var v = options[k];
self.logger.trace('HTMLVideoElement attribute:', k, v);
self.$el[k] = v;
}
/// Debug log
/**
["loadstart",
"progress",
"suspend",
"load",
"abort",
"error",
"emptied",
"stalled",
"play",
"pause",
"loadedmetadata",
"loadeddata",
"waiting",
"playing",
"canplay",
"canplaythrough",
"seeking",
"seeked",
"timeupdate",
"ended",
"ratechange",
"durationchange",
"volumechange"].forEach(function (eventName) {
self.$el.addEventListener(eventName, function () {
console.log('audio: ' + eventName);
}, true);
});
*/
var hookEventName = 'canplay';
var ua_ios = window.navigator.userAgent.match(/(iPhone\sOS)\s([\d_]+)/);
if (ua_ios && 0 < ua_ios.length) { // IOS Safari
hookEventName = 'suspend';
} else if (!!window.navigator.userAgent.match(/(Android)\s+(4)([\d.]+)/)) { // Android 4 basic
hookEventName = 'loadeddata';
} else if (!!window.navigator.userAgent.match(/(Android)\s+(2)([\d.]+)/)) { // Android 2 basic
hookEventName = 'stalled';
}
self.logger.trace('hook event name:', hookEventName);
this.$el.addEventListener(hookEventName, function _canplay(e) {
self.logger.trace('processEvent ' + e.target.id + ' : ' + e.type, 'event');
self.state.loaded = true;
self.$el.removeEventListener(hookEventName, _canplay, false);
return cb(null, self);
});
this.$el.addEventListener(
'ended',
function (e) {
self._onEnded(e);
},
false);
// communication time-out
setTimeout(function () {
if (self.$el && self.$el.readyState !== 4) {
self.$el.src = '';
cb(new Error('load of html video file has timed out. timeout:' + timeout), self);
cb = function () {};
}
}, timeout);
this.$el.load();
// setInterval(function () {
// console.error(self.name,
// 'playback:', self.isPlayback(),
// 'stop:', self.isStop(),
// 'pause:', self.isPause(),
// 'power:', self.state.power
// );
// }, 100);
return this;
};
//HTMLVideo.prototype.addTextTrack = function () { /**...*/ };
//HTMLVideo.prototype.canPlayType = function () { /**...*/ };
//////////
/**
* Is use. (apply HTMLAudio)
*
* @memberof HTMLVideo
* @method
* @name isUse
* @return {Boolean}
*/
HTMLVideo.prototype.isUse = function () {
return boombox.HTMLAudio.prototype.isUse.apply(this, arguments);
};
/**
* Is playing. (apply HTMLAudio)
*
* @memberof HTMLVideo
* @method
* @name isPlayback
* @return {Boolean}
*/
HTMLVideo.prototype.isPlayback = function () {
return boombox.HTMLAudio.prototype.isPlayback.apply(this, arguments);
};
/**
* Is stoped.
*
* @memberof HTMLVideo (apply HTMLAudio)
* @method
* @name isStop
* @return {Boolean}
*/
HTMLVideo.prototype.isStop = function () {
return boombox.HTMLAudio.prototype.isStop.apply(this, arguments);
};
/**
* Is paused.
*
* @memberof HTMLVideo (apply HTMLAudio)
* @method
* @name isPause
* @return {Boolean}
*/
HTMLVideo.prototype.isPause = function () {
return boombox.HTMLAudio.prototype.isPause.apply(this, arguments);
};
/**
* Loop flag (apply HTMLAudio)
*
* @memberof HTMLVideo
* @method
* @name isLoop
* @return {Interger}
*/
HTMLVideo.prototype.isLoop = function () {
return boombox.HTMLAudio.prototype.isLoop.apply(this, arguments);
};
/**
* Is sprite of the parent (apply HTMLAudio)
*
* @memberof HTMLVideo
* @method
* @name isParentSprite
* @return {Boolean}
*/
HTMLVideo.prototype.isParentSprite = function () {
return boombox.HTMLAudio.prototype.isParentSprite.apply(this, arguments);
};
/**
* Is sprite (apply HTMLAudio)
*
* @memberof HTMLVideo
* @method
* @name isSprite
* @return {Boolean}
*/
HTMLVideo.prototype.isSprite = function () {
return boombox.HTMLAudio.prototype.isSprite.apply(this, arguments);
};
/**
* Clear all the setTimeout (apply HTMLAudio)
*
* @memberof HTMLVideo
* @method
* @name clearTimerAll
* @return {HTMLAudio}
*/
HTMLVideo.prototype.clearTimerAll = function () {
return boombox.HTMLAudio.prototype.clearTimerAll.apply(this, arguments);
};
/**
* Clear specified setTimeout (apply HTMLAudio)
*
* @memberof HTMLVideo
* @method
* @name clearTimer
* @param {String} name
* @return {Interger}
*/
HTMLVideo.prototype.clearTimer = function (name) {
return boombox.HTMLAudio.prototype.clearTimer.apply(this, arguments);
};
/**
* Save the specified setTimeout (apply HTMLAudio)
*
* @memberof HTMLVideo
* @method
* @name setTimer
* @param {String} name
* @param {Interger} id setTimeout#id
* @return {Interger}
*/
HTMLVideo.prototype.setTimer = function (name, id) {
return boombox.HTMLAudio.prototype.setTimer.apply(this, arguments);
};
//////////
/**
* video play.
*
* @memberof HTMLVideo
* @method
* @name play
* @param {Boolean} resume resume flag
* @return {HTMLVideo}
*/
HTMLVideo.prototype.play = function (resume) {
if (!this.isUse()) {
this.logger.debug('skip play:', this.name, 'state can not be used');
return this;
} // skip!!
if (this.isPlayback()) {
this.logger.debug('skip play:', this.name, 'is playing');
return this;
}
var self = this;
var type = 'play';
var fn = none;
this.state.time.playback = Date.now();
if (resume && this.state.time.pause) {
// resume
this.setCurrentTime(this.state.time.pause);
if (this.isSprite()) {
var _pause = this.state.time.pause;
fn = function () {
var interval = Math.ceil((self.sprite.current.end - _pause) * 1000); // (ms)
self.setTimer('play', setTimeout(function () {
self.stop();
self._onEnded(); // fire onended evnet
}, interval));
};
}
this.state.time.pause = undefined;
type = 'resume:';
} else {
// zero-play
this.setCurrentTime(0);
if (this.isSprite()) {
var start = this.sprite.current.start;
this.setCurrentTime(start);
fn = function () {
var interval = Math.ceil(self.sprite.current.term * 1000); // (ms)
self.setTimer('play', setTimeout(function () {
self.stop();
self._onEnded(); // fire onended evnet
}, interval));
};
}
}
this.logger.debug(type, this.name);
fn();
this.state.time.name = this.name;
this.$el.play();
return this;
};
/**
* video stop. (apply HTMLAudio)
*
* @memberof HTMLVideo
* @method
* @name stop
* @return {HTMLVideo}
*/
HTMLVideo.prototype.stop = function () {
return boombox.HTMLAudio.prototype.stop.apply(this, arguments);
};
/**
* video pause. (apply HTMLAudio)
*
* @memberof HTMLVideo
* @method
* @name pause
* @return {HTMLVideo}
*/
HTMLVideo.prototype.pause = function () {
return boombox.HTMLAudio.prototype.pause.apply(this, arguments);
};
/**
* video resume. (apply HTMLAudio)
*
* @memberof HTMLVideo
* @method
* @name resume
* @return {HTMLVideo}
*/
HTMLVideo.prototype.resume = function () {
return boombox.HTMLAudio.prototype.resume.apply(this, arguments);
};
/**
* video re-play. (apply HTMLAudio)
*
* @memberof HTMLVideo
* @method
* @name replay
* @return {HTMLVideo}
*/
HTMLVideo.prototype.replay = function () {
return boombox.HTMLAudio.prototype.replay.apply(this, arguments);
};
/**
* audio change volume. (apply HTMLAudio)
*
* @memberof HTMLVideo
* @method
* @name volume
* @return {HTMLVideo}
*/
HTMLVideo.prototype.volume = function (v) {
return boombox.HTMLAudio.prototype.volume.apply(this, arguments);
};
//////////
/**
* Video.ended events (apply HTMLAudio)
*
* @memberof HTMLVideo
* @method
* @name _onEnded
* @param {Event} e event
*/
HTMLVideo.prototype._onEnded = function (e) {
return boombox.HTMLAudio.prototype._onEnded.apply(this, arguments);
};
/**
* Override Video.ended events (apply HTMLAudio)
*
* @OVERRIDE ME
* @memberof HTMLVideo
* @method
* @name onEnded
* @param {Event} e event
*/
HTMLVideo.prototype.onEnded = none;
/**
* Set loop flag (apply HTMLAudio)
*
* @memberof HTMLVideo
* @method
* @name setLoop
* @param {Interger} loop loop flag (Boombox.LOOP_XXX)
* @return {HTMLVideo}
*/
HTMLVideo.prototype.setLoop = function (loop) {
return boombox.HTMLAudio.prototype.setLoop.apply(this, arguments);
};
/**
* Change power on/off (apply HTMLAudio)
*
* @memberof HTMLVideo
* @method
* @name power
* @param {Boolean} p power on/off. boombox.(POWER_ON|POWER_OFF)
* @return {HTMLVideo}
*/
HTMLVideo.prototype.power = function (p) {
return boombox.HTMLAudio.prototype.power.apply(this, arguments);
};
/**
* Set video.currentTime (apply HTMLAudio)
*
* @memberof HTMLVideo
* @method
* @name setCurrentTime
* @param {Interger} t set value(Video.currentTime)
* @return {HTMLVideo}
*/
HTMLVideo.prototype.setCurrentTime = function (t) {
return boombox.HTMLAudio.prototype.setCurrentTime.apply(this, arguments);
};
/**
* Check disposed
*
* @memberof HTMLVideo
* @method
* @name isDisposed
*/
HTMLVideo.prototype.isDisposed = function () {
return boombox.HTMLAudio.prototype.isDisposed.apply(this, arguments);
};
//////////
/**
* Dispose (apply HTMLAudio)
*
* @memberof HTMLVideo
* @method
* @name dispose
*/
HTMLVideo.prototype.dispose = function () {
return boombox.HTMLAudio.prototype.dispose.apply(this, arguments);
};
return HTMLVideo;
})();
//////////////////////////////////
// WebAudio Class
var WebAudio = (function () {
function WebAudio(name, parent) {
/**
* logger
* @memberof WebAudio
* @name logger
*/
this.logger = new Logger('WebAudio');
/**
* Audio name
*
* @memberof WebAudio
* @name name
* @type {String}
*/
this.name = name;
/**
* SetTimeout#id pool running
* @memberof WebAudio
* @name _timer
*/
this._timer = {};
/**
* AudioSprite option data
* @memberof WebAudio
* @name sprite
*/
this.sprite = undefined;
/**
* AudioBuffer in use
*
* @memberof WebAudio
* @name buffer
* @type {AudioBuffer}
*/
this.buffer = undefined;
/**
* AudioBufferSourceNode in use
*
* @memberof WebAudio
* @name source
* @type {AudioBufferSourceNode}
*/
this.source = undefined;
/**
* WebAudioContext in use
* shortcut: boombox.WEB_AUDIO_CONTEXT
*
* @memberof WebAudio
* @name ctx
* @type {AudioContext}
*/
this.ctx = boombox.WEB_AUDIO_CONTEXT;
/**
* WebAudioContext.GainNode instance
* @memberof WebAudio
* @name gainNode
* @type {GainNode}
*/
this.gainNode = this.ctx.createGain();
/**
* State of Audio
*
* @memberof WebAudio
* @name state
* @type {Object}
*/
this.state = {
time: {
playback: undefined, // Playback start time (unixtime)
pause: undefined, // Seek time paused
progress: 0 // Sound progress time
},
loop: boombox.LOOP_NOT, // Loop playback flags
power: boombox.POWER_ON, // power flag
loaded: false, // Audio file is loaded
error: undefined // error state
};
if (parent) {
var sprite_n = getSpriteName(name);
this.parent = parent; // ref
//this.state = this.parent.state; // ref
this.state.loaded = this.parent.state.loaded; // not ref copy
this.state.error = this.parent.state.error; // not ref copy
//this.$el = this.parent.$el; // ref
//this.onEnded = this.parent.onEnded; // TODO: ref
// change Sprite
var current = parent.sprite.options[sprite_n.suffix];
this.sprite = new Sprite(undefined, current); // new
}
}
//////
/**
* Loading web audio
*
* @memberof WebAudio
* @name load
* @method
* @param {Object} options
* @param {Function} callback
* @return {WebAudio}
* @example
* .load({
* src: 'http://example.com/audio.m4a',
* timeout: 15 * 1000
* }, function callback() {});
*
*/
WebAudio.prototype.load = function (options, callback) {
var self = this;
options = options || {};
var cb = callback || none;
if (this.parent) {
//this.buffer = this.parent.buffer; // ref
cb(null, this);
return this;
}
if (options.spritemap) { // Sprite ON
this.sprite = new Sprite(options.spritemap);
delete options.spritemap;
}
for (var k in options) {
var v = options[k];
this.logger.trace('WebAudio attribute:', k, v);
}
var http = new XMLHttpRequest();
http.onload = function (e) {
if (e.target.status.toString().charAt(0) === '2') {
self.ctx.decodeAudioData(
http.response,
function (buffer) {
if (!buffer) {
self.logger.error('error decode file data: ', options.url);
return cb(new Error('error decode file data'), self);
}
self.buffer = buffer;
self.state.loaded = true;
/////
// audiosprite propagation
if (self.isParentSprite()) {
for (var k in boombox.pool) {
if (!!~k.indexOf(self.name + SPRITE_SEPARATOR)) {
boombox.pool[k].buffer = buffer; // ref buffer
boombox.pool[k].state.loaded = self.state.loaded; // not ref copy
}
}
}
return cb(null, self);
},
function () {
return cb(new Error('fail to decode file data'), self);
}
);
} else {
self.logger.error('fail to load resource: ', options.url);
return cb(new Error('fail to load resource'), self);
}
};
//http.timeout = 1;
var timeout = options.timeout || 15 * 1000;
// communication time-out
setTimeout(function () {
if (http.readyState !== 4) {
http.abort();
cb(new Error('load of web audio file has timed out. timeout:' + timeout), self);
cb = none;
}
}, timeout);
http.open('GET', options.src, true);
http.responseType = 'arraybuffer';
// setInterval(function () {
// console.error(self.name,
// 'playback', self.isPlayback(),
// 'stop:', self.isStop(),
// 'pause:', self.isPause(),
// 'power:', self.state.power
// );
// }, 100);
/////////////////////
/// XHR send!!
http.send();
return this;
};
//////////
/**
* Is use. (apply HTMLAudio)
*
* @memberof WebAudio
* @method
* @name isUse
* @return {Boolean}
*/
WebAudio.prototype.isUse = function () {
return boombox.HTMLAudio.prototype.isUse.apply(this, arguments);
};
/**
* Is playing. (apply HTMLAudio)
*
* @memberof WebAudio
* @method
* @name isPlayback
* @return {Boolean}
*/
WebAudio.prototype.isPlayback = function () {
return !!this.source && !!this.state.time.playback && !this.state.time.pause && (this.source.playbackState === 1 || this.source.playbackState === 2);
};
/**
* Is stoped. (apply HTMLAudio)
*
* @memberof WebAudio
* @method
* @name isStop
* @return {Boolean}
*/
WebAudio.prototype.isStop = function () {
return !this.source;
};
/**
* Is paused. (apply HTMLAudio)
*
* @memberof WebAudio
* @method
* @name isPause
* @return {Boolean}
*/
WebAudio.prototype.isPause = function () {
return boombox.HTMLAudio.prototype.isPause.apply(this, arguments);
};
/**
* Loop flag (apply HTMLAudio)
*
* @memberof WebAudio
* @method
* @name isLoop
* @return {Interger}
*/
WebAudio.prototype.isLoop = function () {
return boombox.HTMLAudio.prototype.isLoop.apply(this, arguments);
};
/**
* Is sprite of the parent (apply HTMLAudio)
*
* @memberof WebAudio
* @method
* @name isParentSprite
* @return {Boolean}
*/
WebAudio.prototype.isParentSprite = function () {
return boombox.HTMLAudio.prototype.isParentSprite.apply(this, arguments);
};
/**
* Is sprite (apply HTMLAudio)
*
* @memberof WebAudio
* @method
* @name isSprite
* @return {Boolean}
*/
WebAudio.prototype.isSprite = function () {
return boombox.HTMLAudio.prototype.isSprite.apply(this, arguments);
};
/**
* Clear all the setTimeout (apply HTMLAudio)
*
* @memberof WebAudio
* @method
* @name clearTimerAll
* @return {WebAudio}
*/
WebAudio.prototype.clearTimerAll = function () {
return boombox.HTMLAudio.prototype.clearTimerAll.apply(this, arguments);
};
/**
* Clear specified setTimeout (apply HTMLAudio)
*
* @memberof WebAudio
* @method
* @name clearTimer
* @param {String} name
* @return {Interger}
*/
WebAudio.prototype.clearTimer = function (name) {
return boombox.HTMLAudio.prototype.clearTimer.apply(this, arguments);
};
/**
* Save the specified setTimeout (apply HTMLAudio)
*
* @memberof WebAudio
* @method
* @name setTimer
* @param {String} name
* @param {Interger} id setTimeout#id
* @return {Interger}
*/
WebAudio.prototype.setTimer = function (name, id) {
return boombox.HTMLAudio.prototype.setTimer.apply(this, arguments);
};
//////////
/**
* audio play.
*
* @memberof WebAudio
* @method
* @name play
* @return {WebAudio}
*/
WebAudio.prototype.play = function (resume) {
var self = this;
if (!this.isUse()) {
this.logger.debug('skip play:', this.name, 'state can not be used');
return this;
} // skip!!
if (this.isPlayback()) {
this.logger.debug('skip play:', this.name, 'is playing');
return this;
}
if (!resume) {
this.sourceDispose();
}
this.source = this.ctx.createBufferSource();
if (this.state.loop === boombox.LOOP_NATIVE) {
this.source.loop = this.state.loop;
}
this.source.buffer = this.buffer;
this.source.connect(this.gainNode);
this.gainNode.connect(this.ctx.destination);
var type = 'play';
var fn = none;
var start = 0;
this.state.time.playback = Date.now(); // Playback start time (ms)
if (resume && this.state.time.pause) {
// resume
start = this.state.time.pause / 1000; // Start position (sec)
//this.logger.trace('start:', start);
//this.logger.warn('interval:', interval);
if (this.isSprite()) {
var pause_sec = this.state.time.pause / 1000; // (sec)
start = this.sprite.current.start + pause_sec; // Start position (sec)
var interval = Math.ceil((self.sprite.current.term - pause_sec) * 1000); // (ms)
fn = function () {
self.setTimer('play', setTimeout(function () {
self.stop();
self._onEnded(); // fire onended evnet
}, interval));
};
}
this.state.time.pause = undefined;
} else { // zero
if (this.isSprite()) {
start = this.sprite.current.start;
fn = function () {
var interval = Math.ceil(self.sprite.current.term * 1000);
self.setTimer('play', setTimeout(function () {
self.stop();
self._onEnded(); // fire onended evnet
}, interval));
};
}
}
this.logger.debug(type, this.name, 'offset:', start);
fn();
var duration = this.buffer.duration - start;
if (!this.isSprite()) {
if (this.source.hasOwnProperty('onended')) {
this.source.onended = function (e) {
self._onEnded(e);
};
} else {
var interval = Math.ceil(duration * 1000);
this.setTimer('play', setTimeout(function () {
self.stop();
self._onEnded();
}, interval));
}
}
if (this.source.start) {
this.logger.debug('use source.start()', this.name, start, duration);
this.source.start(0, start, this.buffer.duration);
} else {
if (this.isSprite()) {
duration = self.sprite.current.term;
}
this.logger.debug('use source.noteGrainOn()', this.name, start, duration);
this.source.noteGrainOn(0, start, duration);
}
return this;
};
/**
* audio stop.
*
* @memberof WebAudio
* @method
* @name stop
* @return {WebAudio}
*/
WebAudio.prototype.stop = function () {
if (!this.state.loaded || typeof this.state.error !== 'undefined') {
this.logger.debug('skip stop:', this.name, 'state can not be used');
return this;
} // skip!!
this.logger.debug('stop:', this.name);
this.clearTimer('play');
if (this.source) {
if (this.source.stop) {
this.logger.debug('stop: use source.stop()', this.name);
this.source.stop(0);
} else {
this.logger.debug('stop: use source.noteOff()', this.name);
this.source.noteOff(0);
}
}
this.sourceDispose();
return this;
};
/**
* audio pause.
*
* @memberof WebAudio
* @method
* @name pause
* @return {WebAudio}
*/
WebAudio.prototype.pause = function () {
if (!this.isUse()) {
this.logger.debug('skip pause:', this.name, 'state can not be used');
return this;
} // skip!!
if (!this.source) {
this.logger.debug('skip pause, Not playing.');
return this;
}
if (this.state.time.pause) {
this.logger.debug('skip pause, It is already paused.');
return this;
}
var now = Date.now();
var offset = now - this.state.time.playback;
this.state.time.pause = this.state.time.progress + offset; // Pause time(ms)
this.state.time.progress += offset;
this.logger.trace('state.time.pause:', this.state.time.pause, 'now:', now, 'state.time.playback', this.state.time.playback);
this.logger.debug('pause:', this.name);
this.clearTimer('play');
if (this.source) {
if (this.source.stop) {
this.logger.debug('pause: use source.stop()', this.name);
this.source.stop(0);
} else {
this.logger.debug('pause: use source.noteOff()', this.name);
this.source.noteOff(0);
}
}
return this;
};
/**
* audio resume.
*
* @memberof WebAudio
* @method
* @name resume
* @return {WebAudio}
*/
WebAudio.prototype.resume = function () {
if (!this.isUse()) {
this.logger.debug('skip resume:', this.name, 'state can not be used');
return this;
} // skip!!
if (this.state.time.pause) {
this.play(true);
}
return this;
};
/**
* audio re-play.
*
* @memberof WebAudio
* @method
* @name replay
* @return {WebAudio}
*/
WebAudio.prototype.replay = function () {
if (!this.isUse()) {
this.logger.debug('skip replay:', this.name, 'state can not be used');
return this;
} // skip!!
this.logger.debug('replay:', this.name);
this.clearTimer('play');
this.sourceDispose();
this.play();
return this;
};
/**
* audio change volume.
*
* @memberof WebAudio
* @method
* @name volume
* @return {WebAudio}
*/
WebAudio.prototype.volume = function (v) {
this.logger.trace('volume:', this.name, 'volume:', v);
this.gainNode.gain.value = v;
};
//////////
/**
* Audio.ended events
*
* @memberof WebAudio
* @method
* @name _onEnded
* @param {Event} e event
*/
WebAudio.prototype._onEnded = function (e) {
// check dispose
if (this.isDisposed()) {
return;
}
var self = this;
var now = Date.now();
// skip if sounds is not ended
if (self.source && Math.abs((now - self.state.time.playback + self.state.time.progress) / 1000 - self.source.buffer.duration) >= boombox.THRESHOLD) {
self.logger.debug('skip if sounds is not ended', self.name);
return;
}
this.logger.trace('onended fire!', this.name);
this.state.time.playback = undefined;
this.onEnded(e); // fire user ended event!!
// check dispose
if (this.isDisposed()) {
return;
}
if (this.state.loop && typeof this.state.time.pause === 'undefined') {
this.logger.trace('onended loop play.');
this.play();
} else {
this.sourceDispose();
}
};
/**
* Override Audio.ended events
*
* @memberof WebAudio
* @method
* @name onEnded
* @param {Event} e event
*/
WebAudio.prototype.onEnded = none;
//////////
/**
* Set loop flag
*
* @memberof WebAudio
* @method
* @name setLoop
* @param {Interger} loop loop flag (boombox.LOOP_XXX)
* @return {WebAudio}
*/
WebAudio.prototype.setLoop = function (loop) {
if (!this.isUse()) { return this; } // skip!!
this.state.loop = loop;
if (loop === boombox.LOOP_NOT) {
if (this.source) {
this.source.loop = boombox.LOOP_NOT;
}
//} else if (loop === boombox.LOOP_ORIGINAL) {
} else if (loop === boombox.LOOP_NATIVE) {
if (this.source) {
this.source.loop = loop;
}
}
return this;
};
/**
* Change power on/off (apply HTMLAudio)
*
* @memberof WebAudio
* @method
* @name power
* @param {Boolean} p power on/off. boombox.(POWER_ON|POWER_OFF)
* @return {WebAudio}
*/
WebAudio.prototype.power = function (p) {
return boombox.HTMLAudio.prototype.power.apply(this, arguments);
};
//////////
/**
* Dispose AudioBufferSourceNode
*
* @memberof WebAudio
* @method
* @name sourceDispose
*/
WebAudio.prototype.sourceDispose = function () {
this.logger.trace('source dispose', this.name);
this.source && this.source.disconnect();
this.source = undefined;
this.state.time.playback = undefined;
this.state.time.pause = undefined;
this.state.time.progress = 0;
};
/**
* Check disposed
* @memberof WebAudio
* @method
* @name isDisposed
*/
WebAudio.prototype.isDisposed = function () {
this.logger.trace('check dispose flag', !!this.state);
return !this.state;
};
/**
* Dispose
*
* @memberof WebAudio
* @method
* @name dispose
*/
WebAudio.prototype.dispose = function () {
this.logger.trace('WebAudio dispose', this.name);
delete this.buffer;
//delete this.state.time.playback;
//delete this.state.time.pause;
//delete this.source;
this.sourceDispose();
delete this.source;
this.clearTimerAll();
delete this._timer;
delete this.state.time;
delete this.state.loop;
delete this.state.power;
delete this.state.loaded;
delete this.state.error;
delete this.state;
this.parent = null;
delete this.parent;
if (this.sprite && this.sprite.dispose) {
this.sprite.dispose();
}
delete this.sprite;
delete this.name;
this.gainNode && this.gainNode.disconnect && delete this.gainNode;
this.ctx = null;
};
return WebAudio;
})();
var Sprite = (function () {
function Sprite(options, current) {
/**
* logger
* @memberof Sprite
* @type {Logger}
* @name logger
*/
this.logger = new Logger('Sprite ');
/**
* current options
* @memberof Sprite
* @type {Object}
* @name current
*/
this.current = current; // target sprite
/**
* options
* @memberof Sprite
* @name options
* @type {Object}
*/
this.options = options;
if (!current) { // parent
for (var k in this.options) {
this.options[k].term = this.options[k].end - this.options[k].start;
}
}
}
//////////
/**
* Dispose
* @memberof Sprite
* @method
* @name dispose
*/
Sprite.prototype.dispose = function () {
this.options = null;
delete this.options;
delete this.current;
};
return Sprite;
})();
// Building
boombox.HTMLAudio = HTMLAudio;
boombox.HTMLVideo = HTMLVideo;
boombox.WebAudio = WebAudio;
if (isRequire) {
define([], function () {
return boombox;
});
} else {
w.boombox = boombox;
}
})(window);