betajs/betajs-media-components

View on GitHub
src/dynamics/audio_player/player/player.js

Summary

Maintainability
F
1 wk
Test Coverage
Scoped.define("module:AudioPlayer.Dynamics.Player", [
    "dynamics:Dynamic",
    "module:Assets",
    "module:AudioVisualization",
    "browser:Info",
    "browser:Dom",
    "media:AudioPlayer.AudioPlayerWrapper",
    "base:Types",
    "base:Objs",
    "base:Strings",
    "base:Time",
    "base:Timers",
    "base:States.Host",
    "base:Classes.ClassRegistry",
    "base:Async",
    "module:AudioPlayer.Dynamics.PlayerStates.Initial",
    "module:AudioPlayer.Dynamics.PlayerStates",
    "browser:Events"
], [
    "module:Common.Dynamics.Settingsmenu",
    "module:AudioPlayer.Dynamics.Message",
    "module:AudioPlayer.Dynamics.Loader",
    "module:AudioPlayer.Dynamics.Controlbar",
    "dynamics:Partials.EventPartial",
    "dynamics:Partials.OnPartial",
    "dynamics:Partials.TogglePartial",
    "dynamics:Partials.StylesPartial",
    "dynamics:Partials.TemplatePartial",
    "dynamics:Partials.HotkeyPartial"
], function(Class, Assets, AudioVisualization, Info, Dom, AudioPlayerWrapper, Types, Objs, Strings, Time, Timers, Host, ClassRegistry, Async, InitialState, PlayerStates, DomEvents, scoped) {
    return Class.extend({
            scoped: scoped
        }, function(inherited) {
            return {

                template: "<%= template(dirname + '/player.html') %>",

                attrs: {
                    /* CSS */
                    "css": "ba-audioplayer",
                    "csscommon": "ba-commoncss",
                    "cssplayer": "ba-player",
                    "iecss": "ba-audioplayer",
                    "cssloader": "",
                    "cssmessage": "",
                    "csscontrolbar": "",
                    "width": "",
                    "height": "",
                    /* Themes */
                    "theme": "",
                    "csstheme": "",
                    "themecolor": "",
                    /* Dynamics */
                    "dynloader": "audioplayer-loader",
                    "dynmessage": "audioplayer-message",
                    "dyncontrolbar": "audioplayer-controlbar",
                    "dynsettingsmenu": "common-settingsmenu",

                    /* Templates */
                    "tmplloader": "",
                    "tmplmessage": "",
                    "tmplcontrolbar": "",
                    "tmplsettingsmenu": "",

                    /* Attributes */
                    "source": "",
                    "sources": [],
                    "sourcefilter": {},
                    "playlist": null,
                    "volume": 1.0,
                    "title": "",
                    "initialseek": null,
                    "visibilityfraction": 0.8,
                    "unmuted": false, // Reference to Chrome renewed policy, we have to setup mute for auto plyed players.

                    /* Configuration */
                    "reloadonplay": false,
                    "playonclick": true,
                    "pauseonclick": true,
                    /* Options */
                    "rerecordable": false,
                    "submittable": false,
                    "autoplay": false,
                    "preload": false,
                    "loop": false,
                    "loopall": false,
                    "ready": true,
                    "totalduration": null,
                    "playwhenvisible": false,
                    "playedonce": false,
                    "manuallypaused": false,
                    "disablepause": false,
                    "disableseeking": false,
                    "postervisible": false,
                    "showsettingsmenu": true, // As a property show/hide from users
                    "visualeffectvisible": false,
                    "visualeffectsupported": false,
                    "visualeffectheight": null,
                    "visualeffectminheight": 120,
                    "visualeffecttheme": "red-bars", // types: `balloon`, 'red-bars'
                    "skipseconds": 5,

                    /* States (helper variables which are controlled by application itself not set by user) */
                    "initialoptions": {
                        "volumelevel": null,
                        "playlist": []
                    },
                    "lastplaylistitem": false,
                    // Reference to Chrome renewed policy, we have to setup mute for auto-playing players.
                    // If we do it forcibly then will set as true
                    "forciblymuted": false,
                    // When volume was un muted, by user himself, not automatically
                    "volumeafterinteraction": false
                },

                types: {
                    "rerecordable": "boolean",
                    "loop": "boolean",
                    "loopall": "boolean",
                    "autoplay": "boolean",
                    "preload": "boolean",
                    "ready": "boolean",
                    "volume": "float",
                    "initialseek": "float",
                    "themecolor": "string",
                    "totalduration": "float",
                    "playwhenvisible": "boolean",
                    "playedonce": "boolean",
                    "manuallypaused": "boolean",
                    "disablepause": "boolean",
                    "disableseeking": "boolean",
                    "playonclick": "boolean",
                    "pauseonclick": "boolean",
                    "showsettings": "boolean",
                    "skipseconds": "integer",
                    "visualeffectvisible": "boolean",
                    "visualeffectmode": "string",
                    "visualeffectheight": "integer"
                },

                extendables: ["states"],

                scopes: {
                    settingsmenu: ">[tagname='ba-common-settingsmenu']"
                },

                computed: {
                    "widthHeightStyles:width,height": function() {
                        var result = {};
                        var width = this.get("width");
                        var height = this.get("height");
                        if (width)
                            result.width = width + ((width + '').match(/^\d+$/g) ? 'px' : '');
                        if (height)
                            result.height = height + ((height + '').match(/^\d+$/g) ? 'px' : '');
                        return result;
                    },
                    "buffering:buffered,position,last_position_change_delta,playing": function() {
                        return this.get("playing") && this.get("buffered") < this.get("position") && this.get("last_position_change_delta") > 1000;
                    }
                },

                events: {
                    "change:visualeffectsupported": function(supported) {
                        if (!supported && this.audioVisualization) this.audioVisualization.destroy();
                    }
                },

                remove_on_destroy: true,

                create: function() {
                    if (this.get("visualeffectvisible") && (!this.get("height") || this.get("height") < this.get("visualeffectminheight")))
                        this.set("height", this.get("visualeffectminheight"));
                    // Will set volume initial state
                    this.set("initialoptions", Objs.tree_merge(this.get("initialoptions"), {
                        volumelevel: this.get("volume")
                    }));

                    if (this.get("theme")) this.set("theme", this.get("theme").toLowerCase());
                    if (this.get("theme") in Assets.audioplayerthemes) {
                        Objs.iter(Assets.audioplayerthemes[this.get("theme")], function(value, key) {
                            if (!this.isArgumentAttr(key))
                                this.set(key, value);
                        }, this);
                    }

                    if (!this.get("themecolor"))
                        this.set("themecolor", "default");

                    if (this.get("playlist")) {
                        var pl0 = (this.get("playlist"))[0];
                        this.set("source", pl0.source);
                        this.set("sources", pl0.sources);
                    }

                    this.set("ie8", Info.isInternetExplorer() && Info.internetExplorerVersion() < 9);
                    this.set("duration", this.get("totalduration") || 0.0);
                    this.set("position", 0.0);
                    this.set("buffered", 0.0);
                    this.set("message", "");
                    this.set("csssize", "normal");

                    this.set("loader_active", false);
                    this.set("controlbar_active", false);
                    this.set("message_active", false);

                    this.set("playing", false);

                    this.__attachRequested = false;
                    this.__activated = false;
                    this.__error = null;

                    this.activeElement().onkeydown = this._keyDownActivity.bind(this, this.activeElement());

                    this.host = new Host({
                        stateRegistry: new ClassRegistry(this.cls.playerStates())
                    });
                    this.host.dynamic = this;
                    this.host.initialize(this._initialState);

                    this._timer = new Timers.Timer({
                        context: this,
                        fire: this._timerFire,
                        delay: 100,
                        start: true
                    });
                },

                getMediaType: function() {
                    return "audio";
                },

                _initialState: InitialState,

                state: function() {
                    return this.host.state();
                },

                audioAttached: function() {
                    return !!this.player;
                },

                audioLoaded: function() {
                    return this.audioAttached() && this.player.loaded();
                },

                audioError: function() {
                    return this.__error;
                },

                _error: function(error_type, error_code) {
                    this.__error = {
                        error_type: error_type,
                        error_code: error_code
                    };
                    this.trigger("error:" + error_type, error_code);
                    this.trigger("error", error_type, error_code);
                },

                _clearError: function() {
                    this.__error = null;
                },

                _detachAudio: function() {
                    this.set("playing", false);
                    if (this.player)
                        this.player.weakDestroy();
                    this.player = null;
                    this.__audio = null;
                    this.set("audioelement_active", false);
                },

                _attachAudio: function() {
                    if (this.audioAttached())
                        return;
                    if (!this.__activated) {
                        this.__attachRequested = true;
                        return;
                    }
                    this.__attachRequested = false;
                    this.set("audioelement_active", true);
                    var audio = this.activeElement().querySelector("[data-audio='audio']");
                    this._clearError();
                    AudioPlayerWrapper.create(Objs.extend(this._getSources(), {
                        element: audio,
                        preload: !!this.get("preload"),
                        loop: !!this.get("loop") || (this.get("lastplaylistitem") && this.get("loopall")),
                        reloadonplay: !!this.get("reloadonplay") // reload of AudioMediaElement not act like in VideoMediaElement, no need for reload
                    })).error(function(e) {
                        if (this.destroyed())
                            return;
                        this._error("attach", e);
                    }, this).success(function(instance) {
                        if (this.destroyed())
                            return;

                        this.player = instance;
                        this.__audio = audio;
                        // Draw audio visualization effect
                        if (this.get("visualeffectvisible") && AudioVisualization.supported() && !this.audioVisualization) {
                            if (this.get("height") && this.get("height") > this.get("visualeffectminheight")) {
                                this.set('visualeffectheight', this.get("height"));
                            } else if (this.get("visualeffectheight") < this.get("visualeffectminheight")) {
                                this.set('visualeffectheight', this.get("visualeffectminheight"));
                            }

                            this.audioVisualization = new AudioVisualization(audio, {
                                height: this.get('visualeffectheight'),
                                element: this.activeElement(),
                                theme: this.get("visualeffecttheme")
                            });
                            try {
                                this.audioVisualization.initializeVisualEffect();
                                this.set("visualeffectsupported", true);
                            } catch (e) {
                                this.set("visualeffectsupported", false);
                                console.warn(e);
                            }
                        }

                        if (this.get("visualeffectvisible") && this.audioVisualization) {
                            this.player.on("playing", function() {
                                this.audioVisualization.start();
                            }, this);
                            this.player.on("paused", function() {
                                this.audioVisualization.pause();
                            }, this);
                            this.player.on("ended", function() {
                                this.audioVisualization.stop();
                            }, this);
                        }

                        if (this.get("playwhenvisible")) {
                            this._playWhenVisible(audio);
                        }
                        // If browser is Chrome, and we have manually forcibly muted player
                        if (Info.isChromiumBased() && this.get("forciblymuted")) {
                            audio.isMuted = true;
                            Dom.userInteraction(function() {
                                this.set_volume(this.get("initialoptions").volumelevel);
                                if (this.get("volume") > 0.00)
                                    audio.isMuted = false;
                                this.set("forciblymuted", false);
                            }, this);
                        }
                        this.player.on("playing", function() {
                            this.set("playing", true);
                            this.trigger("playing");
                        }, this);
                        this.player.on("error", function(e) {
                            this._error("audio", e);
                        }, this);
                        if (this.player.error())
                            this.player.trigger("error", this.player.error());
                        this.player.on("paused", function() {
                            this.set("playing", false);
                            this.trigger("paused");
                        }, this);
                        this.player.on("ended", function() {
                            this.set("playing", false);
                            this.set('playedonce', true);
                            this.set("settingsmenu_active", false);
                            this.trigger("ended");
                        }, this);
                        this.trigger("attached", instance);
                        this.player.once("loaded", function() {
                            var volume = Math.min(1.0, this.get("volume"));
                            this.player.setVolume(volume);
                            this.player.setMuted(volume <= 0.0);
                            this.trigger("loaded");
                            this.trigger("ready_to_play");
                            if (this.get("totalduration") || this.player.duration() < Infinity)
                                this.set("duration", this.get("totalduration") || this.player.duration());
                            if (this.get("initialseek"))
                                this.player.setPosition(this.get("initialseek"));
                        }, this);
                        if (this.player.loaded())
                            this.player.trigger("loaded");
                    }, this);
                },

                _getSources: function() {
                    var filter = this.get("sourcefilter");
                    var source = this.get("source");
                    var sources = filter ? Objs.filter(this.get("sources"), function(source) {
                        return Objs.subset_of(filter, source);
                    }, this) : this.get("sources");
                    return {
                        source: source,
                        sources: sources
                    };
                },

                _afterActivate: function(element) {
                    inherited._afterActivate.call(this, element);
                    this.__activated = true;
                    if (this.__attachRequested)
                        this._attachAudio();
                    this.__settingsMenu = this.scopes.settingsmenu;
                    if (this.__settingsMenu.get('settings'))
                        this.set("hassettings", true);

                },

                _playWhenVisible: function(audio) {
                    var _self = this;

                    if (Dom.isElementVisible(audio, this.get("visibilityfraction"))) {
                        this.player.play();
                    }

                    this._visiblityScrollEvent = this.auto_destroy(new DomEvents());
                    this._visiblityScrollEvent.on(document, "scroll", function() {
                        if (!_self.get('playedonce') && !_self.get("manuallypaused")) {
                            if (Dom.isElementVisible(audio, _self.get("visibilityfraction"))) {
                                _self.player.play();
                            } else if (_self.get("playing")) {
                                _self.player.pause();
                            }
                        } else if (_self.get("playing") && !Dom.isElementVisible(audio, _self.get("visibilityfraction"))) {
                            _self.player.pause();
                        }
                    });

                },

                reattachAudio: function() {
                    this.set("reloadonplay", true);
                    this._detachAudio();
                    this._attachAudio();
                },

                _keyDownActivity: function(element, ev) {
                    var _keyCode = ev.which || ev.keyCode;
                    // Prevent whitespace browser center scroll and arrow buttons behaviours
                    if (_keyCode === 32 || _keyCode === 37 || _keyCode === 38 || _keyCode === 39 || _keyCode === 40) ev.preventDefault();


                    if (_keyCode === 9 && ev.shiftKey) {
                        this._findNextTabStop(element, ev, function(target, index) {
                            target.focus();
                        }, -1);
                    } else if (_keyCode === 9) {
                        this._findNextTabStop(element, ev, function(target, index) {
                            target.focus();
                        });
                    }
                },

                _findNextTabStop: function(parentElement, ev, callback, direction) {
                    var _currentIndex, _direction, _tabIndexes, _tabIndexesArray, _maxIndex, _minIndex, _looked, _tabIndex, _delta, _element, _audioPlayersCount;
                    _maxIndex = _minIndex = 0;
                    _direction = direction || 1;
                    _element = ev.target;
                    _currentIndex = _element.tabIndex;
                    _tabIndexes = parentElement.querySelectorAll('[tabindex]');
                    _tabIndexesArray = Array.prototype.slice.call(_tabIndexes, 0);
                    _tabIndexes = _tabIndexesArray
                        .filter(function(element) {
                            if ((element.clientWidth > 0 || element.clientHeight > 0) && (element.tabIndex !== -1)) {
                                if (_maxIndex <= element.tabIndex) _maxIndex = element.tabIndex;
                                if (_minIndex >= element.tabIndex) _minIndex = element.tabIndex;
                                return true;
                            } else return false;
                        });

                    if ((_direction === 1 && _currentIndex === _maxIndex) || (direction === -1 && _currentIndex === _minIndex) || _maxIndex === 0) {
                        _audioPlayersCount = document.querySelectorAll('ba-audioplayer').length;
                        if (_audioPlayersCount > 1) {
                            if (this.get("playing")) this.player.pause();
                            parentElement.tabIndex = -1;
                            parentElement.blur();
                        }
                        return;
                    }

                    for (var i = 0; i < _tabIndexes.length; i++) {
                        if (!_tabIndexes[i])
                            continue;
                        _tabIndex = _tabIndexes[i].tabIndex;
                        _delta = _tabIndex - _currentIndex;
                        if (_tabIndex < _minIndex || _tabIndex > _maxIndex || Math.sign(_delta) !== _direction)
                            continue;

                        if (!_looked || Math.abs(_delta) < Math.abs(_looked.tabIndex - _currentIndex))
                            _looked = _tabIndexes[i];
                    }

                    if (_looked) {
                        ev.preventDefault();
                        callback(_looked, _looked.tabIndex);
                    }
                },

                object_functions: ["play", "rerecord", "pause", "stop", "seek", "set_volume"],

                functions: {

                    message_click: function() {
                        this.trigger("message:click");
                    },

                    play: function() {
                        this.host.state().play();
                        this.set("manuallypaused", false);
                    },

                    rerecord: function() {
                        if (!this.get("rerecordable"))
                            return;
                        this.trigger("rerecord");
                    },

                    submit: function() {
                        if (!this.get("submittable"))
                            return;
                        this.trigger("submit");
                        this.set("submittable", false);
                        this.set("rerecordable", false);
                    },

                    pause: function() {
                        if (this.get('disablepause')) return;

                        if (this.get("playing")) {
                            this.player.pause();
                        }

                        if (this.get("playwhenvisible"))
                            this.set("manuallypaused", true);
                    },

                    stop: function() {
                        if (!this.audioLoaded())
                            return;
                        if (this.get("playing"))
                            this.player.pause();
                        this.player.setPosition(0);
                        this.trigger("stopped");
                    },

                    seek: function(position) {
                        if (this.get('disableseeking')) return;
                        if (this.audioLoaded()) {
                            if (position > this.player.duration()) {
                                this.player.setPosition(this.player.duration() - this.get("skipseconds"));
                            } else {
                                this.player.setPosition(position);
                                this.trigger("seek", position);
                            }
                        }
                    },

                    set_volume: function(volume) {
                        volume = Math.min(1.0, volume);
                        volume = volume <= 0 ? 0 : volume; // Don't allow negative value

                        this.set("volume", volume);
                        if (this.audioLoaded()) {
                            this.player.setVolume(volume);
                            this.player.setMuted(volume <= 0);
                        }
                    },

                    tab_index_move: function(ev, nextSelector, focusingSelector) {
                        var _targetElement, _activeElement, _selector, _keyCode;
                        _keyCode = ev.which || ev.keyCode;
                        _activeElement = this.activeElement();
                        if (_keyCode === 13 || _keyCode === 32) {
                            if (focusingSelector) {
                                _selector = "[data-selector='" + focusingSelector + "']";
                                _targetElement = _activeElement.querySelector(_selector);
                                if (_targetElement)
                                    Async.eventually(function() {
                                        this.trigger("keyboardusecase", _activeElement);
                                        _targetElement.focus({
                                            preventScroll: false
                                        });
                                    }, this, 100);
                            } else {
                                _selector = '[data-audio="audio"]';
                                _targetElement = _activeElement.querySelector(_selector);
                                Async.eventually(function() {
                                    this.trigger("keyboardusecase", _activeElement);
                                    _targetElement.focus({
                                        preventScroll: true
                                    });
                                }, this, 100);
                            }
                        } else if (_keyCode === 9 && nextSelector) {
                            _selector = "[data-selector='" + nextSelector + "']";
                            _targetElement = _activeElement.querySelector(_selector);
                            if (_targetElement)
                                Async.eventually(function() {
                                    this.trigger("keyboardusecase", _activeElement);
                                    _targetElement.focus({
                                        preventScroll: false
                                    });
                                }, this, 100);

                        }
                    },

                    toggle_settings_menu: function() {
                        this.set("settingsmenu_active", !this.get("settingsmenu_active"));
                    },

                    toggle_player: function() {
                        if (this.get("playing") && this.get("pauseonclick")) {
                            this.pause();
                        } else if (!this.get("playing") && this.get("playonclick")) {
                            this.play();
                        }
                    }

                },

                destroy: function() {
                    this._timer.destroy();
                    this.host.destroy();
                    this._detachAudio();
                    inherited.destroy.call(this);
                },

                _timerFire: function() {
                    if (this.destroyed())
                        return;
                    try {
                        if (this.audioLoaded()) {
                            var _now = Time.now();
                            var new_position = this.player.position();
                            if (new_position !== this.get("position") || this.get("last_position_change"))
                                this.set("last_position_change", _now);
                            // In case if prevent interaction with controller set to true
                            this.set("last_position_change_delta", _now - this.get("last_position_change"));
                            this.set("position", new_position);
                            this.set("buffered", this.player.buffered());
                            var pld = this.player.duration();
                            if (this.get("totalduration")) {
                                this.set("duration", this.get("totalduration"));
                            } else if (0.0 < pld && pld < Infinity) {
                                this.set("duration", this.player.duration());
                            } else {
                                this.set("duration", new_position);
                            }
                        }
                    } catch (e) {}
                    try {
                        this._updateCSSSize();
                    } catch (e) {}
                },

                _updateCSSSize: function() {
                    var width = Dom.elementDimensions(this.activeElement()).width;
                    this.set("csssize", width > 400 ? "normal" : (width > 300 ? "medium" : "small"));
                },

                cloneAttrs: function() {
                    return Objs.map(this.attrs, function(value, key) {
                        return this.get(key);
                    }, this);
                }

            };
        }, {

            playerStates: function() {
                return [PlayerStates];
            }

        }).register("ba-audioplayer")
        .registerFunctions({
            /*<%= template_function_cache(dirname + '/player.html') %>*/
        })
        .attachStringTable(Assets.strings)
        .addStrings({
            "audio-error": "An error occurred, please try again later. Click to retry.",
            "all-settings": "All settings"
        });
});