scrolly-video
Advanced tools
Comparing version 0.0.17 to 0.0.18
@@ -9,4 +9,4 @@ { | ||
}, | ||
"extends": ["airbnb-base"], | ||
"plugins": ["svelte3"], | ||
"extends": ["airbnb-base", "plugin:prettier/recommended"], | ||
"plugins": ["svelte3", "prettier"], | ||
"overrides": [ | ||
@@ -26,4 +26,6 @@ { | ||
"import/first": 0, | ||
"camelcase": 0 | ||
"camelcase": 0, | ||
"no-console": 0, | ||
"prettier/prettier": 1 | ||
} | ||
} |
@@ -63,16 +63,32 @@ export default ScrollyVideo; | ||
/** | ||
* Transitions the video or the canvas to the proper frame | ||
* Transitions the video or the canvas to the proper frame. | ||
* | ||
* @param jump | ||
* @param options - Configuration options for adjusting the video playback. | ||
* - jump: boolean - If true, the video currentTime will jump directly to the specified percentage. If false, the change will be animated over time. | ||
* - transitionSpeed: number - Defines the speed of the transition when `jump` is false. Represents the duration of the transition in milliseconds. Default is 8. | ||
* - easing: (progress: number) => number - A function that defines the easing curve for the transition. It takes the progress ratio (a number between 0 and 1) as an argument and returns the eased value, affecting the playback speed during the transition. | ||
*/ | ||
transitionToTargetTime(jump: any): void; | ||
transitioning: boolean; | ||
transitionToTargetTime({ jump, transitionSpeed, easing, }: { | ||
jump: any; | ||
transitionSpeed?: number; | ||
easing?: any; | ||
}): void; | ||
transitioningRaf: number; | ||
/** | ||
* Sets the currentTime as a percentage of the video duration. | ||
* Sets the currentTime of the video as a specified percentage of its total duration. | ||
* | ||
* @param setPercentage | ||
* @param jump | ||
* @param setPercentage - The percentage of the video duration to set as the current time. | ||
* @param options - Configuration options for adjusting the video playback. | ||
* - jump: boolean - If true, the video currentTime will jump directly to the specified percentage. If false, the change will be animated over time. | ||
* - transitionSpeed: number - Defines the speed of the transition when `jump` is false. Represents the duration of the transition in milliseconds. Default is 8. | ||
* - easing: (progress: number) => number - A function that defines the easing curve for the transition. It takes the progress ratio (a number between 0 and 1) as an argument and returns the eased value, affecting the playback speed during the transition. | ||
*/ | ||
setTargetTimePercent(setPercentage: any, jump: any): void; | ||
setTargetTimePercent(setPercentage: any, options?: {}): void; | ||
/** | ||
* Simulate trackScroll programmatically (scrolls on page by percentage of video) | ||
* | ||
* @param percentage | ||
*/ | ||
setScrollPercent(percentage: any): void; | ||
/** | ||
* Call to destroy this ScrollyVideo object | ||
@@ -79,0 +95,0 @@ */ |
@@ -46,3 +46,4 @@ import UAParser from 'ua-parser-js'; | ||
// eslint-disable-next-line no-undef | ||
if (scrollyVideoContainer instanceof Element) this.container = scrollyVideoContainer; | ||
if (scrollyVideoContainer instanceof Element) | ||
this.container = scrollyVideoContainer; | ||
// otherwise it should better be an element | ||
@@ -52,3 +53,4 @@ else if (typeof scrollyVideoContainer === 'string') { | ||
this.container = document.getElementById(scrollyVideoContainer); | ||
if (!this.container) throw new Error('scrollyVideoContainer must be a valid DOM object'); | ||
if (!this.container) | ||
throw new Error('scrollyVideoContainer must be a valid DOM object'); | ||
} else { | ||
@@ -103,3 +105,3 @@ throw new Error('scrollyVideoContainer must be a valid DOM object'); | ||
// Detect webkit (safari), because webkit requires special attention | ||
const browserEngine = (new UAParser()).getEngine(); | ||
const browserEngine = new UAParser().getEngine(); | ||
// eslint-disable-next-line no-undef | ||
@@ -120,8 +122,10 @@ this.isSafari = browserEngine.name === 'WebKit'; | ||
// Used for internally setting the scroll percentage based on built-in listeners | ||
const containerBoundingClientRect = this.container.parentNode.getBoundingClientRect(); | ||
const containerBoundingClientRect = | ||
this.container.parentNode.getBoundingClientRect(); | ||
// Calculate the current scroll percent of the video | ||
const scrollPercent = (-containerBoundingClientRect.top) | ||
const scrollPercent = | ||
-containerBoundingClientRect.top / | ||
// eslint-disable-next-line no-undef | ||
/ (containerBoundingClientRect.height - window.innerHeight); | ||
(containerBoundingClientRect.height - window.innerHeight); | ||
@@ -131,3 +135,3 @@ if (this.debug) console.info('ScrollyVideo scrolled to', scrollPercent); | ||
// Set the target time percent | ||
this.setTargetTimePercent(scrollPercent, jump); | ||
this.setTargetTimePercent(scrollPercent, { jump }); | ||
}; | ||
@@ -149,3 +153,3 @@ | ||
'loadedmetadata', | ||
() => this.setTargetTimePercent(0, true), | ||
() => this.setTargetTimePercent(0, { jump: true }), | ||
{ once: true }, | ||
@@ -188,5 +192,4 @@ ); | ||
// Gets the width and height of the container | ||
const { | ||
width: containerWidth, height: containerHeight, | ||
} = this.container.getBoundingClientRect(); | ||
const { width: containerWidth, height: containerHeight } = | ||
this.container.getBoundingClientRect(); | ||
@@ -197,3 +200,7 @@ // Gets the width and height of the video frames | ||
if (this.debug) console.info('Container dimensions:', [containerWidth, containerHeight]); | ||
if (this.debug) | ||
console.info('Container dimensions:', [ | ||
containerWidth, | ||
containerHeight, | ||
]); | ||
if (this.debug) console.info('Element dimensions:', [width, height]); | ||
@@ -218,5 +225,12 @@ | ||
if (this.useWebCodecs && this.src) { | ||
videoDecoder(this.src, (frame) => { this.frames.push(frame); }, this.debug) | ||
videoDecoder( | ||
this.src, | ||
(frame) => { | ||
this.frames.push(frame); | ||
}, | ||
this.debug, | ||
) | ||
.catch(() => { | ||
if (this.debug) console.error('Error encountered while decoding video'); | ||
if (this.debug) | ||
console.error('Error encountered while decoding video'); | ||
// Remove all decoded frames if a failure happens during decoding | ||
@@ -231,3 +245,4 @@ this.frames = []; | ||
if (this.frames.length === 0) { | ||
if (this.debug) console.error('No frames were received from webCodecs'); | ||
if (this.debug) | ||
console.error('No frames were received from webCodecs'); | ||
return; | ||
@@ -238,3 +253,4 @@ } | ||
this.frameRate = this.frames.length / this.video.duration; | ||
if (this.debug) console.info('Received', this.frames.length, 'frames'); | ||
if (this.debug) | ||
console.info('Received', this.frames.length, 'frames'); | ||
@@ -283,3 +299,9 @@ // Remove the video and add the canvas | ||
// Draw the frame to the canvas context | ||
this.context.drawImage(currFrame, 0, 0, currFrame.width, currFrame.height); | ||
this.context.drawImage( | ||
currFrame, | ||
0, | ||
0, | ||
currFrame.width, | ||
currFrame.height, | ||
); | ||
} | ||
@@ -290,65 +312,140 @@ } | ||
/** | ||
* Transitions the video or the canvas to the proper frame | ||
* Transitions the video or the canvas to the proper frame. | ||
* | ||
* @param jump | ||
* @param options - Configuration options for adjusting the video playback. | ||
* - jump: boolean - If true, the video currentTime will jump directly to the specified percentage. If false, the change will be animated over time. | ||
* - transitionSpeed: number - Defines the speed of the transition when `jump` is false. Represents the duration of the transition in milliseconds. Default is 8. | ||
* - easing: (progress: number) => number - A function that defines the easing curve for the transition. It takes the progress ratio (a number between 0 and 1) as an argument and returns the eased value, affecting the playback speed during the transition. | ||
*/ | ||
transitionToTargetTime(jump) { | ||
transitionToTargetTime({ | ||
jump, | ||
transitionSpeed = this.transitionSpeed, | ||
easing = null, | ||
}) { | ||
if (this.debug) { | ||
console.info('Transitioning targetTime:', this.targetTime, 'currentTime:', this.currentTime); | ||
console.info( | ||
'Transitioning targetTime:', | ||
this.targetTime, | ||
'currentTime:', | ||
this.currentTime, | ||
); | ||
} | ||
// If we are already close enough to our target, pause the video and return. | ||
// This is the base case of the recursive function | ||
if ( | ||
// eslint-disable-next-line no-restricted-globals | ||
isNaN(this.targetTime) | ||
// If the currentTime is already close enough to the targetTime | ||
|| Math.abs(this.currentTime - this.targetTime) < this.frameThreshold | ||
) { | ||
this.video.pause(); | ||
this.transitioning = false; | ||
return; | ||
} | ||
const diff = this.targetTime - this.currentTime; | ||
const distance = Math.abs(diff); | ||
const duration = distance * 1000; | ||
const isForwardTransition = diff > 0; | ||
// Make sure we don't go out of time bounds | ||
if (this.targetTime > this.video.duration) this.targetTime = this.video.duration; | ||
if (this.targetTime < 0) this.targetTime = 0; | ||
const tick = ({ startCurrentTime, startTimestamp, timestamp }) => { | ||
const progress = (timestamp - startTimestamp) / duration; | ||
// How far forward we need to transition | ||
const transitionForward = this.targetTime - this.currentTime; | ||
// if frameThreshold is too low to catch condition Math.abs(this.targetTime - this.currentTime) < this.frameThreshold | ||
const hasPassedThreshold = isForwardTransition | ||
? this.currentTime >= this.targetTime | ||
: this.currentTime <= this.targetTime; | ||
if (this.canvas) { | ||
// Update currentTime and paint the closest frame | ||
this.currentTime += transitionForward / (256 / this.transitionSpeed); | ||
// If jump, we go directly to the frame | ||
if (jump) this.currentTime = this.targetTime; | ||
this.paintCanvasFrame(Math.floor(this.currentTime * this.frameRate)); | ||
} else if (jump || this.isSafari || this.targetTime - this.currentTime < 0) { | ||
// We can't use a negative playbackRate, so if the video needs to go backwards, | ||
// We have to use the inefficient method of modifying currentTime rapidly to | ||
// get an effect. | ||
this.video.pause(); | ||
this.currentTime += transitionForward / (64 / this.transitionSpeed); | ||
// If jump, we go directly to the frame | ||
if (jump) this.currentTime = this.targetTime; | ||
this.video.currentTime = this.currentTime; | ||
} else { | ||
// Otherwise, we play the video and adjust the playbackRate to get a smoother | ||
// animation effect. | ||
const playbackRate = Math.max(Math.min((transitionForward) * 4, this.transitionSpeed, 16), 1); | ||
if (this.debug) console.info('ScrollyVideo playbackRate:', playbackRate); | ||
// eslint-disable-next-line no-restricted-globals | ||
if (!isNaN(playbackRate)) { | ||
this.video.playbackRate = playbackRate; | ||
this.video.play(); | ||
// If we are already close enough to our target, pause the video and return. | ||
// This is the base case of the recursive function | ||
if ( | ||
// eslint-disable-next-line no-restricted-globals | ||
isNaN(this.targetTime) || | ||
// If the currentTime is already close enough to the targetTime | ||
Math.abs(this.targetTime - this.currentTime) < this.frameThreshold || | ||
hasPassedThreshold | ||
) { | ||
this.video.pause(); | ||
if (this.transitioningRaf) { | ||
// eslint-disable-next-line no-undef | ||
cancelAnimationFrame(this.transitioningRaf); | ||
this.transitioningRaf = null; | ||
} | ||
return; | ||
} | ||
// Set the currentTime to the video's currentTime | ||
this.currentTime = this.video.currentTime; | ||
} | ||
// Recursively calls ourselves until the animation is done. | ||
// eslint-disable-next-line no-undef | ||
// Make sure we don't go out of time bounds | ||
if (this.targetTime > this.video.duration) | ||
this.targetTime = this.video.duration; | ||
if (this.targetTime < 0) this.targetTime = 0; | ||
// How far forward we need to transition | ||
const transitionForward = this.targetTime - this.currentTime; | ||
const easedProgress = | ||
easing && Number.isFinite(progress) ? easing(progress) : null; | ||
const easedCurrentTime = isForwardTransition | ||
? startCurrentTime + | ||
easedProgress * Math.abs(distance) * transitionSpeed | ||
: startCurrentTime - | ||
easedProgress * Math.abs(distance) * transitionSpeed; | ||
if (this.canvas) { | ||
if (jump) { | ||
// If jump, we go directly to the frame | ||
this.currentTime = this.targetTime; | ||
} else if (easedProgress) { | ||
this.currentTime = easedCurrentTime; | ||
} else { | ||
this.currentTime += transitionForward / (256 / transitionSpeed); | ||
} | ||
this.paintCanvasFrame(Math.floor(this.currentTime * this.frameRate)); | ||
} else if (jump || this.isSafari || !isForwardTransition) { | ||
// We can't use a negative playbackRate, so if the video needs to go backwards, | ||
// We have to use the inefficient method of modifying currentTime rapidly to | ||
// get an effect. | ||
this.video.pause(); | ||
if (easedProgress) { | ||
this.currentTime = easedCurrentTime; | ||
} else { | ||
this.currentTime += transitionForward / (64 / transitionSpeed); | ||
} | ||
// If jump, we go directly to the frame | ||
if (jump) { | ||
this.currentTime = this.targetTime; | ||
} | ||
this.video.currentTime = this.currentTime; | ||
} else { | ||
// Otherwise, we play the video and adjust the playbackRate to get a smoother | ||
// animation effect. | ||
const playbackRate = Math.max( | ||
Math.min(transitionForward * 4, transitionSpeed, 16), | ||
1, | ||
); | ||
if (this.debug) | ||
console.info('ScrollyVideo playbackRate:', playbackRate); | ||
// eslint-disable-next-line no-restricted-globals | ||
if (!isNaN(playbackRate)) { | ||
this.video.playbackRate = playbackRate; | ||
this.video.play(); | ||
} | ||
// Set the currentTime to the video's currentTime | ||
this.currentTime = this.video.currentTime; | ||
} | ||
// Recursively calls ourselves until the animation is done. | ||
if (typeof requestAnimationFrame === 'function') { | ||
// eslint-disable-next-line no-undef | ||
this.transitioningRaf = requestAnimationFrame((currentTimestamp) => | ||
tick({ | ||
startCurrentTime, | ||
startTimestamp, | ||
timestamp: currentTimestamp, | ||
}), | ||
); | ||
} | ||
}; | ||
if (typeof requestAnimationFrame === 'function') { | ||
// eslint-disable-next-line no-undef | ||
requestAnimationFrame(() => this.transitionToTargetTime()); | ||
this.transitioningRaf = requestAnimationFrame((startTimestamp) => { | ||
tick({ | ||
startCurrentTime: this.currentTime, | ||
startTimestamp, | ||
timestamp: startTimestamp, | ||
}); | ||
}); | ||
} | ||
@@ -358,28 +455,57 @@ } | ||
/** | ||
* Sets the currentTime as a percentage of the video duration. | ||
* Sets the currentTime of the video as a specified percentage of its total duration. | ||
* | ||
* @param setPercentage | ||
* @param jump | ||
* @param setPercentage - The percentage of the video duration to set as the current time. | ||
* @param options - Configuration options for adjusting the video playback. | ||
* - jump: boolean - If true, the video currentTime will jump directly to the specified percentage. If false, the change will be animated over time. | ||
* - transitionSpeed: number - Defines the speed of the transition when `jump` is false. Represents the duration of the transition in milliseconds. Default is 8. | ||
* - easing: (progress: number) => number - A function that defines the easing curve for the transition. It takes the progress ratio (a number between 0 and 1) as an argument and returns the eased value, affecting the playback speed during the transition. | ||
*/ | ||
setTargetTimePercent(setPercentage, jump) { | ||
setTargetTimePercent(setPercentage, options = {}) { | ||
// eslint-disable-next-line | ||
// The time we want to transition to | ||
this.targetTime = Math.max(Math.min(setPercentage, 1), 0) | ||
* ((this.frames.length && this.frameRate) ? this.frames.length | ||
/ this.frameRate : this.video.duration); | ||
this.targetTime = | ||
Math.max(Math.min(setPercentage, 1), 0) * | ||
(this.frames.length && this.frameRate | ||
? this.frames.length / this.frameRate | ||
: this.video.duration); | ||
// If we are close enough, return early | ||
if (!jump && Math.abs(this.currentTime - this.targetTime) < this.frameThreshold) return; | ||
if ( | ||
!options.jump && | ||
Math.abs(this.currentTime - this.targetTime) < this.frameThreshold | ||
) | ||
return; | ||
// If we are already transitioning, bail early | ||
if (!jump && this.transitioning) return; | ||
// Play the video if we are in video mode | ||
if (!this.canvas && !this.video.paused) this.video.play(); | ||
// Set transitioning state to true and begin transition | ||
this.transitioning = true; | ||
this.transitionToTargetTime(jump); | ||
this.transitionToTargetTime(options); | ||
} | ||
/** | ||
* Simulate trackScroll programmatically (scrolls on page by percentage of video) | ||
* | ||
* @param percentage | ||
*/ | ||
setScrollPercent(percentage) { | ||
if (!this.trackScroll) { | ||
console.warn('`setScrollPercent` requires enabled `trackScroll`'); | ||
return; | ||
} | ||
const parent = this.container.parentNode; | ||
const { top, height } = parent.getBoundingClientRect(); | ||
// eslint-disable-next-line no-undef | ||
const startPoint = top + window.pageYOffset; | ||
// eslint-disable-next-line no-undef | ||
const containerHeightInViewport = height - window.innerHeight; | ||
const targetPoint = startPoint + containerHeightInViewport * percentage; | ||
// eslint-disable-next-line no-undef | ||
window.scrollTo({ top: targetPoint }); | ||
} | ||
/** | ||
* Call to destroy this ScrollyVideo object | ||
@@ -390,4 +516,5 @@ */ | ||
// eslint-disable-next-line no-undef | ||
if (this.trackScroll) window.removeEventListener('scroll', this.updateScrollPercentage); | ||
if (this.trackScroll) | ||
// eslint-disable-next-line no-undef | ||
window.removeEventListener('scroll', this.updateScrollPercentage); | ||
@@ -394,0 +521,0 @@ // eslint-disable-next-line no-undef |
@@ -15,3 +15,4 @@ /* eslint-disable no-undef */ | ||
getData() { | ||
if (this.idx !== this.size) throw new Error('Mismatch between size reserved and sized used'); | ||
if (this.idx !== this.size) | ||
throw new Error('Mismatch between size reserved and sized used'); | ||
return this.data.slice(0, this.idx); | ||
@@ -97,4 +98,8 @@ } | ||
*/ | ||
const decodeVideo = (src, emitFrame, { VideoDecoder, EncodedVideoChunk, debug }) => new Promise( | ||
(resolve, reject) => { | ||
const decodeVideo = ( | ||
src, | ||
emitFrame, | ||
{ VideoDecoder, EncodedVideoChunk, debug }, | ||
) => | ||
new Promise((resolve, reject) => { | ||
if (debug) console.info('Decoding video from', src); | ||
@@ -112,17 +117,16 @@ | ||
output: (frame) => { | ||
createImageBitmap(frame, { resizeQuality: 'low' }) | ||
.then((bitmap) => { | ||
emitFrame(bitmap); | ||
frame.close(); | ||
createImageBitmap(frame, { resizeQuality: 'low' }).then((bitmap) => { | ||
emitFrame(bitmap); | ||
frame.close(); | ||
if (decoder.decodeQueueSize <= 0) { | ||
// Give it an extra half second to finish everything | ||
setTimeout(() => { | ||
if (decoder.state !== 'closed') { | ||
decoder.close(); | ||
resolve(); | ||
} | ||
}, 500); | ||
} | ||
}); | ||
if (decoder.decodeQueueSize <= 0) { | ||
// Give it an extra half second to finish everything | ||
setTimeout(() => { | ||
if (decoder.state !== 'closed') { | ||
decoder.close(); | ||
resolve(); | ||
} | ||
}, 500); | ||
} | ||
}); | ||
}, | ||
@@ -138,7 +142,8 @@ error: (e) => { | ||
if (info && info.videoTracks && info.videoTracks[0]) { | ||
([{ codec }] = info.videoTracks); | ||
[{ codec }] = info.videoTracks; | ||
if (debug) console.info('Video with codec:', codec); | ||
// Gets the avccbox used for reading extradata | ||
const avccBox = mp4boxfile.moov.traks[0].mdia.minf.stbl.stsd.entries[0].avcC; | ||
const avccBox = | ||
mp4boxfile.moov.traks[0].mdia.minf.stbl.stsd.entries[0].avcC; | ||
const extradata = getExtradata(avccBox); | ||
@@ -172,28 +177,26 @@ | ||
// Fetches the file into arraybuffers | ||
fetch(src) | ||
.then((res) => { | ||
const reader = res.body.getReader(); | ||
let offset = 0; | ||
fetch(src).then((res) => { | ||
const reader = res.body.getReader(); | ||
let offset = 0; | ||
function appendBuffers({ done, value }) { | ||
if (done) { | ||
mp4boxfile.flush(); | ||
return null; | ||
} | ||
function appendBuffers({ done, value }) { | ||
if (done) { | ||
mp4boxfile.flush(); | ||
return null; | ||
} | ||
const buf = value.buffer; | ||
buf.fileStart = offset; | ||
offset += buf.byteLength; | ||
mp4boxfile.appendBuffer(buf); | ||
const buf = value.buffer; | ||
buf.fileStart = offset; | ||
offset += buf.byteLength; | ||
mp4boxfile.appendBuffer(buf); | ||
return reader.read().then(appendBuffers); | ||
} | ||
return reader.read().then(appendBuffers); | ||
} | ||
return reader.read().then(appendBuffers); | ||
}); | ||
return reader.read().then(appendBuffers); | ||
}); | ||
} catch (e) { | ||
reject(e); | ||
} | ||
}, | ||
); | ||
}); | ||
@@ -211,5 +214,13 @@ /** | ||
// If our browser supports WebCodecs natively | ||
if (typeof VideoDecoder === 'function' && typeof EncodedVideoChunk === 'function') { | ||
if (debug) console.info('WebCodecs is natively supported, using native version...'); | ||
return decodeVideo(src, emitFrame, { VideoDecoder, EncodedVideoChunk, debug }); | ||
if ( | ||
typeof VideoDecoder === 'function' && | ||
typeof EncodedVideoChunk === 'function' | ||
) { | ||
if (debug) | ||
console.info('WebCodecs is natively supported, using native version...'); | ||
return decodeVideo(src, emitFrame, { | ||
VideoDecoder, | ||
EncodedVideoChunk, | ||
debug, | ||
}); | ||
} | ||
@@ -216,0 +227,0 @@ |
{ | ||
"name": "scrolly-video", | ||
"version": "0.0.17", | ||
"version": "0.0.18", | ||
"description": "A component for scroll-based (or other externally controlled) playback.", | ||
@@ -50,4 +50,7 @@ "main": "dist/scrolly-video.js", | ||
"eslint-config-airbnb-base": "^15.0.0", | ||
"eslint-config-prettier": "^9.1.0", | ||
"eslint-plugin-import": "^2.25.4", | ||
"eslint-plugin-prettier": "^5.1.3", | ||
"eslint-plugin-svelte3": "^3.4.0", | ||
"prettier": "^3.2.4", | ||
"rimraf": "^3.0.2", | ||
@@ -66,3 +69,11 @@ "rollup": "^2.46.0", | ||
"typescript": "^4.9.5" | ||
}, | ||
"prettier": { | ||
"semi": true, | ||
"trailingComma": "all", | ||
"singleQuote": true, | ||
"printWidth": 80, | ||
"tabWidth": 2, | ||
"useTabs": false | ||
} | ||
} |
@@ -92,4 +92,8 @@ # ScrollyVideo.js | ||
***setCurrentTimePercent*** (`setPercentage | Number`): A number between 0 and 1 that specifies the percentage position of the video. | ||
***setTargetTimePercent*** (`percentage: number`, `options: Options`): Pass a progress in between of 0 and 1 that specifies the percentage position of the video. Optionally, to customise experience of separate `setTargetTimePercent` calls you can utilize options: | ||
- `transitionSpeed`: `number` | ||
- `easing`: `(progress: number) => number` | ||
Example: `setTargetTimePercent(0.5, { transitionSpeed: 12, easing: d3.easeLinear })` | ||
## Technical Details and Cross Browser Differences | ||
@@ -96,0 +100,0 @@ To make this library perform optimally in all browsers, three different approaches are taken to animating the video. |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
15584472
29026
123
27