ccurtin
6/7/2017 - 8:27 PM

Asynchronously download MP4 videos plays on all modern devices and browsers. - preloads content - only the script... need the HTML Can

Asynchronously download MP4 videos plays on all modern devices and browsers.

  • preloads content
  • only the script... need the HTML Can be seen in action in to "HeroSlider.js" React Component in the Two Words website: whiteboard-pictures(private repo)
var URLs = new Array();

// URL[0] = "http://104.152.110.248/~whiteboardpics/videos/sample-video-2.mp4";
// URL[1] = "http://104.152.110.248/~whiteboardpics/videos/sample-video-3.mp4";
URLs[0] = "http://104.152.110.248/~whiteboardpics/videos/video-00.mp4";
URLs[1] = "http://104.152.110.248/~whiteboardpics/videos/video-02.mp4";
URLs[2] = "http://104.152.110.248/~whiteboardpics/videos/video-03.mp4";

var total = [];
var loaded = [];
var blobs = [];

var request = new Array();
for (var i=0; i<3; i++){
   (function(i) {
      request[i] = new XMLHttpRequest();
      request[i].open("GET", URLs[i], true);

      request[i].onprogress = function(e,) {
          if (e.lengthComputable) {
              total[i] = e.total
              loaded[i] = e.loaded
              // progressBar.max = e.total;
              // total += e.total
              // progressBar.value = e.loaded;
              // var percentage = Math.round((e.loaded/e.total)*100);
              // console.log("percent (" + i + ")   :  " + percentage + '%' );
              var allLoaded = loaded.reduce(function(a, b) { return a + b; }, 0);
              console.log("SINGLE.loaded (" + i + ")   :  " + loaded[i] );
              console.log("TOTAL.loaded as array :  " + loaded );
              console.log("TOTAL.loaded :  " + allLoaded );
              // console.log( "PROGRESSBAR.MAX: " + request[i] + " --- " + progressBar.max  );
              var allTotals = total.reduce(function(a, b) { return a + b; }, 0);
              console.log("SINGLE.MAX (" + i + ")   :  " + total[i] );
              console.log( "TOTAL.MAX as array: " + total  );
              console.log( "REAL.MAX: " + allTotals  );

              console.log( "-----------------------------------" );
              var percentage = Math.round( ( allLoaded / allTotals ) * 100 );
              console.log( percentage );
              console.log( "-----------------------------------" );

          }
      };


      request[i].onreadystatechange = function (oEvent) {
         if (request[i].readyState === 4) {
            if (request[i].status === 200) {
              // console.log(request[i])
              // alert("RESOURCE READY: " + request[i].responseURL)
            } else {
              console.log("Error", request[i].statusText);
            }
         }
      };


      request[i].onload = function(e) {
        if (this.status == 200) {
          blobs[i] = this.response;

          var binaryData = [];
          binaryData.push(blobs[i]);

          var vid = (window.webkitURL ? webkitURL : URL).createObjectURL(new Blob(binaryData, {type: "video/mp4"}));
          // blobs[i] is now the blob that the object URL pointed to.
          // get #video_1, #video_2, #video_3,
          var video = document.getElementById("video_"+i);

          video.src = vid;
          // not needed if autoplay is set for the video element
          // video.play()
        }
      }

      request[i].send(null);

   })(i);
}
import React from 'react'
import Slider from 'react-slick'
import Helmet from 'react-helmet'
import { connect } from 'react-redux'
import { Link } from 'react-router'

import { loadVideoArray, homeSliderVideoBlobsLoaded } from './../redux'

import LineEffect from '_App/components/LineEffect'

import logo from "_images/whiteboard-pictures-logo-white"
import styles from './HeroSlider.scss'

class HeroSlider extends React.Component {

  constructor(props) {
    super(props)
    this.state = {
      loading: true,
      preloader: "visible",
      preloader_width: 0,
      fetchedOnce: false,
      fetchComplete: false,
      mounted: false
    }
    this.displayVideoPreloader = this.displayVideoPreloader.bind(this);
    this.displayImagePreloader = this.displayImagePreloader.bind(this);
    this.setupVideoData = this.setupVideoData.bind(this);
    this.setupImageData = this.setupImageData.bind(this);
    this.preloaderDots = this.preloaderDots.bind(this);
  }

