// audio.js
// 0.7
// Stephen Band
//
// webdev.stephband.info/audio
//
// Re-writes the Audio constructor to 'invisibly' choose native audio or QuickTime plugin, based on the support for the file type.
// Wraps html5 attributes and methods onto QuickTime objects so you can interact with them using the same API.
//
// Dependencies:
// PluginDetect by Eric Gerds
// www.pinlady.net/PluginDetect
// 
// Resources:
// http://developer.apple.com/documentation/QuickTime/Conceptual/QTScripting_Javascript/bQTScripting_JavaScri_Document/QuickTimeandJavaScri.html
// https://developer.mozilla.org/En/Using_audio_and_video_in_Firefox
// https://developer.mozilla.org/En/nsiDOMHTMLMediaElement
// http://www.pinlady.net/PluginDetect/PluginDet%20Generator.htm


// For testing - uncomment to switch off your browser's native Audio contructor
// Audio = false;


// QuickTime constructor
// 0.8
// Stephen Band
//
// Example usage
// var sound = new QuickTime('file.m4a');
// The resulting <object> is automatically added to the DOM, as you can't interact with a QuickTime plugin instance otherwise.
//
// Tested in IE6/IE8/FF3.5/Safari4/Opera9.6 - Not yet tested in IE7. Limitations:
// In IE, DOM events are not sent until content is fully loaded. You can still, however, poll the QuickTime.GetPluginStatus() method.
// In IE, any QuickTime objects inserted dynamically (after window.onload) will not send DOM events. You can still call their methods, but they won't notify changes.

