9

I have a JavaScript audio player with skip forward/back 10 second buttons. I do this by setting the currentTime of my audio element:

function Player(skipTime)
{
    this.skipTime = skipTime;
    this.waitLoad = false;

    // initialise main narration audio
    this.narration = new Audio(getFileName(dynamicNarration));
    this.narration.preload = "auto";
    this.narration.addEventListener('canplaythrough', () => { this.loaded();       });
    this.narration.addEventListener('timeupdate',     () => { this.seek();         });
    this.narration.addEventListener('ended',          () => { this.ended();        });
    this.narration.addEventListener('waiting',        () => { this.audioWaiting(); });
    this.narration.addEventListener('playing',        () => { this.loaded();       });
}

Player.prototype = {
    rew: function rew()
    {
        if (!this.waitLoad) {
            this.skip(-this.skipTime);
        }
    },

    ffw: function ffw()
    {
        if (!this.waitLoad) {
            this.skip(this.skipTime);
        }
    },

    skip: function skip(amount)
    {
        const curTime = this.narration.currentTime;
        const newTime = curTime + amount;
        console.log(`Changing currentTime (${curTime}) to ${newTime}`);
        this.narration.currentTime = newTime;
        console.log(`Result: currentTime = ${this.narration.currentTime}`);
    },

    loaded: function loaded()
    {
        if (this.waitLoad) {
            this.waitLoad = false;
            playButton.removeClass('loading');
        }
    },

    audioWaiting: function audioWaiting()
    {
        if (!this.waitLoad) {
            this.waitLoad = true;
            playButton.addClass('loading');
        }
    },
}

(I'm including here some of the event listeners I'm attaching because previously I'd debugged a similar problem as being down to conflicts in event listeners. Having thoroughly debugged event listeners this time though, I don't think that's the root of the problem.)

Though this all works fine on my local copy, when I test an online version I get the following results:

  • Chrome: resets play position to 0. Final console line reads Result: currentTime = 0.
  • Safari: doesn't change play position at all. Final console.log line gives a value for currentTime equal to newTime (even though the play position actually doesn't change).
  • Firefox: skipping forward works; skipping backwards interrupts the audio for a few seconds, then it starts playing again from a couple of seconds before where the playhead had been. In both cases, final console.log line gives a value for currentTime equal to newTime

The issue must have something to do with the way audio is loaded. I have tried adding another console log line to show the start and end values for buffered.

In Chrome it goes up to 2 seconds after current play position. In Safari it goes up to ~170 seconds, and in Firefox it seems to buffer the full audio length.

However, in each case the start of the buffered object is 0.

Does anyone have any idea what might be going wrong?

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Igid
  • 515
  • 4
  • 15
  • Igid, if you are satisfied with my answer below, please mark it as accepted on the left side from my answer or write me a feedback please. – Bharata Sep 07 '18 at 19:53
  • @lgid Can you provide a sample of the file that you are using? What type is it? I've tried your code with an mp3 file that is hosted somewhere and it is working as expected. – Stavros Zavrakas Sep 13 '18 at 09:40

4 Answers4

6

There are some requirements to properly load an audio file and use the properties. Your response while serving the file needs to have the following headers.

accept-ranges: bytes

Content-Length: BYTE_LENGTH_OF_YOUR_FILE

Content-Range: bytes 0-BYTE_LENGTH_OF_YOUR_FILE/BYTE_LENGTH_OF_YOUR_FILE

content-type: audio/mp3

My colleagues and I have been struggling over this for a few days and finally this worked

Image of Response header for an audio file

Yuvaraj Mani
  • 71
  • 1
  • 3
  • 1
    What a coincidence that you add this answer now! I've just been diving back into the world of audio streaming, and through implementing a service worker caching mechanism for audio files on a PWA I have learned aaall about range headers in the last few days. I'm adding an explanation to my own answer and upvoting this. – Igid Jul 23 '20 at 20:04
  • Worked for me! My API is sending out AAC bytes, and setting the hears properly worked perfectly. – KareemJ Feb 26 '22 at 19:32
0

If your browser did not load the audio then the audio can not be played. The browser did not know your audio file and becaue of this it tries to play your audio from the start. May be your audio could be only 1 second long or even shorter.

Solution

You have to wait for loadedmetadata event and after it you can play your audion from any time position. After this event your browser knows all relevant information about your audio file.

Please change your code like follows:

