Buffering in jPlayer

2019-08-30 02:45发布

问题:

We use Icecast streaming server to jPlayer on our website and also used on our mobile app. I was trying to add an <intro> to the Icecast config, but when I do, it presents an issue on mobile devices. Whenever the phone has an interruption causing a temporary disconnect, like a call that comes in, the stream repeats what you started listening to when you first connected to the stream, after the intro plays again of course. For instance, if I start the stream listening to one show or song, a call comes in and ends, the intro plays on the reconnect and the stream plays from where I initially started listening.

I have played with Icecast queue and burst settings up and down and none at all, and tried different formats, the same result. I've also had conversations on a couple of other streaming related posts and have been told it seems the issue is with the client buffer and player, which I did not set up. I took a look at our stream-player.js, it is jPlayer 2.9.2 with the following tacked on to the end at line 3507:

;(function() {
  var DOMParser, find, parse;

  DOMParser = (typeof window !== "undefined" && window !== null ? window.DOMParser : void 0) || (typeof require === "function" ? require('xmldom').DOMParser : void 0) || function() {};

  find = function(node, list) {
    var attributes, childNode, childNodeName, childNodes, i, match, x, _i, _j, _ref, _ref1;
    if (node.hasChildNodes()) {
      childNodes = node.childNodes;
      for (i = _i = 0, _ref = childNodes.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
        childNode = childNodes[i];
        childNodeName = childNode.nodeName;
        if (/REF/i.test(childNodeName)) {
          attributes = childNode.attributes;
          for (x = _j = 0, _ref1 = attributes.length; 0 <= _ref1 ? _j < _ref1 : _j > _ref1; x = 0 <= _ref1 ? ++_j : --_j) {
            match = attributes[x].nodeName.match(/HREF/i);
            if (match) {
              list.push({
                file: childNode.getAttribute(match[0]).trim()
              });
              break;
            }
          }
        } else if (childNodeName !== '#text') {
          find(childNode, list);
        }
      }
    }
    return null;
  };

  parse = function(playlist) {
    var doc, ret;
    ret = [];
    doc = (new DOMParser()).parseFromString(playlist, 'text/xml').documentElement;
    if (!doc) {
      return ret;
    }
    find(doc, ret);
    return ret;
  };

  (typeof module !== "undefined" && module !== null ? module.exports : window).ASX = {
    name: 'asx',
    parse: parse
  };

}).call(this);

(function() {
  var COMMENT_RE, EXTENDED, comments, empty, extended, parse, simple;

  EXTENDED = '#EXTM3U';

  COMMENT_RE = /:(?:(-?\d+),(.+)\s*-\s*(.+)|(.+))\n(.+)/;

  extended = function(line) {
    var match;
    match = line.match(COMMENT_RE);
    if (match && match.length === 6) {
      return {
        length: match[1] || 0,
        artist: match[2] || '',
        title: match[4] || match[3],
        file: match[5].trim()
      };
    }
  };

  simple = function(string) {
    return {
      file: string.trim()
    };
  };

  empty = function(line) {
    return !!line.trim().length;
  };

  comments = function(line) {
    return line[0] !== '#';
  };

  parse = function(playlist) {
    var firstNewline;
    playlist = playlist.replace(/\r/g, '');
    firstNewline = playlist.search('\n');
    if (playlist.substr(0, firstNewline) === EXTENDED) {
      return playlist.substr(firstNewline).split('\n#').filter(empty).map(extended);
    } else {
      return playlist.split('\n').filter(empty).filter(comments).map(simple);
    }
  };

  (typeof module !== "undefined" && module !== null ? module.exports : window).M3U = {
    name: 'm3u',
    parse: parse
  };

}).call(this);

