├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package.json └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | *.mp4 4 | *.ogg 5 | *.ogv 6 | *.webm -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Watson Design Group 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | video-cache 2 | === 3 | 4 | Preload a set of videos and keep them in memory to avoid reloading them on page change. 5 | 6 | Note that the videos will hang around in memory, so this library is intended to use with a small number of videos. 7 | On mobile and tablets you might need a user interaction to play the videos. 8 | 9 | ## Install 10 | 11 | ``` 12 | npm install watsondg/video-cache -S 13 | ``` 14 | 15 | ## Usage 16 | 17 | ``` 18 | var VideoCache = require('video-cache'); 19 | 20 | var videoCache = new VideoCache(); 21 | videoCache.load([ 22 | formats: ['mp4', 'webm'], 23 | 'intro', // will try intro.mp4 and intro.webm 24 | { 25 | path: 'intro', 26 | formats: ['mp4'] 27 | } // will try intro.mp4 only 28 | ]); 29 | 30 | videoCache.once('load', function() { 31 | var video = videoCache.get('intro'); 32 | video.play(); 33 | }); 34 | 35 | ``` 36 | 37 | ## Instance Methods 38 | 39 | ### new VideoCache([options]) 40 | 41 | Create a new instance of VideoCache. 42 | * `options` - (OPTIONAL) - configuration parameters: 43 | - baseURL: the baseURL to prepend to all video paths. 44 | - formats: limit and order supported formats. Defaults to `webm, mp4, ogv, ogg`. 45 | - eventName: The event used to detect video load. Defaults to `canplaythrough`. 46 | - crossOrigin: CORS attribute. Defaults to `undefined` (no CORS). 47 | 48 | 49 | ### load(videos) 50 | 51 | Load an array of videos and append them to the cache. 52 | 53 | * `videos` - an array containing: 54 | - paths to a video (without extension) 55 | - objects containing `path` to video and `formats` array (will take priority over the formats option). 56 | 57 | 58 | ### get(id[, addToCache]) 59 | 60 | Retrieve a video from the cache. If it doesn't exist or isn't loaded, return a new video. 61 | Make the video ready for playback (unmute, reset currentTime). 62 | * `id` - the video path used for loading, minus the baseURL. 63 | * `addToCache` - if true, add the created video to the cache if not already in it. 64 | 65 | 66 | ### clear(id) 67 | 68 | Stop a video. 69 | **DO NOT** call video.remove() as this will effectively destroy the video. 70 | * `id` - the video path used for loading, minus the baseURL. 71 | 72 | 73 | ### destroy() 74 | 75 | Completely destroy all videos and the videoCache instance. 76 | 77 | ## Instance Events 78 | 79 | ### progress 80 | ### load 81 | ### error 82 | 83 | ## License 84 | MIT. 85 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var EXT_REGEX = /\.(mp4|webm|ogg|ogv)$/i; 4 | var NAME_REGEX = /(.+?)(\.[^.]*$|$)/; 5 | var Emitter = require('tiny-emitter'); 6 | 7 | module.exports = VideoCache; 8 | function VideoCache(options) { 9 | options = options || {}; 10 | this.baseURL = options.baseURL || ''; 11 | this.formats = options.formats || ['webm', 'mp4', 'ogv', 'ogg']; 12 | this.eventName = options.eventName || 'canplaythrough'; 13 | this.assetsLoaded = 0; 14 | this.totalAssets = 0; 15 | this.crossOrigin = options.crossOrigin; 16 | this.cache = Object.create(null); // Pure hash, no prototype 17 | this.el = document.createElement('div'); 18 | this.el.style.display = 'none'; 19 | document.body.appendChild(this.el); 20 | 21 | this.onError = this.onError.bind(this); 22 | } 23 | 24 | VideoCache.prototype = Object.create(Emitter.prototype); 25 | 26 | VideoCache.prototype.load = function(videos) { 27 | this.totalAssets = videos.length; 28 | 29 | videos.forEach(function(url) { 30 | var video = document.createElement('video'); 31 | 32 | var formats = url.formats || this.formats; 33 | var videoId = url.path || url; 34 | 35 | // Clean listeners on load 36 | video.onerror = this.onError; 37 | 38 | var onVideoReady = function(video) { 39 | video.onerror = null; 40 | video['on' + this.eventName] = null; 41 | this.onVideoLoad(videoId, video); 42 | }.bind(this, video); 43 | 44 | if (video.readyState > 3) onVideoReady(); 45 | else video['on' + this.eventName] = onVideoReady; 46 | 47 | // Add source for every URL (format) 48 | video.crossOrigin = this.crossOrigin; 49 | addSources(video, this.baseURL + videoId, formats); 50 | video.muted = true; 51 | video.loop = true; 52 | video.preload = 'metadata'; 53 | this.el.appendChild(video); 54 | video.load(); 55 | // video.play(); 56 | }, this); 57 | 58 | return this; 59 | }; 60 | 61 | VideoCache.prototype.onVideoLoad = function(videoId, video) { 62 | this.assetsLoaded++; 63 | video.pause(); 64 | video.currentTime = 0; 65 | this.cache[videoId] = video; 66 | 67 | this.emit('progress', this.assetsLoaded / this.totalAssets); 68 | 69 | if (this.assetsLoaded >= this.totalAssets) this.onLoad(); 70 | }; 71 | 72 | VideoCache.prototype.onLoad = function() { 73 | this.emit('load'); 74 | this.off(); 75 | }; 76 | 77 | VideoCache.prototype.onError = function(error) { 78 | this.emit('error', error); 79 | }; 80 | 81 | VideoCache.prototype.get = function(id, addToCache) { 82 | if (!this.cache) throw new Error('VideoCache has been destroyed.'); 83 | 84 | var video = this.cache[id] || createVideo(id, this.baseURL, this.formats, this.crossOrigin); 85 | if (addToCache && !this.cache[id]) this.cache[id] = video; 86 | 87 | video.muted = false; 88 | if (video.readyState > 3) video.currentTime = 0; 89 | return video; 90 | }; 91 | 92 | VideoCache.prototype.clear = function(id) { 93 | var video = this.cache[id]; 94 | if (!video) return; 95 | 96 | video.pause(); 97 | video.currentTime = 0; 98 | }; 99 | 100 | VideoCache.prototype.destroy = function() { 101 | for (var id in this.cache) { 102 | var video = this.cache[id]; 103 | video.pause(); 104 | video['on' + this.eventName] = null; 105 | video.onerror = null; 106 | Array.prototype.slice.call(video.children).forEach(function(source) { 107 | source.src = ''; 108 | }); 109 | video.load(); 110 | if (video.remove) video.remove(); 111 | else video.parentNode.removeChild(video); 112 | } 113 | this.el.parentNode.removeChild(this.el); 114 | this.cache = null; 115 | }; 116 | 117 | function addSources(video, url, formats) { 118 | var source = formats.reduce(function(sources, format) { 119 | return sources + [ 120 | '' 123 | ].join(''); 124 | }, ''); 125 | video.innerHTML = source; 126 | } 127 | 128 | function createVideo(url, baseURL, formats, crossOrigin) { 129 | var video = document.createElement('video'); 130 | var videoId = url.path || url; 131 | video.crossOrigin = crossOrigin; 132 | addSources(video, baseURL + videoId, formats); 133 | return video; 134 | } 135 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video-cache", 3 | "version": "1.2.0", 4 | "description": "Preload a set of videos and keep them in memory to avoid reloading them on page change.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "browserify test/index.js | tap-closer | smokestack | faucet", 8 | "test-debug": "budo test/index.js --live" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+ssh://git@github.com/watsondg/video-cache.git" 13 | }, 14 | "keywords": [ 15 | "video", 16 | "cache", 17 | "load" 18 | ], 19 | "author": "Florian Morel", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/watsondg/video-cache/issues" 23 | }, 24 | "homepage": "https://github.com/watsondg/video-cache#readme", 25 | "devDependencies": { 26 | "faucet": "0.0.1", 27 | "smokestack": "^3.4.1", 28 | "tap-closer": "^1.0.0", 29 | "tape": "^4.5.1" 30 | }, 31 | "dependencies": { 32 | "tiny-emitter": "^1.0.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var VideoCache = require('../index.js'); 5 | 6 | /* 7 | Tests are run using Big Buck Bunny 8 | trailer converted using Miro &/or Handbrake 9 | (not on the repo) 10 | */ 11 | var VIDEO_NAME = 'trailer_480p'; 12 | var FORMATS = ['webm', 'mp4', 'ogv']; 13 | var BASE = 'test/'; 14 | 15 | // test video load 16 | test('Video load test', function(assert) { 17 | var videoCache = new VideoCache({ 18 | formats: FORMATS, 19 | baseURL: BASE 20 | }); 21 | videoCache.once('load', function() { 22 | videoCache.destroy(); 23 | assert.pass('Loaded video'); 24 | assert.end(); 25 | }); 26 | videoCache.load([VIDEO_NAME]); 27 | }); 28 | 29 | // test video format 30 | test('Formats test', function(assert) { 31 | var videoCache = new VideoCache({ 32 | formats: FORMATS, 33 | baseURL: BASE 34 | }); 35 | videoCache.once('load', function() { 36 | var video = videoCache.get(VIDEO_NAME); 37 | assert.ok(video.children.length === 1, 'The video should have only one source.'); 38 | videoCache.destroy(); 39 | assert.end(); 40 | }); 41 | videoCache.load([{ 42 | path: VIDEO_NAME, 43 | formats: ['mp4'] 44 | }]); 45 | }); 46 | 47 | // test get + play 48 | test('Playback test', function(assert) { 49 | var videoCache = new VideoCache({ 50 | formats: FORMATS, 51 | baseURL: BASE 52 | }); 53 | videoCache.once('load', function() { 54 | function onPlay() { 55 | assert.pass('Video should play fine.'); 56 | video.removeEventListener('playing', onPlay); 57 | videoCache.destroy(); 58 | assert.end(); 59 | } 60 | var video = videoCache.get(VIDEO_NAME); 61 | video.addEventListener('playing', onPlay); 62 | video.play(); 63 | }); 64 | videoCache.load([VIDEO_NAME]); 65 | }); 66 | 67 | // test get + clear + get 68 | test('Clear playback test', function(assert) { 69 | var videoCache = new VideoCache({ 70 | formats: FORMATS, 71 | baseURL: BASE 72 | }); 73 | videoCache.once('load', function() { 74 | function onPlay() { 75 | assert.pass('Video should play fine.'); 76 | video.removeEventListener('playing', onPlay); 77 | videoCache.destroy(); 78 | assert.end(); 79 | } 80 | var video = videoCache.get(VIDEO_NAME); 81 | videoCache.clear(VIDEO_NAME); 82 | video = videoCache.get(VIDEO_NAME); 83 | video.addEventListener('playing', onPlay); 84 | video.play(); 85 | }); 86 | videoCache.load([VIDEO_NAME]); 87 | }); 88 | 89 | // test CORS 90 | test('CORS test', function(assert) { 91 | var anonymous = 'anonymous'; 92 | var videoCache = new VideoCache({ 93 | formats: FORMATS, 94 | baseURL: BASE, 95 | crossOrigin: anonymous 96 | }); 97 | videoCache.once('load', function() { 98 | var video = videoCache.get(VIDEO_NAME); 99 | assert.ok(video.crossOrigin == anonymous, 'Video CORS should be anonymous.'); 100 | videoCache.clear(VIDEO_NAME); 101 | videoCache.destroy(); 102 | assert.end(); 103 | }); 104 | videoCache.load([VIDEO_NAME]); 105 | }); 106 | 107 | 108 | // test destroy 109 | test('Destroy test', function(assert) { 110 | var videoCache = new VideoCache({ 111 | formats: FORMATS, 112 | baseURL: BASE 113 | }); 114 | videoCache.once('load', function() { 115 | var video = videoCache.get(VIDEO_NAME); 116 | assert.ok(!!video, 'Video should exist.'); 117 | videoCache.clear(VIDEO_NAME); 118 | videoCache.destroy(); 119 | 120 | try { 121 | video = videoCache.get(VIDEO_NAME); 122 | } catch(e) { 123 | assert.pass('VideoCache should throw an error.'); 124 | } 125 | assert.end(); 126 | }); 127 | videoCache.load([VIDEO_NAME]); 128 | }); 129 | --------------------------------------------------------------------------------