(function(){

    var quicktime = {
        width: 240,
        height: 16,
        behaviourId: 'qtBehaviourElement',
        init: function() {
            if (isIE()) {
                var that = this;
                if (!document.getElementById(that.behaviourId)) {
                    // Put QT's behaviour object into the <head>
                    var head = document.getElementsByTagName('head')[0];
                    var frag = document.createElement('div');
                    frag.innerHTML = that.behaviourHtml();
                    head.appendChild(frag.lastChild);
                }
                // IE6 (and maybe IE7) Only responds to QT's event bindings if they have previously been bound (to ANY other element!  WTF?)
                // Might as well bind to the qt behaviour object, then, since it's kicking around...
                // You have to wait for document ready to do this (might be better on DOM ready)
                window.onload = function(){that.bindEventListeners(that.behaviourId);};
            }
            return PluginDetect.getVersion('QuickTime');
        },
        // What does QuickTime do, by default, on hearing it's own events?
        actions: {
            qt_begin:            function(){ if(window.console && console.log) console.log('qt_begin'); },               // The plug in has been instantiated and can interact with JavaScript.
            qt_loadedmetadata:   function(){ if(window.console && console.log) console.log('qt_loadedmetadata'); },      // The movie header information has been loaded or created. The duration, dimensions, looping state, and so on are now known.
            qt_loadedfirstframe: function(){ if(window.console && console.log) console.log('qt_loadedfirstframe'); },    // The first frame of the movie has been loaded and can be displayed. (The frame is displayed automatically at this point.)
            qt_canplay:          function(){ if(window.console && console.log) console.log('qt_canplay'); },             // Enough media data has been loaded to begin playback (but not necessarily enough to play the entire file without pausing).
            qt_canplaythrough:   function(){ if(window.console && console.log) console.log('qt_canplaythrough'); },      // Enough media data has been loaded to play through to the end of the file without having to pause to buffer, assuming data continues to come in at the current rate or faster.
            qt_durationchange:   function(){ if(window.console && console.log) console.log('qt_durationchange'); },      // The media file’s duration is available or has changed. (A streaming movie, a SMIL movie, or a movie with a QTNEXT attribute may load multiple media segments or additional movies, causing a duration change.)
            qt_load:             function(){ if(window.console && console.log) console.log('qt_load'); },                // All media data has been loaded.
            qt_ended:            function(){ if(window.console && console.log) console.log('qt_ended'); },               // Playback has stopped because end of the file was reached. (If the movie is set to loop, this event will not occur.)
            qt_error:            function(){ if(window.console && console.log) console.log('qt_error'); },               // An error occurred while loading the file. No more data will be loaded.
            qt_pause:            function(){ if(window.console && console.log) console.log('qt_pause'); },               // Playback has paused. (This happens when the user presses the pause button before the movie ends.)
            qt_play:             function(){ if(window.console && console.log) console.log('qt_play'); },                // Playback has begun.
            qt_progress:         function(){ if(window.console && console.log) console.log('qt_progress'); },            // More media data has been loaded. This event is fired no more than three times per second.
            qt_waiting:          function(){ if(window.console && console.log) console.log('qt_waiting'); },             // Playback has stopped because no more media data is available, but more data is expected. (This usually occurs if the user presses the play button prior to the qt_canplaythrough event.)
            qt_stalled:          function(){ if(window.console && console.log) console.log('qt_stalled'); },             // No media has been received for approximately three seconds.
            qt_timechanged:      function(){ if(window.console && console.log) console.log('qt_timechanged'); },         // The current time has been changed (current time is indicated by the position of the playhead).
            qt_volumechange:     function(){ if(window.console && console.log) console.log('qt_volumechange'); }         // The audio volume or mute attribute has changed.
        },
        // Extra methods given to the DOMElement
        methods: {},
        bindEventListeners: function (id) {
            // is id an id or a DOMElement ?
            var obj = (typeof(id) === 'string') ? document.getElementById(id) : id,
                actions = this.actions;
            if (obj) {
                for (i in actions) {
                    this.addListener(obj, i, actions[i], false);
                }
            }
        },
        addListener: function addListener(obj, evt, handler, captures) {
            // for Standards browsers
            if ( document.addEventListener ) obj.addEventListener(evt, handler, captures);
            // for IE
            else obj.attachEvent('on'+evt, handler);
        },
        behaviourIsInDOM: function () {
            var obj = document.getElementById( this.idBehaviour );
            return obj && obj.getAttribute('classid') === "clsid:CB927D12-4FF7-4a9e-A169-56E4B8A75598";
        },
        behaviourHtml: function(){
            return '<object id="' + this.behaviourId + '" classid="clsid:CB927D12-4FF7-4a9e-A169-56E4B8A75598"></object>'
        },
        objectHtml: function(url, options) {
            var o = options,
                obj = document.createElement('div'),
                html = ((isIE()) ?
                    // Use this for IE
                    // Beware the spelling of 'behavior'!
                    '<object  classid="clsid:02BF25D5-8C17-4B23-BC80-D3488ABDDC6B" width="'+ o.width +'" height="'+ o.height +'" codebase="http://www.apple.com/qtactivex/qtplugin.cab#version='+ o.activex +'" '+((o.id) ? 'id="'+o.id+'"' : '')+' style="behavior:url(\'#'+ this.behaviourId +'\')">'+
                        '<param name="src" value="'+ url +'" />' :
                    // This for standards
                    '<object '+((o.id) ? 'id="'+o.id+'"' : '')+' type="video/quicktime" data="'+ url +'" width="'+ o.width +'" height="'+ o.height +'">' ) +
                    // And this for all
                        '<param name="enablejavascript" value="true" />'+
                        '<param name="postdomevents" value="true" />'+
                        '<param name="autoplay" value="'+ o.autoplay +'" />'+
                        '<param name="controller" value="'+ o.controls +'" />'+
                    '</object>';
            return html;
        },
        make: function(url, opt){
            var options = opt || {},
                o = {
                    id: options.id || false,
                    width: options.width || this.width,
                    height: options.height || this.height,
                    controller: (options.controller || options.controller === false) ? options.controller : 'true',
                    autoplay: (!options.controller) ? 'false' : options.controller,
                    activex: options.activex || '7,3,0,0'
                },
                html = this.objectHtml(url, o),
                temp = document.createElement('div'),
                qtObj, i;
            
            temp.innerHTML = html;
            qtObj = temp.lastChild;
            
            // Copy methods to it
            for (i in this.methods) {
                qtObj[i] = this.methods[i];
            }
            
            // You can't call methods or bind listeners to it unless it's in the DOM
            document.body.appendChild(qtObj);
            
            // Bind actions to qt events
            if (isIE()) {
                // Poll the quicktime instance until it is ready...
                // This works in IE8, but IE6 still won't bind those pesky listeners - unless they've been bound to something else - ANYTHING else - before (like at window.onload, for example)
                var qt = this,
                    t = setInterval(function(){
                    // I'm anticipating that this may throw errors in IE sometimes, as IE has trouble asking QuickTime for its methods during instantiation
                    // if it does, wrap it in a try ... catch
                    var status = qtObj.GetPluginStatus();
                    
                    if (status === "Complete") {
                        clearInterval(t);
                        if (window.console && console.log) console.log(status);
                        qt.bindEventListeners(qtObj);
                    }
                }, 5);
            }
            else {
                // Standards browsers have no problem binding events sensibly
                this.bindEventListeners(qtObj);
            }
            
            return qtObj;
        }
    }
    
    function isIE(){
        // Uses IE conditional comments to see if we're dealing with IE
        if (this.flag === undefined) {
            var obj = document.createElement('div');
            // Stick a IE conditional in the DOM, check if it's contents become nodes, and remove it again
            obj.innerHTML ='<!--[if IE]><div id="ie_test"></div><![endif]-->';
            document.body.appendChild(obj);
            this.flag = !!document.getElementById("ie_test");
            document.body.removeChild(obj);
        }
        return this.flag;
    }
    
    // Run init as soon as you can (it is needed for QuickTime in IE to be able to send DOM Events) 
    var version = quicktime.init();
    
    if (version) {
        // Declare QuickTime constructor (in global scope)
        QuickTime = quicktime.make;
        QuickTime.prototype = quicktime;
        
        // Give QuickTime object some information about the QuickTime player
        QuickTime.version = version;
        QuickTime.support = {
            m4a:    true,
            m4p:    true,
            mp3:    true,
            mp4:    true,
            mov:    true,
            aif:    true,
            aiff:   true,
            wav:    true
        };
    }

})();