(function() {
  var LISTING_RE, parse;

  LISTING_RE = /(file|title|length)(\d+)=(.+)\r?/i;

  parse = function(playlist) {
    var index, key, line, match, tracks, value, _, _i, _len, _ref;
    tracks = [];
    _ref = playlist.trim().split('\n');
    for (_i = 0, _len = _ref.length; _i < _len; _i++) {
      line = _ref[_i];
      match = line.match(LISTING_RE);
      if (match && match.length === 4) {
        _ = match[0], key = match[1], index = match[2], value = match[3];
        if (!tracks[index]) {
          tracks[index] = {};
        }
        tracks[index][key.toLowerCase()] = value;
      }
    }
    return tracks.filter(function(track) {
      return track != null;
    });
  };

  (typeof module !== "undefined" && module !== null ? module.exports : window).PLS = {
    name: 'pls',
    parse: parse
  };

}).call(this);
;(function() {
  var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };

  window.PlayerUI = (function() {
    function PlayerUI(container) {
      var _this = this;
      this.container = container;
      this.onStateButtonClicked = __bind(this.onStateButtonClicked, this);
      this.duration = null;
      this.state = 'loading';
      this.player = $('<div></div>');
      this.container.append(this.player);
      this.player.jPlayer({
        ready: function() {
          return _this.state = 'paused';
        }
      });
      this.volume = this.container.find('.volume-slider input').rangeslider({
        polyfill: false,
        onSlide: function(position, value) {
          return _this.player.jPlayer('volume', value / 100.0);
        },
        onSlideEnd: function(position, value) {
          return _this.player.jPlayer('volume', value / 100.0);
        }
      });
      this.hookEvents();
    }

    PlayerUI.prototype.hookEvents = function() {
      var _this = this;
      this.container.find('.state-button a').click(this.onStateButtonClicked);
      this.player.on($.jPlayer.event.play, function() {
        return _this.setState('playing');
      });
      this.player.on($.jPlayer.event.pause, function() {
        return _this.setState('paused');
      });
      this.player.on($.jPlayer.event.durationchange, function(e) {
        return _this.container.trigger('player.setProgressMax', {
          maxValue: e.jPlayer.status.duration
        });
      });
      this.player.on($.jPlayer.event.timeupdate, function(e) {
        return _this.container.trigger('player.updateProgress', {
          value: e.jPlayer.status.currentTime
        });
      });
      return this.player.on($.jPlayer.event.ended, function(e) {
        return _this.container.trigger('player.trackEnded');
      });
    };

    PlayerUI.prototype.setState = function(state) {
      this.state = state;
      return this.container.find('.state-button a').removeClass().addClass("state-" + state);
    };

    PlayerUI.prototype.onStateButtonClicked = function(event) {
      event.preventDefault();
      switch (this.state) {
        case 'playing':
          return this.pause();
        case 'paused':
          return this.play();
        default:
          return this.noop();
      }
    };

    PlayerUI.prototype.setMedia = function(media) {
      this.pause();
      return this.player.jPlayer('setMedia', media);
    };

    PlayerUI.prototype.setProgress = function(pct) {
      return this.player.jPlayer('playHead', pct);
    };

    PlayerUI.prototype.play = function() {
      this.setState('playing');
      return this.player.jPlayer('play');
    };

    PlayerUI.prototype.pause = function() {
      this.setState('paused');
      return this.player.jPlayer('pause');
    };

    PlayerUI.prototype.noop = function() {
      return null;
    };

    return PlayerUI;

  })();

}).call(this);
;(function() {
  window.PlaylistUI = (function() {
    function PlaylistUI(container) {
      var _this = this;
      this.container = container;
      this.container.hide();
      $(window).on('playlistloader.finished', function(evt, data) {
        return _this.setPlaylist(PlaylistLoader.coalescePlaylists(data.playlists));
      });
    }

    PlaylistUI.prototype.loadM3UList = function(m3uList) {
      return new PlaylistLoader(m3uList);
    };

    PlaylistUI.prototype.setPlaylist = function(playlistData) {
      if (typeof playlistData.data !== 'undefined') {
        this.name = playlistData.name;
        playlistData = playlistData.data;
      }
      this.playlist = playlistData;
      this.container.hide();
      this.unhookEvents();
      this.renderPlaylist();
      this.container.show();
      this.hookEvents();
      return this.container.trigger('playlistui.ready', {
        ui: this,
        autoplay: false //this.getAutoplay()
      });
    };

    PlaylistUI.prototype.unhookEvents = function() {
      return this.container.find('.playlist-item').off('click.playlistUI', 'a');
    };

    PlaylistUI.prototype.hookEvents = function() {
      var _this = this;
      return this.container.find('.playlist-item').on('click.playlistUI', 'a', function(evt) {
        var idx, item;
        evt.preventDefault();
        idx = $(evt.target).parent('.playlist-item').data('idx');
        item = _this.getItemByIdx(idx);
        return _this.select(item);
      });
    };

    PlaylistUI.prototype.renderPlaylist = function() {
      var idx, item, playlist, _i, _len, _ref, _results;
      playlist = this.container.find('.playlist');
      playlist.empty();
      _ref = this.playlist;
      _results = [];
      for (idx = _i = 0, _len = _ref.length; _i < _len; idx = ++_i) {
        item = _ref[idx];
        _results.push(playlist.append(this.rowTemplate(item, idx)));
      }
      return _results;
    };

    PlaylistUI.prototype.rowTemplate = function(item, idx) {
      return $("<li class=\"playlist-item\" data-idx=\"" + idx + "\"><a href=\"" + item.file + "\">" + item.title + "</a></li>");
    };

    PlaylistUI.prototype.getAutoplay = function() {
      var item, _i, _len, _ref;
      _ref = this.playlist;
      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
        item = _ref[_i];
        if (item.autoplay) {
          return item;
        }
      }
      return null;
    };

    PlaylistUI.prototype.getItemByIdx = function(idx) {
      return this.playlist[idx];
    };

    PlaylistUI.prototype.getRowForItem = function(item) {
      var compare, found, idx, _i, _len, _ref;
      _ref = this.playlist;
      for (idx = _i = 0, _len = _ref.length; _i < _len; idx = ++_i) {
        compare = _ref[idx];
        if (compare === item) {
          found = this.container.find(".playlist-item[data-idx=" + idx + "]");
          return found;
        }
      }
      return null;
    };

    PlaylistUI.prototype.getIndexForItem = function(item) {
      var compare, idx, _i, _len, _ref;
      _ref = this.playlist;
      for (idx = _i = 0, _len = _ref.length; _i < _len; idx = ++_i) {
        compare = _ref[idx];
        if (item === compare) {
          return idx;
        }
      }
      return null;
    };

    PlaylistUI.prototype.findNext = function() {
      var currentIndex, nextIndex;
      currentIndex = this.getIndexForItem(this.current);
      if (currentIndex === null) {
        return null;
      }
      nextIndex = currentIndex + 1;
      if (nextIndex >= this.playlist.length) {
        return null;
      }
      return this.playlist[nextIndex];
    };

    PlaylistUI.prototype.select = function(item) {
      if (item) {
          this.current = item;
          this.getRowForItem(item).addClass('selected').siblings().removeClass('selected');
          return this.container.trigger('playlistui.select', {
            ui: this,
            item: item
        });
      }
    };

    PlaylistUI.prototype.selectFirst = function() {
      return this.select(this.playlist[0]);
    };

    PlaylistUI.prototype.selectNext = function() {
      var nextItem;
      nextItem = this.findNext();
      if (nextItem === null) {
        return false;
      }
      this.select(nextItem);
      return true;
    };

    return PlaylistUI;

  })();

}).call(this);
;(function() {
  var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };

  window.PlaylistLoader = (function() {
    function PlaylistLoader(playlists) {
      this.playlists = playlists;
      this.loadedItem = __bind(this.loadedItem, this);
      this.loadPlaylists();
    }

    PlaylistLoader.prototype.loadPlaylists = function() {
      var idx, item, _i, _len, _ref, _results,
        _this = this;
      this.loadCount = 0;
      this.data = new Array(this.playlists.length);
      _ref = this.playlists;
      _results = [];
      for (idx = _i = 0, _len = _ref.length; _i < _len; idx = ++_i) {
        item = _ref[idx];
        _results.push((function() {
          var tmp;
          tmp = idx;
          return jQuery.ajax({
            url: item
          }).done(function(data) {
            return _this.loadedItem(tmp, data);
          });
        })());
      }
      return _results;
    };

    PlaylistLoader.prototype.loadedItem = function(idx, data) {
      var playlist;
      playlist = M3U.parse(data);
      this.data[idx] = playlist;
      $(window).trigger('playlistloader.loadeditem', {
        index: idx,
        playlist: playlist
      });
      this.loadCount++;
      if (this.loadCount === this.playlists.length) {
        return this.finishedLoading();
      }
    };

    PlaylistLoader.prototype.finishedLoading = function() {
      return $(window).trigger('playlistloader.finished', {
        playlists: this.data
      });
    };

    PlaylistLoader.coalescePlaylists = function(playlistsLoaded) {
      var fileEntry, output, playlist, _i, _j, _len, _len1;
      output = [];
      for (_i = 0, _len = playlistsLoaded.length; _i < _len; _i++) {
        playlist = playlistsLoaded[_i];
        for (_j = 0, _len1 = playlist.length; _j < _len1; _j++) {
          fileEntry = playlist[_j];
          output.push(fileEntry);
        }
      }
      return output;
    };

    return PlaylistLoader;

  })();

}).call(this);
;(function() {
  var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };

  window.StreamUI = (function() {
    function StreamUI(selector, streamPlaylists) {
      this.selector = selector;
      this.streamPlaylists = streamPlaylists;
      this.playlistSelect = __bind(this.playlistSelect, this);
      this.playlistReady = __bind(this.playlistReady, this);
      this.container = jQuery(this.selector);
      this.playlist = new PlaylistUI(this.container.find('.playlist-ui'));
      this.player = new PlayerUI(this.container.find('.player-ui'));
      this.hookEvents();
      this.playlist.loadM3UList(this.streamPlaylists);
    }

    StreamUI.prototype.hookEvents = function() {
      var playlistUI;
      playlistUI = this.container.find('.playlist-ui');
      playlistUI.on('playlistui.ready', this.playlistReady);
      return playlistUI.on('playlistui.select', this.playlistSelect);
    };

    StreamUI.prototype.playlistReady = function(evt, eventinfo) {
      if (eventinfo.autoplay !== null) {
        return eventinfo.ui.select(eventinfo.autoplay);
      } else {
        return eventinfo.ui.selectFirst();
      }
    };

    StreamUI.prototype.playlistSelect = function(evt, eventinfo) {
      this.player.setMedia({
        mp3: eventinfo.item.file
      });
      return this.player.play();
    };

    return StreamUI;

  })();

}).call(this);

Although I'm primarily a linux developer with most of my programming experience in Perl and PHP, and do know jQuery pretty well dealing with my web development, I'm surely a novice when it comes to jPlayer or even audio streaming. I was hoping someone could spot something in hte code above that could contribute to the issue we have when adding an intro to our Icecast 2.4.4 stream?

Our streams are available at the URL below, I have the intro on our HD4 stream at the moment.

streaming player

The issue is easily duplicated by starting the stream and listening a bit until the song changes, call the phone letting it interrupt the stream, then hang up. This will cause the first song listened to be playing again after the intro.

I believe the codec is a match, I did have an issue getting the intro to work until I formatted as MP3 128Kbps bit rate 44.1KHz sampling and 2 channel stereo. Here is the intro file info:

user@stream:~$ mediainfo /usr/share/icecast2/web/high_quality.mp3
General
Complete name                            : /usr/share/icecast2/web/high_quality.mp3
Format                                   : MPEG Audio
File size                                : 138 KiB
Duration                                 : 8s 777ms
Overall bit rate mode                    : Constant
Overall bit rate                         : 128 Kbps
Writing library                          : LAME3.99r

Audio
Format                                   : MPEG Audio
Format version                           : Version 1
Format profile                           : Layer 3
Mode                                     : Joint stereo
Mode extension                           : MS Stereo
Duration                                 : 8s 803ms
Bit rate mode                            : Constant
Bit rate                                 : 128 Kbps
Channel(s)                               : 2 channels
Sampling rate                            : 44.1 KHz
Compression mode                         : Lossy
Stream size                              : 137 KiB (100%)
Writing library                          : LAME3.99r
Encoding settings                        : -m j -V 4 -q 3 -lowpass 17 -b 128

回答1:

Sounds like the underlying browser cache kicks in and forces replay of something held in memory. Browsers are 'awesome' like that under some circumstances and will then go out of their way to ignore no-cache directives and other things.

One way to make sure the browser doesn't try to play cache shenanigans is to add a "cache buster". Essentially a query string ( /stream?foo=bar ), which makes the browser engine think it's dynamically generated content and discard its cache; cf. https://www.keycdn.com/support/what-is-cache-busting .

At this time your Icecast server doesn't seem to answer any requests. So I can't look into the specifics on your side.