Source: tools/player.js

/**
 * Mobizen - Mobizen/Tools/Player
 *
 * IMPORTANT NOTE: This file is licensed only for use in providing the RSUPPORT services,
 *
 * @copyright 2015 RSUPPORT CO., LTD. All rights reserved.
 * @license Dedicated to the public domain.
 * @author Joon Kyoung <firejune@gmail.com>
 *
 */

(function(exports) {
  "use strict";

  /**
   * @typedef {Mobizen}
   */
  var Mobizen = exports.Mobizen || (exports.Mobizen = {})
  /**
   * @typedef {Mobizen.Tools}
   */
    , Tools = Mobizen.Tools || (Mobizen.Tools = {})
  /**
   * @typedef {Mobizen.i18n|function}
   */
    , i18n = Mobizen.i18n || function() {};

  /**
   * @constructs Player
   * @memberof Mobizen.Tools
   * @param {Mobizen.Tools} tools - parent object
   *
   * @requires {@linkplain Mobizen.Tools.Player~Context}
   * @requires {@linkplain Mobizen.Tools.Player~Controller}
   *
   */
  function Player(tools) {
    console.info('Tools.Player');

    /**
     * @typedef {Mobizen.Tools}
     * @private
     */
    this.t = tools;
    /**
     * @typedef {Mobizen.Apps}
     * @private
     */
    this.a = tools.a;
    /**
     * A provider of Mobizen/Socket API.
     * @typedef {Mobizen.Socket}
     * @private
     */
    this.s = tools.s;
    /**
     * @typedef {Mobizen(Service)}
     * @private
     */
    this.m = tools.m;

    /** @type {number} */
    this.played = 0;
    /** @type {array} */
    this.history = [];

    /** @type {object} */
    this.song = this.storage('song');
    /** @type {array} */
    this.list = this.storage('album');
    /**
     * @type {object}
     * @prop {boolean} mute
     * @prop {boolean} random
     * @prop {boolean} repeat
     */
    this.status = this.storage('status') || {
      mute: false,
      random: false,
      repeat: true
    };
    // 뮤트 기록은 보존 안함
    this.status.mute = false;

    // NOTE: do not change this position
    /** @typedef {Mobizen.Tools.Player~Context} */
    this.audio = new Context();
    /** @typedef {Mobizen.Tools.Player~Controller} */
    this.control = new Controller(this);

    this.song && this.control.disable(false);
    this.list && this.control.playlist(this.list, this.song);

    this.audio.on('playing', this.onplaying.bind(this));
    this.audio.on('ended', this.onended.bind(this));
  }


  Player.prototype = /** @lends Mobizen.Tools.Player# */ {
    /**
     * @param {string} key
     * @param {*} [val]
     */
    storage: function(key, val) {
      //console.log('Player.storage', key, val);
      var deviceKey = this.s.specific.deviceKey;
      return this.m.storage('player-' + deviceKey, key, val);
    },

    /**
     * @return {object|null} prev song or null
     */
    prev: function() {
      //console.log('Player.prev');

      //if (!this.playing || this.loading) return;
      //this.history[this.history.length - 2];

      if (this.played > 0) this.played -= 2;

      this.history.pop(); // current song

      var song = this.history.pop(); // previous song
      if (this.playAll && this.list && this.list.length && this.list[0].albumId != song.albumId) {
        return this.prevAlbum(song);
      }

      return song || null;

      /*
      if (this.status.random) {
         return this.list[Math.round(Math.random() * (this.list.length - 1))];

      } else {
        var _song;
        for (var i = 0; i < this.list.length; i++) {
          var song = this.list[i];
          if (song.id == this.song.id) return _song;
          _song = song;
        }

        return this.list[this.list.length - 1];
      }
      */
    },

    /**
     * @return {object|null} next song or null
     */
    next: function() {
      //console.log('Player.next');

      //if (!this.playing || this.loading) return;

      if (this.playAll && this.list && this.list.length && this.list[0].albumId != this.song.albumId) {
        this.played = 1;
        return this.currentAlbum();
      }

      var song = null
        , songs
        , next;

      // 자유 재생이면
      if (this.status.random) {
        if (!this.list) {
          console.log('WTF?', new Error().stack);
        }

        // 반복 재생이고 전채 재생이 아니면
        if (this.status.repeat && !this.playAll) {

          songs = pop(this.list, this.list.map(function(song) {
            return song.id;
          }).indexOf(this.song.id));

          song = songs[random(songs.length)];

        // 지금 재생한 수가 앨범 수 보다 작으면
        } else if (this.played < this.list.length) {

          // 재생했던 곡은 빼고 랜덤 선택
          var lefts = []
            , cursor = -this.list.length
            , playeds = this.history.slice(cursor).map(function(song) {
              return song.id;
            });

          //playeds.push(this.song.id);

          $.each(this.list, function(idx, item) {
            if (playeds.indexOf(item.id) == -1) lefts.push(item);
          });

          song = lefts[random(lefts.length)];

          console.debug('player.next.rendom', {
            played: this.played,
            listLength: this.list && this.list.length,
            song: song
          });
        }

      // 랜덤이 아니고 앨범에 속한 곡이 하나 이상이면
      } else if (this.list && this.list.length > 1) {

        var stop = false;

        songs = sorting(this.list);

        for (var i = 0; i < songs.length; i++) {
          next = songs[i];

          if (stop) {
            song = next; // is next song
            break;
          }
          if (next.id == this.song.id) stop = true;
        }

        // 모두 재생했고 반복재생이 활성화 되어 있고 전채 재생이 아니면
        if (!song && this.status.repeat && !this.playAll) song = songs[0];

      // 랜덤 아니고 반복인데 앨범에 속한 곡이 하나면
      } else if (this.status.repeat && this.list && this.list.length == 1 && !this.playAll) {
        song = this.list[0];
        song.noresume = true;
      }

      if (!song && this.playAll) {
        this.played = 0;

        // FIXED #18663
        next = this.nextAlbum();
        if (next) {
          return next;
        } if (this.status.repeat) {
          song = sorting(this.list)[0];
        }
      }

      return song || null;
    },

    /**
     * @param {array} songs
     * @return {object} next song
     */
    nextSong: function(songs) {
      var next = songs[0];

      if (this.status.random) {
        // 현재 곡은 빼고 랜덤 선택
        songs = pop(songs, songs.map(function(song) {
          return song.id;
        }).indexOf(this.song.id));

        next = songs[random(songs.length)];
        this.played = 1;

      // 앨범이 동일하면 현재 다음곡 선택
      } else if (this.song.albumId == next.albumId) {
        var stop = false;

        for (var i = 0; i < songs.length; i++) {
          next = songs[i];
          if (stop) break;
          if (next.id == this.song.id) stop = true;
        }
      }

      return next;
    },

    /**
     * @return {function} trigger
     */
    currentAlbum: function() {
      var music = this.a.music
        , song = this.song
        , channel = this.channel
        , next = this.nextAlbum.bind(this);

      return function(callback) {
        // FIXED FUCKING BUGS OF THE MUSIC PLAYER
        music.select(song.albumId, channel).then(function(data) {
          if (data.items.length <= 1) next()(callback);
          else callback(data.items);
        });
      };
    },

    /**
     * @param {object} song
     * @return {function} trigger
     */
    prevAlbum: function(song) {
      var music = this.a.music
        , channel = this.channel;

      return function(callback) {
        // FIXED #18652
        music.select(song.albumId, channel).then(function(data) {
          callback(data.items, song);
        });
      };
    },

    /**
     * @return {function} trigger or null
     */
    nextAlbum: function() {
      if (!this.playAll || !this.group || this.group.length <= 1) return null; // ?

      var music = this.a.music
        , channel = this.channel
        , albums = this.group
        , next = null
        , stop = false;

      for (var i = 0; i < albums.length; i++) {
        var id = albums[i];

        if (stop) {
          next = id;
          break;
        }

        if (this.song.albumId == id) {
          stop = true;
        }
      }

      if (this.status.repeat && !next) {
        next = albums[0];
      }

      if (next) {
        return function(callback) {
          //console.debug('Player.nextAlbum', next, channel);
          music.select(next, channel).then(function(data) {
            callback(data.items);
          });
        };
      }

      return null;
    },

    /**
     * @fires Mobizen.Tools.Player~Context#source
     * @param {object} song
     * @private
     */
    _play: function(song) {
      console.log('Player._play', song.id, song.title);

      this.control.show();
      this.history.push(song);

      if (this.audio.getSrc() && this.song.path == song.path && this.song.id == song.id) {
        if (song.noresume) {
          delete song.noresume;
        } else {
          this.sameSong = true;
          return this.resume();
        }
      }

      if (this.playing) this.stop(true);

      this.loading = true;

      /**
       * @event Mobizen.Tools.Player~Context#source
       * @type {Mobizen.Event}
       */
      this.audio.emit('source', this.s.url(song.path, song.$channel));

      this.a.exec('music', 'access', [song]);

      var min = song.duration.min
        , sec = song.duration.sec;

      this.control.timeleft((min < 10 && '0' || '') + min, sec);
      this.control.standby(true);

      this.storage('song', this.song = song);
      this.sameSong = false;

      // FIXED #41019
      this.played++;
    },

    /**
     * @param {object} song
     * @param {number} total
     * @private
     */
    _append: function(song, total) {
      var $ul = this.control.$playlist.find('ul')
        , play = song.id == this.song.id ? ' class="playing"' : ''
        , duration = '<span>' + song.duration.min + ':' + song.duration.sec + '</span>';

      $ul.append([
        '<li data-idx="' + song.order + '" data-id="' + song.id + '"' + play + '>',
          '<a>' + song.title + '</a>',
          duration,
        '</li>'
      ].join('\n'));

      this.list.push(song);

      this.control.label(
          this.song.artist + ' - ' + this.song.album
        , this.list.length + ' of ' + total + ', ' + i18n('tools.progress.label'));

      this.control.$element.toggleClass('decoding', true);
    },

    /**
     * @param {array} list
     * @private
     */
    _update: function(list) {
      // FIXED #25273
      if (this.sameSong && this.sameTab && list && list.length == this.list.length) {
        return;
      }

      //console.log('Player._update', song, list);
      this.control.disable(false);

      if (list) {
        this.storage('album', this.list = list || null);
        this.control.playlist(list, this.song);
        this.control.$element.toggleClass('decoding', false);
        this.played = 0; // ?
        //this._clear();
      }

      this.control.button(list, !this.next(), this.history.length < 2);
      this.control.update(true);
      this.control.playmark(this.song.id);

      this.a.exec('music', 'playmark', [this.song]);
    },

    /**
     * @method
     * @private
     */
    _clear: function() {
      this.played = 0;
      this.history = [];
    },

    /**
     * @param {boolean} [customPlayList]
     */
    set: function(customPlayList) {
      var app = this.a.music;

      if (app) {
        var selectedId = app.tab.$subside.find('.select').data('id');

        this.sameTab = this.tab == app.tab.is;

        this.tab == app.tab.is;
        this.channel = app.tab.channel;
        this.playAll = !customPlayList && (app.tab.eq('songs', 'artists') || this.channel == 'local');

        if (app.tab.eq('artists', 'genres') && selectedId != -1) {
          app.groupby(selectedId).then(function(albums) {
            this.group = albums;
          }.bind(this));
        } else if (!app.tab.eq('playlists')) {
          app.groups(this.channel).then(function(albums) {
            this.group = albums.map(function(album) {
              return album.id;
            });
          }.bind(this));
        }
      }
    },

    /**
     * @param {object} song
     * @param {array} album
     * @param {boolean} [clear]
     */
    play: function(song, album, clear) {
      // TODO cancel이 없어서 일단 리턴함
      //if (this.loading) return console.warn('player.loading');

      clear && this._clear();

      this._play(song);
      this._update(album);

      /*
      setTimeout(function() {
        this.loading && this.stop(true);
      }.bind(this), 2000);
      */
    },

    /**
     * @fires Mobizen.Tools.Player~Context#source
     * @param {object} song
     */
    preview: function(song) {
      // 오디오가 재생중이었고 요청한 음악이 같은거면 resume
      if (song) {
        if (this.playing) this.stop(true);
        this.audio.emit('source', this.s.url(song.path, song.$channel));

        this.control.label(song.name, '');
        this.control.button(null, true, true);

        this.previewing = true;
      } else {
        this.stop(true);

        if (this.a.files && this.a.files.$detail) {
          this.a.files.$detail.find('.button.play').html(i18n('tools.player.play'));
        }

        this.control.label('', '');

        if (this.audio.context) {
          this.audio.context.src = '';
          delete this.audio.context.src;
        }

        this.playing = false;
        this.previewing = false;
      }

      this.control.$play
        .toggleClass('icon-pause', !!song)
        .parent().toggleClass('active', !!song);
      //this.list = null;
    },

    /**
     * @param {DOMEvent} event
     */
    toggle: function(event) {
      if (this.previewing) {
        this.preview();
        this.stop(!event);
        return;
      }

      if (this.playing || this.loading) {
        this.stop(!event);
      } else {
        if (this.song) {
          this.play(this.song);
        } else if (this.a.music && this.a.music.$selected) {
          this.a.music.action(this.a.music.$selected);
        }
      }
    },

    // FIXED #28386, #28960
    /**
     * @param {array} items
     * @param {string} channel - The name of channel of socket.
     */
    remove: function(items, channel) {
      console.log('Player.remove', items, channel);

      var idx;
      if (this.history) {
        for (idx = 0; idx < this.history.length; idx++) {
          if (this.history[idx].$channel == channel && items.indexOf(this.history[idx].id) != -1) {
            this.history = pop(this.history, idx);
          }
        }
      }

      if (this.list) {
        for (idx = 0; idx < this.list.length; idx++) {
          if (this.list[idx].$channel == channel && items.indexOf(this.list[idx].id) != -1) {
            this.list = pop(this.list, idx);
          }
        }
      }
    },

    /**
     * @param {object} item
     */
    prepend: function(item) {
      console.log('Player.prepend', item);
      //
    },

    /**
     * @param {object} item
     */
    append: function(item) {
      console.log('Player.append', item);
      //
    },

    /**
     * @param {boolean} [noupdate]
     */
    stop: function(noupdate) {
      //console.log('Player.stop');

      this.control.update(false);

      this.loading = false;
      this.playing = false;
      this.previewing = false;

      this.a.exec('music', 'playmark');
      this.audio.stop();

      this.control.progress();
      this.control.standby(false);
      !noupdate && this.control.hide();
    },

    /**
     * @method
     */
    resume: function() {
      // console.log('Player.resume');
      this.control.show();
      this.control.update(true);

      this.playing = true;
      this.loading = false;

      this.audio.play();
      this.a.exec('music', 'playmark', [this.song]);
    },

    /**
     * @listens Mobizen.Tools.Player~Context#playing
     */
    onplaying: function() {
      console.log('Player.onplay');

      this.control.complete();

      this.loading = false;
      this.playing = true;

      /*
      setTimeout(function() {
        this.parent.bytes = 0;
        this.parent.$progress.width('0%');
      }.bind(this), 400);
      */
    },

    /**
     * @method
     */
    onstop: function() {
      console.log('Player.onstop');

      if (this.playing && !this.previewing) {
        var result = this.next();
        if (typeof result == 'function') {
          result(function(songs) {
            songs = sorting(songs);
            this.play(this.nextSong(songs), songs);
            this.playing = false;
            this.control.update(true);
          }.bind(this));

        } else if (result) {
          this.play(result);
          this.playing = false;
          this.control.update(true);

        } else {
          this.a.exec('music', 'playmark');
          this.stop();
        }
      } else {
        this.control.update(false);
      }
    },

    /**
     * @listens Mobizen.Tools.Player~Context#ended
     */
    onended: function() {
      console.log('Player.onended');
      this.onstop();
      /*
      var song = this.parent.next();
      if (song) this.parent.play(song);
      */
    }
  };


  /**
   * @constructs Controller
   * @memberof Mobizen.Tools.Player~
   * @param {Mobizen.Tools.Player} parent - parent object
   *
   */
  function Controller(parent) {
    var self = this
      , tools = parent.t
      , status = parent.status
      , audio = parent.audio;

    /** @type {boolean} */
    this.dragging = false;
    /** @type {?number} */
    this.pagePos = null;
    /** @type {number} */
    this.ratio = 0;
    /** @typedef {Mobizen.Tools.Player} */
    this.parent = parent;

    /** @type {jQueryObject} */
    this.$element = $('#player');
    /** @type {jQueryObject} */
    this.$play = this.$element.find('.play').click(parent.toggle.bind(parent));

    /** @type {jQueryObject} */
    this.$meter = this.$element.find('.meter');
    /** @type {jQueryObject} */
    this.$timeleft = this.$element.find('.timeleft');
    /** @type {jQueryObject} */
    this.$handler = this.$element.find('.handle').mousedown(function(e) {
      if (e.which !== 1) return;
      // FIXED #18647 issue
      e.preventDefault();
      this.dragging = true;
      //self.$handler.addClass("dragging");
      this.drag(e.pageX);
    }.bind(this));

    /** @type {jQueryObject} */
    this.$gutter = this.$element.find('.gutter').mousedown(function(e) {
      if (e.which !== 1) return;
      this.drag(e.pageX, true);
      this.dragging = true;
    }.bind(this));

    /** @type {jQueryObject} */
    this.$album = this.$element.find('.album');
    /** @type {jQueryObject} */
    this.$playlist = this.$element.find('.playlist').on('click', 'li', function() {
      var $el = $(this).addClass('playing');
      $el.siblings().removeClass('playing');
      parent.play(parent.list[$el.data('idx')]);
      self.playlist.hide();
    });
    this.$playlist.find('ul').antiscroll({x: false, y: true});

    /** @type {jQueryObject} */
    this.$song = this.$element.find('.song').click(function() {
      var antiscroll = this.$playlist.toggle().find('ul').data('antiscroll');
      this.$playlist.is(':visible') && antiscroll.refresh();
    }.bind(this));

    /** @type {jQueryObject} */
    this.$tool = parent.t.menu.$switchtool.find('[data-cmd=player]');

    /** @type {jQueryObject} */
    this.$bnf = this.$element.find('.backward, .forward').click(function() {
      var $el = $(this)
        , result = null;

      if ($el.hasClass('off')) return;

      if ($el.hasClass('backward')) result = parent.prev();
      if ($el.hasClass('forward')) result = parent.next();

      if (typeof result == 'function') {
        // FIXED #20896
        result(function(songs, song) {
          songs = sorting(songs);
          parent.play(song || parent.nextSong(songs), songs);
        });
      } else if (result) {
        parent.play(result);
      }
    });

    this.$element.find('.mute, .random, .repeat').click(function() {
      var $el = $(this).toggleClass('disable');
      if ($el.hasClass('mute')) $el.toggleClass('icon-volume-up icon-volume-off');

      for (var mode in status) {
        if ($el.hasClass(mode)) {
          if (mode != 'mute') {
            status[mode] = !$el.hasClass('disable');
          } else {
            status[mode] = $el.hasClass('disable');
          }
        }
      }

      parent._clear();
      // FIXED #18662
      parent.song && parent.history.push(parent.song);

      self.button(null, !parent.next(), true);

      parent.storage('status', status);
    });

    for (var mode in status) {
      if (mode == 'mute') continue;
      if (!status[mode]) this.$element.find('.' + mode).click();
    }

    tools.shortcut.addEvent('space , . left right', function(evt, key) {
      if (tools.is == 'player'
        && (evt.target && !(evt.target.tagName == 'INPUT' || evt.target.tagName == 'TEXTAREA'))
        && !tools.d.act('screen')) {

        if (key == ',' || !tools.a.running && key == 'left') {
          this.$bnf.filter('.backward').click();
          return true;
        }

        if (key == '.' || !tools.a.running && key == 'right') {
          this.$bnf.filter('.forward').click();
          return true;
        }

        if (key == 'space') {
          parent.toggle();
          return true;
        }
      }
    }.bind(this));

    parent.m.$.win.bind('mousedown blur', function(e) {
      var $target = $(e.target);
      if (this.$playlist.is(':visible') && !$target.parents('.playlist').length && !$target.hasClass('song')) {
        this.playlist.hide();
      }
    }.bind(this));

    parent.m.$.body.mousemove(function(e) {
      if (this.dragging) {
        this.drag(e.pageX);
        this.$element.css('cursor', 'pointer');
      }
    }.bind(this)).mouseup(function() {
      if (this.dragging) {
        this.dragging = false;
        //self.$handler.removeClass("dragging");
        this.$element.css('cursor', 'auto');
        audio.seek(this.ratio);
      }
    }.bind(this));

    audio.on('progress', this.onprogress.bind(this));
    audio.on('timeupdate', this.ontimeupdate.bind(this));

    /** @method */
    this.playlist.hide = function() {
      this.$playlist.hide();
    }.bind(this);
  }

  Controller.prototype = /** @lends Mobizen.Tools.Player~Controller# */ {
    /**
     * @param {number} pageX
     */
    drag: function(pageX) {
      var pagePos = pageX - this.$gutter.offset().left;
      pagePos = Math.min(this.$gutter.outerWidth(), pagePos);
      pagePos = Math.max(0, pagePos);

      if (this.pagePos !== pagePos) {
        this.pagePos = pagePos;
        this.$handler.css('left', pagePos);
        this.ratio = pagePos / this.$gutter.outerWidth();
      }
    },

    /**
     * @param {boolean} playing
     */
    update: function(playing) {
      //console.log('Player.update', playing);

      if (this.parent.previewing) return;

      this.$play
        .toggleClass('icon-pause', playing)
        .toggleClass('icon-play', !playing)
        .parent().toggleClass('active', playing);

      if (playing) {
        var song = this.parent.song
          , album = this.parent.list
          , track = 1 // song.track // fake track number
          , title = song.artist + ' - ' + song.album;

        for (; track < album.length; track++) {
          if (album[track - 1].id == song.id) break;
        }

        //this.timeleft(song.duration.min, song.duration.sec);
        this.label(title, track + ' of ' + album.length + ', ' + song.title);

      } else {
        this.timeleft('00', '00');
        this.handle(0);
      }
    },

    /**
     * @param {string} title
     * @param {string} alt
     */
    label: function(title, alt) {
      this.$album.html(title);
      this.$song.html(alt);
    },

    /**
     * @param {array} album
     * @param {boolean} next
     * @param {boolean} prev
     */
    button: function(album, next, prev) {
      album !== null && this.$bnf.toggleClass('off', !album || album.length == 1);
      this.$bnf.filter('.forward').toggleClass('off', next);
      this.$bnf.filter('.backward').toggleClass('off', prev);
    },

    /**
     * @param {array} album
     * @param {object} current
     */
    playlist: function(album, current) {
      if (!album) return;

      var $ul = this.$playlist.find('ul').empty();

      for (var i = 0; i < album.length; i++) {
        var song = album[i]
          , play = song.id == current.id ? ' class="playing"' : ''
          , duration = '<span>' + song.duration.min + ':' + song.duration.sec + '</span>';

        $ul.append([
          '<li data-idx="' + i + '" data-id="' + song.id + '"' + play + '>',
            '<a>' + song.title + '</a>',
            duration,
          '</li>'
        ].join('\n'));
      }
    },

    /**
     * @param {string} id
     */
    playmark: function(id) {
      this.$playlist.find('.playing').removeClass('playing').end()
        .find('li[data-id=' + id + ']').addClass('playing');
    },

    /**
     * @method
     */
    show: function() {
      if (this.parent.t.is != 'player' && this.parent.t.is != 'fullscreen') {
        this.parent.t.change('player');
      }
    },

    /**
     * @method
     */
    hide: function() {
     this.parent.t.is == 'player' && this.parent.t.change();
    },

    /**
     * @param {boolean} notready
     */
    standby: function(notready) {
      this.$element.toggleClass('decoding', notready);
      this.$element.toggleClass('loading', notready);
    },

    /**
     * @param {boolean} disabled
     */
    disable: function(disabled) {
      this.$tool.toggleClass('disable', disabled);
    },

    /**
     * @param {number} left
     */
    handle: function(left) {
      this.$handler.stop().animate({left: left + '%'}, 100, 'linear');
    },

    /**
     * @param {array} ranges
     * @param {number} duration
     */
    progress: function(ranges, duration) {
      if (!ranges) {
        this.$meter.empty();
        return;
      }

      //then add or remove spans so we have the same number as time ranges
      while (this.$meter.find('span').length < ranges.length) {
        this.$meter.append($('<span></span>'));
      }

      while (this.$meter.find('span').length > ranges.length) {
        this.$meter.find('span:last-child').remove();
      }

      //get the current collection of spans inside the container
      var $spans = this.$meter.find('span');

      //now iterate through the ranges and convert each set of timings
      //to a percentage position and width for the corresponding span
      for(var i = 0; i < ranges.length; i ++) {
        $spans.eq(i).css({
          left: Math.round((100 / duration) * ranges[i][0]) + '%',
          width: Math.round((100 / duration) * (ranges[i][1] - ranges[i][0])) + '%'
        });
      }
    },

    /**
     * @param {number} min
     * @param {number} sec
     */
    timeleft: function(min, sec) {
      if ('NaN' == min.toString() || 'NaN' == sec.toString()) return;
      this.$timeleft.html(min + ':' + sec);
    },

    /**
     * @param {boolean} [ended]
     */
    complete: function(ended) {
      this.$element.removeClass('decoding');
      if (ended) this.$element.removeClass('loading');
    },

    /**
     * @method
     */
    onerror: function() {
      console.warn('Player.onerror');

      this.complete();
      this.parent.stop();
    },

    // FIXED #18406
    /**
     * @listens Mobizen.Tools.Player~Context#progress
     * @param {Audio} audio
     */
    onprogress: function(audio) {
      //console.log('Player.onprogress', audio.buffered.end(0));

      //get the buffered ranges data
      var ranges = []
        , duration = audio.duration;

      try {
        for(var i = 0; i < audio.buffered.length; i ++) {
          ranges.push([
            audio.buffered.start(i),
            audio.buffered.end(i)
          ]);
        }
      } catch(e) {
        console.warn('Player.onprogress.error');
        return;
      }

      this.progress(ranges, duration);

      var percent = ranges.length && (ranges[ranges.length - 1][1] / duration) * 100 || 0;
      if (percent >= 99.9) this.complete(true);
    },

    /**
     * @listens Mobizen.Tools.Player~Context#timeupdate
     * @param {Audio} audio
     */
    ontimeupdate: function(audio) {
      //console.log('Player.timeupdate', audio.currentTime);
      if (this.dragging) return;

      var duration = audio.duration
        , currentTime = audio.currentTime
        , time = new Date((duration - currentTime) * 1000)
        , min = time.getMinutes()
        , sec = time.getSeconds();

      this.timeleft((min < 10 && '0' || '') + min, (sec < 10 && '0' || '') + sec);
      this.handle((currentTime / duration) * 100);
    }
  };


  /**
   * @fires Mobizen.Tools.Player~Context#timeupdate
   * @fires Mobizen.Tools.Player~Context#progress
   * @fires Mobizen.Tools.Player~Context#playing
   * @fires Mobizen.Tools.Player~Context#ended
   * @fires Mobizen.Tools.Player~Context#error
   *
   * @constructs Context
   * @memberof Mobizen.Tools.Player~
   * @extends Mobizen.Event
   *
   */
  function Context() {
    console.info('Tools.Player~Context');

    this.super_.call(this);

    try {
      /** @type {Audio} */
      this.context = new Audio();
    } catch(e) {
      return console.error('Not Supported HTML5 Audio');
    }

    if (!this.context.canPlayType('audio/mpeg')) console.error(i18n('tools.player.notSupportMP3'));

    /**
     * @event Mobizen.Tools.Player~Context#timeupdate
     * @type {Mobizen.Event}
     * @param {Audio} audio
     */
    this.context.addEventListener('timeupdate', this.emitting.bind(this), false);
    /**
     * @event Mobizen.Tools.Player~Context#progress
     * @type {Mobizen.Event}
     * @param {Audio} audio
     */
    this.context.addEventListener('progress', this.emitting.bind(this), false);
    /**
     * @event Mobizen.Tools.Player~Context#playing
     * @type {Mobizen.Event}
     * @param {Audio} audio
     */
    this.context.addEventListener('playing', this.emitting.bind(this), false);
    /**
     * @event Mobizen.Tools.Player~Context#ended
     * @type {Mobizen.Event}
     * @param {Audio} audio
     */
    this.context.addEventListener('ended', this.emitting.bind(this), false);
    /**
     * @event Mobizen.Tools.Player~Context#error
     * @type {Mobizen.Event}
     * @param {Audio} audio
     */
    this.context.addEventListener('error', this.emitting.bind(this), false);

    this.on('source', this.onsource.bind(this));
    this.reset();
  }

  Mobizen.Inherits(Context, Mobizen.Event, /** @lends Mobizen.Tools.Player~Context# */ {
    /**
     * @method
     */
    reset: function() {
      //console.log('Audio.reset');
      this.isPlaying && this.stop();
    },

    /**
     * @param {DOMEvent} event
     */
    event: function(event) {
      console.debug('Audio.event', event.type, event);
    },

    /**
     * @param {DOMEvent} event
     */
    emitting: function(event) {
      //console.debug('Audio.progress', this.context.currentTime);
      if (!this.context) return;

      this.emit(event.type, this.context);
    },

    /**
     * @method
     */
    play: function() {
      //console.log('Audio.play');
      if (!this.context) return;

      this.isPlaying = true;
      this.context.play();
      this.emit('play');
    },

    /**
     * @method
     */
    stop: function() {
      //console.log('Audio.stop');
      if (!this.context) return;

      this.context.pause();
      this.isPlaying = false;

      this.emit('stop');
    },

    /**
     * @param {number} value
     */
    seek: function(value) {
      if (!this.context) return;
      try {
        this.context.currentTime = this.context.duration * value;
      } catch(e) {
        // 버퍼링중에 시크하면 오류남
        //console.error(e);
      }

    },

    /**
     * @method
     */
    getSrc: function() {
      if (!this.context) return;

      return decodeURIComponent(this.context.src || '');
    },

    /**
     * @listens Mobizen.Tools.Player~Context#source
     * @param {string} src
     */
    onsource: function(src) {
      if (!this.context) return;

      //console.log('Audio.onsrc');
      this.context.setAttribute('src', src);
      this.context.load();
      this.play();

      //this.buffer = append(this.buffer, chunk);
    }
  });


  /**
   * Inner functions
   */

  /**
   * @memberof Mobizen.Tools.Player~
   * @param {number} length - max number
   * @return {number} rendom picked number
   */
  function random(length) {
    return Math.round(Math.random() * (length - 1));
  }

  /**
   * @memberof Mobizen.Tools.Player~
   * @param {array} arr - array
   * @param {number} i - number of index
   * @return {array} poped array
   */
  function pop(arr, i) {
    return (!i ? arr.slice(1) : arr.slice(0, i).concat(arr.slice(i + 1)));
  }

  /**
   * @memberof Mobizen.Tools.Player~
   * @param {array} array
   * @return {array} sorted
   */
  function sorting(array) {
    if (array.nosort) return array;

    return array.sort(function(a, b) {
      // group by album
      if (a.albumId > b.albumId){
        return -1;
      } else if (a.albumId < b.albumId){
        return 1;
      // sorting song number
      } else {
        var numA = a.track || a.id
          , numB = b.track || b.id;

        if (numA < numB){
           return -1;
        } else if (numA > numB){
          return 1;
        } else {
          return 0;
        }
      }
    });
  }


  /*
   * Exporting modules
   */

  Tools.Player = Player;

})(this);
comments powered by Disqus