function Player(skipTime)
{
    this.skipTime = skipTime;

    // initialise main narration audio
    this.narration = new Audio(getFileName(dynamicNarration));
    this.narration.preload = "auto";
    this.narration.addEventListener('canplaythrough', () => { this.loaded();       });
    this.narration.addEventListener('timeupdate',     () => { this.seek();         });
    this.narration.addEventListener('ended',          () => { this.ended();        });
    this.narration.addEventListener('waiting',        () => { this.audioWaiting(); });
    this.narration.addEventListener('playing',        () => { this.loaded();       });

    this.narration.addEventListener('loadedmetadata', () => {playButton.removeClass('loading');});

    playButton.addClass('loading');
}

Player.prototype =
{
    rew: function()
    {
        this.skip(-this.skipTime);
    },

    ffw: function()
    {
        this.skip(this.skipTime);
    },

    skip: function(amount)
    {
        var curTime = this.narration.currentTime;
        var newTime = curTime + amount;
        console.log(`Changing currentTime (${curTime}) to ${newTime}`);
        this.narration.currentTime = newTime;
        console.log(`Result: currentTime = ${this.narration.currentTime}`);
    }
};

But if you do not want long to wait for audio loading then you have only one option more: to convert all your audiofiles to dataURL format which looks like follows:

var data = "data:audio/mp3;base64,...

But in this case you have to wait for your page load even more than for one audio file load. And by audio file load it is only the metadata and it is faster.

Bharata
  • 13,509
  • 6
  • 36
  • 50
  • I did try to add a `loadedmetadata` event listener as well. However, since this is happening once the audio is already playing, metadata has long been loaded already. In fact I'm using the duration in other parts of the player logic. – Igid Sep 09 '18 at 21:16
  • @Igid, I do not see anything in your code with duration. Have I answered your question correctly or not? If you are satisfied with my answer, please mark it as accepted on the left side from my answer and / or upvote it. – Bharata Sep 09 '18 at 21:27
  • I meant my actual production code also uses duration, it's not included in my simplified example. Thanks for your response, but no, waiting for metadata to load does not help me as it's loaded long before I even play the file. – Igid Sep 09 '18 at 21:33
  • @Igid, but it is the only one way to play the files online. Your local files do not need it and you can play they immediately after page load, but with online files - you have only one way - you have to wait for `loadedmetadata` event. And this is long because your page is already loaded in browser and the metadata from files must be loaded from your server after page load.. – Bharata Sep 09 '18 at 22:01
  • I did test this by adding an event listener with a console log. `loadedmetadata` event fires before I press play. So waiting for this event does not solve my issue. – Igid Sep 10 '18 at 11:44
0

I found a solution to my problem, if not exactly an explanation.

My hosting provider uses a CDN, for which it must replace resource's URLs with those of a different domain. The URLs of my audio resources are dynamically constructed by JS, because there's a random element to them; as such, the deployment process that replaces URLs wasn't catching those for my audio files. To get around this, I manually excluded the audio files from the CDN, meaning I could refer to them using relative file paths.

This was how things stood when I was having this issue.

Then, due to a separate issue, I took a different approach: I got the audio files back on the CDN and wrote a function to extract the domain name I needed to use to retrieve the files. When I did that, suddenly I found that all my problems to do with setting currentTime had disappeared. Somehow, not having the files on the CDN was severely interfering with the browser's ability to load them in an orderly manner.

If anyone can volunteer an explanation for why this might have been, I'd be very curious to hear it...

Edit

I've been working on another project which involves streaming audio, this time also with PWA support, so I had to implement a caching mechanism in my service worker for audio files. Through this guide I learned all about the pitfalls of range requests, and understand now that failing to serve correct responses to requests with range headers will break seeking on some browsers.

It seems that in the above case, when I excluded my files from the CDN they were served from somewhere that didn't support range headers. When I moved them back on the CDN this was fixed, as it must have been built with explicit support for streaming media.

Here is a good explanation of correct responses to range requests. But for anyone having this issue while using a third party hosting service, it suffices to know that probably they do not support range headers for streaming media. If you want to verify this is the case, you can query the audio object's duration. At least in Safari's case, the duration is set to infinity when it can't successfully make a range request, and at that point seeking will be disabled.

Igid
  • 515
  • 4
  • 15
0

This solved my issue...

    private refreshSrc() {
      const src = this.media.src;
      this.media.src = '';
      this.media.src = src;
    }
Ricardo Almeida
  • 163
  • 3
  • 14