// Audio constructor
// 0.7
// Stephen Band
//
// Example usage
// var sound = new Audio('file.m4a');

(function(AudioObject){
    // AudioObject is now the Audio constructor (if there is one)
    
    var regex = {
            fileExt:    /\.\w{1,6}$/                                    // any 1-6 character preceded by a '.' at the end of a string
        };
    
    // Native Audio constructor detection inspired by a blog post by Mark Boas
    // http://html5doctor.com/native-audio-in-the-browser/ 
    if (AudioObject) {
        var native = {
            tag: !!(document.createElement('audio').canPlayType)
        };
        
        try {
            var audioTest = new AudioObject('');
            
            native.obj = !!(audioTest.canPlayType);
            native.basic = !!(!native.obj ? audioTest.play : false);
            native.support = (native.obj) ? {
                ogg:    audioTest.canPlayType("audio/ogg") !== "no"     && audioTest.canPlayType("audio/ogg") !== '',
                m4a:    audioTest.canPlayType("audio/x-m4a") !== "no"   && audioTest.canPlayType("audio/x-m4a") !== '',
                mp3:    audioTest.canPlayType("audio/mpeg") !== "no"    && audioTest.canPlayType("audio/mpeg") !== '',
                wav:    audioTest.canPlayType("audio/wav") !== "no"     && audioTest.canPlayType("audio/wav") !== '',
                aif:    audioTest.canPlayType("audio/aiff") !== "no"    && audioTest.canPlayType("audio/aiff") !== ''
            } : {};
        }
        catch (error) {
            native.obj = false;
            native.basic = false;
        }
    }
    
    // Setup QuickTime prototype to emulate W3C Audio spec (loosely! There are some miracles we can't do.)
    // (I've yet to do this without overriding the original QuickTime object's prototype)
    if (QuickTime) {
        QuickTime.prototype.actions = {
            qt_begin:            function(){ this.readyState = 0; this.networkState = 0; },                     // The plug in has been instantiated and can interact with JavaScript.
            qt_loadedmetadata:   function(){ this.readyState = 1; this.networkState = 2; },                     // The movie header information has been loaded or created. The duration, dimensions, looping state, and so on are now known.
            qt_loadedfirstframe: function(){ this.readyState = 2; this.networkState = 3; },                     // The first frame of the movie has been loaded and can be displayed. (The frame is displayed automatically at this point.)
            qt_canplay:          function(){ this.readyState = 3; },                                            // Enough media data has been loaded to begin playback (but not necessarily enough to play the entire file without pausing).
            qt_canplaythrough:   function(){ this.readyState = 4; },                                            // Enough media data has been loaded to play through to the end of the file without having to pause to buffer, assuming data continues to come in at the current rate or faster.
            qt_durationchange:   function(){ this.GetDuration() / this.GetTimeScale() },                        // The media file’s duration is available or has changed. (A streaming movie, a SMIL movie, or a movie with a QTNEXT attribute may load multiple media segments or additional movies, causing a duration change.)
            qt_load:             function(){ this.networkState = 4; },                                          // All media data has been loaded.
            qt_ended:            function(){ this.ended = true; },                                              // Playback has stopped because end of the file was reached. (If the movie is set to loop, this event will not occur.)
            qt_error:            function(){ this.error = this.GetPluginStatus() },                                  // An error occurred while loading the file. No more data will be loaded.
            qt_pause:            function(){ this.paused = true; },                                             // Playback has paused. (This happens when the user presses the pause button before the movie ends.)
            qt_play:             function(){ this.paused = false; },                                            // Playback has begun.
            qt_timechanged:      function(){ this.currentTime = this.GetTime() / this.GetTimeScale(); },        // The current time has been changed (current time is indicated by the position of the playhead).
            qt_volumechange:     function(){                                                                    // The audio volume or mute attribute has changed.
                var volume = this.GetVolume() / 256;
                this.muted = (volume <= 0);
                this.volume = volume;
            }
        //    qt_progress:         function(){ if(window.console && console.log) console.log('qt_progress'); },   // More media data has been loaded. This event is fired no more than three times per second.
        //    qt_waiting:          function(){ if(window.console && console.log) console.log('qt_waiting'); },    // Playback has stopped because no more media data is available, but more data is expected. (This usually occurs if the user presses the play button prior to the qt_canplaythrough event.)
        //    qt_stalled:          function(){ if(window.console && console.log) console.log('qt_stalled'); },    // No media has been received for approximately three seconds.
        };
        QuickTime.prototype.methods = {
            canPlayType: function(type){
                var mime = {
                    'audio/x-m4a':           true,
                    'audio/x-mp4':           true,
                    'audio/mpeg':            true,
                    'audio/wav':             true,
                    'audio/aiff':            true,
                    'video/mov':             true,
                    'video/quicktime':       true,
                    'application/quicktime': true
                }
                return mime[type];
            },
            play: function(){ 
                var rate = this.playbackRate,
                    volume = Math.ceil(this.volume * 256);
                this.SetRate(rate);
            },
            pause: function(){ this.Stop(); },
            load: function(){
                var src = this.src;
                // SetURL requires a full url as src
                this.networkState = 0;
                this.readyState = 0;
                this.SetURL(src);
            },
            // These currently await a proper W3C spec
            addCueRange: function(name, start, end, pauseOnExit, enterCallback, exitCallback){
                var timescale = this.GetTimeScale();
                
                if (start)  { this.SetStartTime( start*timescale ); }
                if (end)    { this.SetEndTime( end*timescale ); }
            },
            removeCueRanges: function(name){
                var duration = this.GetDuration();
                
                this.SetStartTime( 0 );
                this.SetEndTime( duration );
            }
        };
    }
    
    // The new Audio constructor, declared in global scope
    Audio = function(src){
        var obj,
            options = arguments[1] || {},
            methods;
        var ext = src.match(regex.fileExt)[0].replace(/./, '');
        
        if (native && native.obj && native.support[ext])  {
            return new AudioObject(src);
        }
        
        if (QuickTime && QuickTime.version && QuickTime.support[ext]) {
            obj = new QuickTime(src, options);
        }
        
        else if (PluginDetect.getVersion('Flash') && ext === 'mp3') {
            obj = flash.make();  // Some form of SWFObject support
        }
        else {};
        
        obj.autoplay            = options.autoplay || false;   // boolean 	true if playback should automatically begin as soon as enough media is available to do so without interruption.
        obj.controls            = !(!options.controller || false);   // boolean 	true if user interface for controlling the media should be presented.
        obj.currentTime         = 0;          // float 	The current playback time, in seconds.  Setting this value will seek the media to the new time.
        obj.defaultPlaybackRate = 1;          // float 	The default playback rate for the media.  The Ogg backend does not support this.  1.0 is "normal speed," values lower than 1.0 make the media play slower than normal, higher values make it play faster.  The value 0.0 is invalid and throws a NOT_SUPPORTED_ERR exception.
        obj.muted               = false;      // boolean 	true if the audio is muted, otherwise false.
        obj.networkState        = 0;          // unsigned short  The current state of the fetch of the media. Read only.
        obj.playbackRate        = 1;          // float 	The current rate at which the media is being played back.  This is used to implement user controls for fast forward, slow motion, and so forth.
        obj.readyState          = 0;          // unsigned short 	The media's current readiness state.  Read only.
        obj.src                 = src;        // DOMString 	The URL of the media to present.
        obj.volume              = 1;          // float 	The current audio volume, from 0.0 (silent) to 1.0 (maximum)
        obj.loop                = false;      // The loop  attribute is a boolean attribute
        obj.paused              = true;       // boolean 	true if the media playback is currently paused.  Read only.
//        obj.currentSrc          =       ;   // DOMString 	The absolute URL of the chosen media resource (if, for example, the server selects a media file based on the resolution of the user's display), or an empty string if the networkState is EMPTY. Read only.
//        obj.duration            =       ;   // float The length of the media in seconds, or zero if no media data is available.  If the media data is available but the length is unknown, this value is NaN.  If the media is streamed and has no predefined length, the value is Inf.  Read only.
//        obj.totalBytes          =       ;   // unsigned long 	The length, in bytes, of the media resource.  If the length is unknown, or the content is streamed and therefore has no specified length, this value is 0.  Read only.
//        obj.seeking             =       ;   // boolean 	true if the media is in the process of seeking to a new position, otherwise false. Read only.
//        obj.ended               =       ;   // boolean 	true if playback has finished.  Read only.
//        obj.error               =       ;   // nsIDOMHTMLMediaError 	The media's error status.  Read only.
//        obj.autobuffer          =       ;   // boolean 	true if the media should automatically begin to download, regardless of whether or not autoplay is true.
//        obj.buffered            =       ;   // TimeRanges 	A static, normalized TimeRanges object describing the time ranges of the media that have been buffered.  Read only.  Unimplemented
//        obj.bufferedBytes       =       ;   // ByteRanges 	A static, normalized ByteRanges object describing the byte ranges of the media that have been buffered.  Read only.  Unimplemented
//        obj.bufferingRate       =       ;   // float 	The average number of bits per second for the current download over the past few seconds.  If there is no download in progress, this value is 0.0.  Read only.  Unimplemented
//        obj.bufferingThrottled  =       ;   // boolean 	true if Firefox is intentionally throttling the bandwidth used by the download (such as when the download is paused), otherwise false. Read only.  Unimplemented
        
        return obj;
    }
    
})(Audio);