Last active
October 27, 2017 17:54
-
-
Save ccurtin/3ad7d8e91600d11e54baab606449299c to your computer and use it in GitHub Desktop.
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment