Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save ccurtin/3ad7d8e91600d11e54baab606449299c to your computer and use it in GitHub Desktop.
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)
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)
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