  /*
    3-dots animation
  */
  preloaderDots() {
    return (
      <div className={styles.videoPreloader + " " + this.state.preloader}>
        {/*<div className={styles.logo}>
          <img src={logo} alt=""/>
        </div>*/}
        <div className="preloader_1">
          <div className="spinner">
            <div className="bounce1"></div>
            <div className="bounce2"></div>
            <div className="bounce3"></div>
          </div>
        </div>
      </div>
    )
  }

  /*
    What to display when `iOS <= 9`
  */
  displayImagePreloader() {
    return (this.preloaderDots())
  }

  /*
    What to display when `iOS >= 10`
  */
  displayVideoPreloader() {
    // when going back to Home.js route do NOT show preloader if already fully loaded.
    // `homeSliderLoaded` is only equal to TRUE once all XHR requests complete! via dispatch(homeSliderVideoBlobsLoaded())
    if (this.props.homeSliderLoaded === false) {
      return (
        <div className={styles.videoPreloader + " " + this.state.preloader + " " + `ios-version-major${this.props.iosVersion.major}`}>
          <span>{this.state.preloader_width}%</span>
          <div className={styles.video_preloader + " " + this.state.preloader} style={{width:`${this.state.preloader_width}%`}}><span></span></div>
        </div>
      )
    } else {
      return (this.preloaderDots())
    }
  }

  /*
    NOT PRE-LOADING(yet?) - just update the local state.
  */
  setupImageData() {
    if (this.state.preloader === "visible") {
      this.setState({
        loading: false,
        preloader: "hidden"
      })
    }
  }

