4

I've written a custom media preloader which uses a series of XMLHttpRequests to load large amounts of media prior to displaying my ng2 app. It is a stakeholder requirement that all media is downloaded in full prior to use of the app.

 private loadFile(media: any) {
    return new Promise(function (resolve, reject) {
        var error: boolean = false;

        //for (var media of media.videos) {
        //TODO: Check how this loads.....
        //console.log("Now Loading video >> ", media, media.hasOwnProperty("path"));


        // Standard XHR to load an image
        var request = new XMLHttpRequest();
        request.open("GET", (<any>media).path);
        request.responseType = 'blob';

        // When the request loads, check whether it was successful
        request.onload = () => {
            if (request.status === 200) {
                resolve(request.response);
            }
            else
                // If it fails, reject the promise with a error message
                reject(Error('Media didn\'t load successfully; error code:' + request.statusText));
        };

        // If an error occurs
        request.onerror = () => {
            // Also deal with the case when the entire request fails to begin with
            // This is probably a network error, so reject the promise with an appropriate message
            reject(Error('There was a network error.'));
        };

        request.onreadystatechange = function () {
            if (request.readyState == 4) {
                console.log("Load Complete >> ", media, "from", request.status); // Another callback here
            }
        };

        // Every tick of the progress loader
        request.onprogress = data => console.log(data);

        // Send the request
        request.send();
    })
}

It works great and successfully loads in all the media that I feed it.

I only have 1 issue and that is in Chrome, when I reference a <video> or <audio> which has been pre-loaded,, it doesn't pull it from the cache, it instead re-downloads it from the server. (IE9 even pulls from cache)


Any audio and video elements will always re-download from the server...
<video width="640px" height="auto" controls autoplay preload="auto">
    <source src="./app/assets/video/Awaiting%20Video%20Master.mov" type="video/mp4"/>
</video>

<audio controls autoplay preload="auto">
    <source src="./app/assets/audio/1_2_4_audio1.mp3" type="audio/mp3" />
</audio>

This will always load from cache...

<img src="./app/assets/images/BTFG-BOLD_Fundamentals.png" />


Here are 2 screenshots, one from chrome and one from edge showing the network activitiy from the dev tools (both had their caches reset prior to launch)...

Chrome enter image description here

Edge enter image description here

The main difference that I notice is the request status is different between browsers when it comes to rendering the content (post preloading). But why is this the case?

I found this SO post from 2013 which states that:

How video is buffered is browser implementation dependent and therefor may vary from browser to browser.

Various browsers can use different factors to determine to keep or to discard a part of the buffer. Old segments, disk space, memory and performance are typical factors.

Is this what is happening here? And if so, does anyone know a way to fix this so that chrome always attempts to pull the videos from cache?

Community
  • 1
  • 1
Zze
  • 18,229
  • 13
  • 85
  • 118

1 Answers1

3

Not sure if the caching issue is a chrome bug, but what you do seems really odd to me.

You are preloading your media, or actually, downloading it entirely, but then set the mediaElement to the original source.

When we load media through a mediaElement (<audio> or <video>), the browser will make range requests, i.e, it won't download the full file, but only what it needs to play without interruption.
That's why you get 206 Partial content responses. It's also probably why chrome doesn't recognize it as being the same requests, and hence doesn't use the cache once again I'm not sure if it's a chrome bug or not.

But since you already did download the full file, why don't you set your mediaElement's src to this downloaded file ?

// since you are setting the hr reponseType to `'blob'`
mediaElement.src = URL.createObjectURL(request.response);
// don't forget to URL.revokeObjectURL(mediaElement.src) when loaded

Working example : (which triggers a weird bug on my FF...)

function loadVideo(url) {
  return new Promise((resolve, reject) => { // here we download it entirely
      let request = new XMLHttpRequest();
      request.responseType = 'blob';
      request.onload = (evt)=>resolve(request.response);
      request.onerror = reject;
      request.open('GET', url);
      request.send();
    }).then((blob)=> 
     new Promise((resolve, reject)=>{
      resolve(URL.createObjectURL(blob)); // return the blobURL directly
      })
     );

}

loadVideo('https://dl.dropboxusercontent.com/s/bch2j17v6ny4ako/movie720p.mp4')
  .then(blobUrl => { // now it's loaded
    document.body.className = 'loaded';
    let vid = document.querySelector('video');
    vid.src = blobUrl; // we just set our mediaElement's src to this blobURL
    vid.onload = () => URL.revokeObjectURL(blobUrl);
  }).catch((err) => console.log(err));
video{
  display: none;
  }
.loaded p{
  display: none;
  }
.loaded video{
  display: unset;
  }
<p>loading.. please wait</p>
<video controls></video>

Or with the fetch API :

function loadVideo(url) {
  return fetch(url)
    .then(resp => resp.blob())
    .then(blob => URL.createObjectURL(blob));
}

loadVideo('https://dl.dropboxusercontent.com/s/bch2j17v6ny4ako/movie720p.mp4')
  .then(blobUrl => { // now it's loaded
    document.body.className = 'loaded';
    let vid = document.querySelector('video');
    vid.src = blobUrl; // we just set our mediaElement's src to this blobURL
    vid.onload = () => URL.revokeObjectURL(blobUrl);
  }).catch((err) => console.log(err));
video {
  display: none;
}
.loaded p {
  display: none;
}
.loaded video {
  display: unset;
}
<p>loading.. please wait</p>
<video controls></video>
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Thanks for the response... Unfortunately it is a requirement that all media is downloaded in **full** prior to use of the app (I will add this to the question). I will try and implement your solution now though - cheers. – Zze Feb 02 '17 at 00:50
  • @Zze, ok, but what I'm saying is that here you are not using the downloaded data, you are making new requests to the server. So it may be a bug in chrome that it doesn't use the cache, but you can force it to use the data you downloaded, and it makes more sense anyway (following this advice, all UAs will behave the same). – Kaiido Feb 02 '17 at 00:52
  • What you're saying makes sense to me - I am going to try and implement this right now and will get back to you. Also, I haven't heard the term UA before - can you please calrify this for me? – Zze Feb 02 '17 at 00:55
  • *UA* => User Agent(s) or most of the time web browsers, but some other type of software also do implement some of the WebAPIs, so I prefer to refer to the generic UA acronym, while I don't know any UA that does implement media API which is not a web browser ;-P – Kaiido Feb 02 '17 at 01:10
  • I've almost got this working, just having some issues with angular in relation to `sanitizing` the new url....... – Zze Feb 02 '17 at 02:34
  • @Zze, I don't know angular, but a quick search revealed [this](http://stackoverflow.com/questions/39524536/angular-2-disable-sanitize) (sounds like a bug to me if their sanitizer doesn't trust dataURIs and blobURIs...) or https://github.com/angular/angular.js/issues/15374#issuecomment-258973526 – Kaiido Feb 02 '17 at 02:43
  • You've been awesome - thanks for the help, now it works perfectly in all browsers! – Zze Feb 02 '17 at 02:57
  • Cheers, this works nicely! Eventhough, it's worth noting that this returns 206 if the mp4 has moov atom at beginning (f.ex. "Web optimized" -feature in Handbrake, which people tempt to use). – kaarto Dec 03 '18 at 05:05