  /*
    pre-loads the <videos> and updates the local state
  */
  setupVideoData() {
    // make sure the global `window` is defined (client side request only)
    if (typeof(window) !== 'undefined') {
      var videos = document.querySelectorAll('video')
        // `videos` is a NodeList so can't use forEach() or map()
        // for (var i = 0, len = videos.length; i < len; ++i) {
        //   // enableInlineVideo(videos[i])
        // }
      const URLs = this.props.videoList.content
      var total = []
      var loaded = []
      var blobs = []
      var request = new Array()
      for (var i = 0; i < URLs.length; i++) { // COULD POSSIBLY MAKE A SETTINGS LIKE: `how many videos to preload?`
        (function(i) {
          var customPlaybackRate = URLs[i].acf_fix.video_playback_speed ? URLs[i].acf_fix.video_playback_speed : 1
          videos[i].defaultPlaybackRate = customPlaybackRate
          request[i] = new XMLHttpRequest()
          request[i].open("GET", URLs[i].acf_fix.mp4_video_file, true)
            // responseType MUST be AFTER XHR.open to work in IE 10-11 or `InvalidStateError`
          request[i].responseType = 'blob'
          request[i].onprogress = function(e, ) {
            if (e.lengthComputable) {
              total[i] = e.total
              loaded[i] = e.loaded
              var allLoaded = loaded.reduce(function(a, b) {
                  return a + b
                }, 0)
                //// uncommet console.logs() to show all single file and combined loading size
                ///
                // console.log("SINGLE.loaded (" + i + ")   :  " + loaded[i])
                // console.log("TOTAL.loaded as array :  " + loaded)
                // console.log("TOTAL.loaded :  " + allLoaded)
              var allTotals = total.reduce(function(a, b) {
                  return a + b
                }, 0)
                // console.log("SINGLE.MAX (" + i + ")   :  " + total[i])
                // console.log("TOTAL.MAX as array: " + total)
                // console.log("REAL.MAX: " + allTotals)
                // console.log("-----------------------------------")
              var percentage = Math.round((allLoaded / allTotals) * 100)
                // console.log(percentage)
                // console.log("-----------------------------------")
                /*
                  kind of buggy without `precentage >` ... percetage zigZags up and down...
                  probably because of looping though videos and FULL buffer size changes so percentage goes down.
                  only update if greater than previous value
                */
              if (percentage > this.state.preloader_width) {
                this.setState({
                  preloader_width: percentage
                })
              }
              if (percentage >= 99) {
                var firstVideo = document.getElementById("video_0")
                // setTimeout(function() {
                //   // console.log( "THIZ", this );
                //   firstVideo && firstVideo.play()
                //     // let the site know that videos have been loaded and are ready to play and do not show pre-loader again.
                  this.props.dispatch(homeSliderVideoBlobsLoaded(true))
                  this.setState({
                    loading: false,
                    preloader: "hidden"
                  })
                  setTimeout(function() {
                    this.SlickSlider.slickGoTo(4)
                  }.bind(this), 250)
                //   this.SlickSlider.slickGoTo(1);
                //   // this.SlickSlider.slickGoTo(0);
                // }.bind(this), 500)
              }
            }
          }.bind(this)

          request[i].onreadystatechange = function(oEvent) {
            if (request[i].readyState === 4) {
              if (request[i].status !== 200) {
                console.log("Error", request[i].statusText)
              }
            }
          }

          request[i].onload = function(e) {
              if (request[i].status == 200) {
                blobs[i] = request[i].response
                  // `createObjectURL` must NOT be RAW data, ie blobs[i]. Push data into an ARRAY first.
                var vid = (window.webkitURL ? webkitURL : URL).createObjectURL(new Blob([blobs[i]], {
                    type: "video/mp4"
                  }))
                  // blobs[i] is now the blob that the object URL pointed to.
                  // get #video_1, #video_2, #video_3,
                var video = document.getElementById("video_" + i)
                if (!video) return null
                  // Set the video SOURCE to the blob
                video ? (video.src = vid) : null

                video && video.addEventListener('ended', realVideoEnded.bind(this))
                video && video.addEventListener('timeupdate', rightBeforeVideoEnds.bind(this, video, i))

                // start playing next video RIGHT before current one ends(quarter of a second).
                function rightBeforeVideoEnds(video, i) {
                  // add a classname 750ms before video ends so that text can be "transitioned" out.
                  if (video.currentTime >= video.duration-0.75) {
                    var slideContent = document.querySelector(".slick-active .HeroSlide-content")
                    slideContent.classList.add('HeroSlide-content--fade')
                    setTimeout(function() {
                      slideContent.classList.remove('HeroSlide-content--fade')
                    }, 750);
                  }
                  // if (isNaN(video.duration)) return
                  // in order to prevent interruption/pauses when videos transition, begin playing next video RIGHT before current video ends.
                  if (video.currentTime >= video.duration-0.5) {
                    // how many videos exist in the Slider in total? Length starts at 0.
                    const videoArrayLength = this.props.videoList.content.length ? (this.props.videoList.content.length - 1) : 0;
                    const nextIndex = (i === videoArrayLength) ? 0 : (i + 1)
                    const nextVideo = document.getElementById("video_" + nextIndex)

                    this.props.iosVersion.major === "not_iOS" && nextVideo.play()
                  }
                }
                // Goes to the next slide when a video ends.
                function realVideoEnded() {
                  // console.log("INDEX on `videoEnded` is : " + i);
                  // console.log( i, this.props.videoList.content.length, this.SlickSlider );
                  this.SlickSlider.slickNext()
                  // (i === this.props.videoList.content.length - 1) ? this.SlickSlider.slickGoTo(0) : this.SlickSlider.slickNext()
                }
              }
            }.bind(this)
            // send the XHR request
          request[i].send(null)
        }.bind(this))(i)
      } // #! forLoop
    }
  }

  componentDidMount() {
    // `this.setState()` MUST BE IN ITS OWN FUNCTION. Bugs out on RE-Mount w/ React.
    if (this.props.homeSliderLoaded === true) {
      /* RUN iOS check here.. if not_iOS or > 9.... else setState(preloader:"hidden") */
      if (this.props.iosVersion && this.props.iosVersion.major >= 10 || this.props.iosVersion && this.props.iosVersion.major === "not_iOS") {
        this.setupVideoData()
      } else {
        this.setupImageData();
      }
    } else {
      if (this.state.fetchedOnce === false && this.props.vidz) {
        this.setState({ fetchedOnce: true })
        this.props.dispatch(loadVideoArray(this.props.vidz))
          .then(() => {
            this.setState({ fetchComplete: true });
            if (this.props.iosVersion && this.props.iosVersion.major >= 10 || this.props.iosVersion && this.props.iosVersion.major === "not_iOS") {
              this.setupVideoData()
            } else {
              this.setupImageData();
            }
          })
      }
    }
  }

  componentDidUpdate() {
    if (this.state.fetchedOnce === false && this.props.vidz) {
      this.setState({ fetchedOnce: true })
      this.props.dispatch(loadVideoArray(this.props.vidz))
        .then(() => {
          this.setState({ fetchComplete: true });
          if (this.props.iosVersion && this.props.iosVersion.major >= 10 || this.props.iosVersion && this.props.iosVersion.major === "not_iOS") {
            this.setupVideoData()
          } else {
            this.setupImageData();
          }
        })
    }
  }

  render() {
    if (this.props.iosVersion && this.props.iosVersion.major === "not_iOS" || this.props.iosVersion && this.props.iosVersion.major >= 10) {
      // VIDEO slide settings
      var settings = {
          swipeToSlide: true, // bugs out w/ <video>.
          touchMove: true,
          slide: true,
          swipe: false,
          dots: true,
          infinite: true,
          speed: 500,
          slidesToShow: 1,
          slidesToScroll: 1,
          fade: true,
          arrows: true,
          autoplay: false,
          autoplaySpeed: 3000,
          // vertical: true,
          // BUG?????: when AUTOMATICALLY changing slides the `index` differs from MANUALLY changing slides
          beforeChange: function(index) {
            console.log( "THE CURRENT VIDEO INDEX IS: ", index );
            // how many videos exist in the Slider in total? Length starts at 0.
            const videoArrayLength = this.props.videoList.content.length ? (this.props.videoList.content.length - 1) : 0;
            // get the video that is transitioning OUT
            const video = document.getElementById("video_" + index)
              // IMPORTANT: <video> must be PAUSED before going to next slide so that the `video ended` eventListener doesn't trigger `SlickSlider.slickNext()`
            // video.pause()

            const nextIndex = (index === videoArrayLength) ? 0 : (index + 1)
            const nextVideo = document.getElementById("video_" + nextIndex)
            // will force start from beginning of video
            nextVideo.load()

          }.bind(this), // bind HeroSlider to SlickSlider

          afterChange: function(index) {
            // if paused for any reason, force play after change.
            var video = document.getElementById("video_" + index)
            if (video.paused) {
              video.play()
            }
            // previous video should be "`load()`ed" so it starts at beginning on manual slide navigation when user goes back to view prev video.
            const videoArrayLength = this.props.videoList.content.length ? (this.props.videoList.content.length - 1) : 0;
            const prevIndex = (index === 0) ? videoArrayLength : (index - 1)
            const prevVideo = document.getElementById("video_" + prevIndex)
            prevVideo.load()
            prevVideo.pause()

          }.bind(this)
        } // VIDEO settings
    } else {
      // IMAGE slides settings for `iOS < 9`
      var settings = {
          swipeToSlide: true, // bugs out w/ <video>.
          touchMove: true,
          slide: true,
          swipe: false,
          dots: true,
          infinite: true,
          speed: 500,
          slidesToShow: 1,
          slidesToScroll: 1,
          fade: true,
          arrows: true,
          autoplay: true,
          autoplaySpeed: 5500,
          // vertical: true,
          // BUG?????: when AUTOMATICALLY changing slides the `index` differs from MANUALLY changing slides
          beforeChange: function(index) {}, // bind HeroSlider to SlickSlider
          afterChange: function(index) {}
        } // IMAGE settings
    }

    function videoDisplays() {
      if (this.props.videoList.content && this.props.videoList.content.length > 0) {
        return (
          this.props.videoList.content.map(function(project, i) {
            const directors = project.acf_fix.director.map(function(director, di) {
              if (project.acf_fix.director.length === 1) { // if ONE director listed
                return <div key={`hero-director-${di}`} className={styles.director_name_wrapper}>
                    <div className={styles.by_line}><em>by</em></div>
                    <div className={styles.director_name} >
                      <LineEffect tagName="h4" tagClass="h5" lineHeight="3px" padding="20px">
                        {director.post_title}
                      </LineEffect>
                    </div>
                </div>
              } else if (project.acf_fix.director.length > 1) { // if MULTIPLE directors listed
                // const  separator = di === project.acf_fix.director.length ? null : ""
                return <div key={`hero-director-${di}`} className={styles.director_name_wrapper}>
                <div className={styles.by_line}><em>by</em></div>
                  <div className={styles.director_name} >
                    <LineEffect tagName="h4" tagClass="h5" lineHeight="3px" padding="0">
                      {director.post_title}
                    </LineEffect>
                    <span className={styles.d_separator}></span>
                  </div>
                </div>
              } else { // if NO Directors added
                return null
              }
            })
            return (
              <div key={"project-slide-video"+i} className={styles.slick_slide_wrapper}>
                <div className={`HeroSlide-content ${styles.slide_content}`}>
                  <h1 dangerouslySetInnerHTML={{__html: project.title.rendered}} className={styles.title}></h1>
                  <div className={styles.director_names}>{directors}</div>
                  <div className={styles.btn}>
                    <Link to={`project/${project.slug}`}>View Project</Link>
                  </div>
                </div>

                <video preload playsInline id={`video_${i}`} muted="muted" className="video">
                  <source/>
                </video>
              </div>
            )
          })
        )
      }
    }

    function imageDisplays() {
      if (this.props.videoList.content && this.props.videoList.content.length > 0) {
        return (
          this.props.videoList.content.map(function(project, i) {
            const directors = project.acf_fix.director.map(function(director, di) {
              if (project.acf_fix.director.length === 1) { // if ONE director listed
                return <div key={`hero-director-${di}`} className={styles.director_name_wrapper}>
                  <div className={styles.by_line}><em>by</em></div>
                  <div className={styles.director_name} >
                    <h4 className="h4">{director.post_title} </h4>
                  </div>
                </div>
              } else if (project.acf_fix.director.length > 1) { // if MULTIPLE directors listed
                // const  separator = di === project.acf_fix.director.length ? null : ""
                return <div key={`hero-director-${di}`} className={styles.director_name_wrapper}>
                  <div className={styles.by_line}><em>by</em></div>
                  <div className={styles.director_name} >
                    <h4 className="h4">{director.post_title}</h4>
                    <span className={styles.d_separator}></span>
                  </div>
                </div>
              } else { // if NO Directors added
                return null
              }
            })
            return (
              <div key={`project-slide-image-${i}`} className={styles.slick_slide_wrapper}>
                <div className={styles.slide_content}>
                  <h1 dangerouslySetInnerHTML={{__html: project.title.rendered}} className={styles.title}></h1>
                  <div className={styles.director_names}>{directors}</div>
                  <div className={styles.btn}>
                    <Link to={`project/${project.slug}`}>View Project</Link>
                  </div>
                </div>
                <img src={project.acf_fix.homepage_slider_image} alt=""/>
              </div>
            )
          })
        )
      }
    }

    // render()'s return
    return (
      <div className={styles.hero_slider + " hero-slider " + this.state.preloader}>
      {/* this will dispaly the loading dots "..." while waiting for components to mount and get dispatch iOS version to redux store */}
      {this.props.iosVersion === null && this.preloaderDots()}
      {this.props.iosVersion && this.props.iosVersion.major === "not_iOS" && this.displayVideoPreloader()}
      {this.props.iosVersion && this.props.iosVersion.major <= 9 && this.displayImagePreloader()}
      {this.props.iosVersion && this.props.iosVersion.major >= 10  && this.displayVideoPreloader()}
      {(
        this.props.iosVersion && this.props.iosVersion.major === "not_iOS" || this.props.iosVersion && this.props.iosVersion.major >= 10) && this.props.videoList.content && this.props.videoList.content.length > 0 && (
          <Slider {...settings} ref={(input) => {this.SlickSlider = input}}>
            {videoDisplays.call(this)}
          </Slider>
      )}
      {
        this.props.iosVersion && this.props.iosVersion.major <= 9 && this.props.videoList.content && this.props.videoList.content.length > 0 && (
        <div>
          <Helmet>
            <body className="bodywrapper-home HeroSlider-images" />
          </Helmet>
          <Slider {...settings} ref={(input) => {this.SlickSlider = input}}>
            {imageDisplays.call(this)}
          </Slider>
        </div>
      )}
      </div>
    )
  }

}

function mapStateToProps(state, dispatch) {
  const { homepageContent, featuredBlogs, homeSliderLoaded, videoList } = state.homepage
  const { iosVersion } = state.featureDetection
  return {
    homepageContent,
    featuredBlogs,
    homeSliderLoaded,
    videoList,
    iosVersion
  }
}

export default connect(mapStateToProps)(HeroSlider)