├── .gitignore ├── README.md ├── bower.json ├── converter ├── nodejs │ ├── app.js │ └── package.json ├── nodejs_one │ ├── app.js │ └── package.json └── php │ └── to_data_uri.php ├── dist ├── frameplayer.min.css └── frameplayer.min.js ├── gulpfile.js ├── index.html ├── package.json ├── run_server.py ├── src ├── frameplayer.css └── frameplayer.js ├── videos ├── dragons.json ├── video01.json └── video02.json └── videos_src └── dragons.mp4 /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Frame Player 2 | 3 | A video player without video files, just JSON. Based on "images frames" thought to mobile devices! 4 | 5 | - Project Page: [http://vagnervjs.github.io/frame-player](http://vagnervjs.github.io/frame-player) 6 | 7 | ##The Problem 8 | The problem of today's HTML5 video is that it can't be played in-line on an HTML page on mobile devices. The way the handheld devices handle it is they open the video in a native player which takes over the page itself, making it impossible to implement any interaction while the video is playing. Also, you can't play two videos at the same time. 9 | 10 | ##The Solution 11 | Create a player instead of playing video files, show a sequence of images at a certain rate. 12 | 13 | ##Instalation 14 | 15 | - Download the [latest version](https://github.com/vagnervjs/frame-player/releases/) of Frame Player. 16 | 17 | - **OR** Use **bower** to install Frame Player 18 | 19 | ```bash 20 | bower install frame-player 21 | ``` 22 | 23 | - Put the script and the style on your page 24 | 25 | ```html 26 | 27 | 28 | ``` 29 | 30 | ##Usage 31 | 32 | - Insert this HTML code on any part of your page and set the data-src attribute for your JSON video file 33 | 34 | ```html 35 |
36 | ``` 37 | 38 | - Set the options 39 | 40 | ```javascript 41 | var options = ({ 42 | 'rate': 30, 43 | 'controls': false, 44 | 'autoplay': true, 45 | 'backwards': false, 46 | 'startFrame': 10, 47 | 'width': '640px', 48 | 'height': '390px', 49 | // 'radius': '50%' 50 | }); 51 | ``` 52 | 53 | - Init the player 54 | 55 | ```javascript 56 | var player = new FramePlayer('my-player', options); 57 | player.play(); 58 | ``` 59 | 60 | ### Methods 61 | 62 | Method | Parameters | Returns | Description 63 | --- | --- | --- | --- 64 | `play()` | None. | Nothing. | Start playing the video. 65 | `pause()` | None. | Nothing. | Pause the current video. 66 | `resume()` | None. | Nothing. | Play the current video from the moment it was paused. 67 | `gotoFrame()` | Integer. | Nothing. | Jumps to a specific frame of the video. 68 | 69 | 70 | ##Generating the JSON Video File (ffmpeg lib must be installed) 71 | 72 | - Option 1: Node.js - single command 73 | 74 | ```bash 75 | cd converter/nodejs_one 76 | node app.js path/to/video/file path/to/video.json/file startTime endTime 77 | ``` 78 | 79 | - Option 2: Node.js 80 | 81 | - Use ffmpeg to generate the frames from a video file: 82 | 83 | ```bash 84 | ffmpeg -i video.mp4 -an -f image2 "%d.jpg" 85 | ``` 86 | 87 | - Convert all frames on a single JSON file 88 | 89 | ```bash 90 | cd converter/nodejs 91 | node app.js frameStart frameEnd folder/to/imgs/ json/video.json 92 | ``` 93 | 94 | - Option 3: PHP 95 | 96 | - Use ffmpeg to generate the frames from a video file: 97 | 98 | ```bash 99 | ffmpeg -i video.mp4 -an -f image2 "%d.jpg" 100 | ``` 101 | 102 | ```bash 103 | cd converter/php 104 | php to_data_uri.php frameStart frameEnd folder/to/imgs/ json/video.json 105 | ``` 106 | 107 | ## Development 108 | 109 | In order to run it locally you'll need to fetch some dependencies and a basic setup. 110 | 111 | 1. Install [Gulp](http://gulpjs.com/): 112 | 113 | ```sh 114 | $ [sudo] npm install --global gulp 115 | ``` 116 | 117 | 2. Install local dependencies: 118 | 119 | ```sh 120 | $ npm install 121 | ``` 122 | 123 | 3. To test your project, start the development server (using your prefered server) and open `http://localhost:8000`. 124 | 125 | ```sh 126 | $ python -m SimpleHTTPServer 8080 127 | ``` 128 | 129 | 4. To build the distribution files before releasing a new version. 130 | 131 | ```sh 132 | $ gulp build 133 | ``` 134 | 135 | 5. Send everything to `gh-pages` branch. 136 | 137 | 138 | ## Contributing 139 | 140 | 1. Fork it! 141 | 2. Create your feature branch: `git checkout -b my-new-feature` 142 | 3. Commit your changes: `git commit -m 'Add some feature'` 143 | 4. Push to the branch: `git push origin my-new-feature` 144 | 5. Submit a pull request :D 145 | 146 | ## Author 147 | 148 | [![Vagner Santana](http://gravatar.com/avatar/d050e3a593aa5c49738028ade14606ed?s=70)](http://vagnersantana.com) | 149 | --- | --- | --- | --- | --- | --- | --- 150 | [Vagner Santana](http://vagnersantana.com)
[@vagnervjs](http://twitter.com/vagnervjs)| 151 | 152 | 153 | 154 | 155 | ## License 156 | 157 | - Code is under [MIT license](http://vagnersantana.mit-license.org) © Vagner Santana 158 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frame-player", 3 | "version": "0.2.0", 4 | "homepage": "https://github.com/vagnervjs/frame-player", 5 | "authors": [ 6 | "Vagner Santana " 7 | ], 8 | "description": "A video player without video files, just JSON. Based on \"images frames\" thought to mobile devices!", 9 | "main": "src/js/frameplayer.js", 10 | "keywords": [ 11 | "json", 12 | "player", 13 | "frame", 14 | "mobile" 15 | ], 16 | "license": "MIT", 17 | "ignore": [ 18 | "**/.*", 19 | "node_modules", 20 | "bower_components", 21 | "test", 22 | "tests", 23 | "videos", 24 | "js", 25 | "css" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /converter/nodejs/app.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | util = require('util'), 3 | mime = require('mime'), 4 | path = require('path'); 5 | 6 | function base64Img(src) { 7 | var data = fs.readFileSync(src).toString('base64'); 8 | return util.format('data:%s;base64,%s', mime.lookup(src), data); 9 | } 10 | 11 | /* 12 | * Possible argument positions 13 | * 14 | * converter frameStart(number) frameEnd(number) folder(string) outputFile(string) 15 | * converter folder(string) outputFile(string) 16 | * 17 | */ 18 | 19 | if(process.argv.length > 2) { 20 | // Check if the first argument is a number or string 21 | if(!isNaN(parseInt(process.argv[2]))) { 22 | var frameStart = process.argv[2], 23 | frameEnd = process.argv[3], 24 | folder = process.argv[4], 25 | outputFile = process.argv[5], 26 | dataUri = null, 27 | framesImg = []; 28 | } else { 29 | var frameStart = 1, 30 | frameEnd = fs.readdirSync(process.argv[2]).length, 31 | folder = process.argv[2], 32 | outputFile = process.argv[3]; 33 | } 34 | 35 | var dataUri = null, 36 | framesImg = [], 37 | fileTypeExt = path.extname(fs.readdirSync(folder)[0]); //If there's a better way, please do it... 38 | 39 | for (; frameStart <= frameEnd; frameStart++) { 40 | console.log('Converting file: ' + frameStart); 41 | dataUri = base64Img(folder + frameStart + fileTypeExt); 42 | framesImg[frameStart] = dataUri; 43 | } 44 | 45 | framesImg.splice(null, 1); 46 | 47 | var json = { 48 | frames: framesImg 49 | }; 50 | 51 | fs.writeFile(outputFile, JSON.stringify(json), function(err) { 52 | if(err) { 53 | console.log(err); 54 | } else { 55 | console.log('Completed!'); 56 | } 57 | }); 58 | } else { 59 | console.error('Not enough parameters supplied!'); 60 | } -------------------------------------------------------------------------------- /converter/nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Frame-Player-Img-to-dataURI", 3 | "version": "0.0.1", 4 | "description": "Converts a sequence of images in dataURI base64", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/vagnervjs/frame-player" 12 | }, 13 | "keywords": [ 14 | "frame-player", 15 | "datauri", 16 | "base64" 17 | ], 18 | "author": "Vagner Santana", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/vagnervjs/frame-player/issues" 22 | }, 23 | "devDependencies": { 24 | "mime": "^1.2.11", 25 | "path": "^0.4.9" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /converter/nodejs_one/app.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | util = require('util'), 3 | mime = require('mime'), 4 | path = require('path'), 5 | ffmpeg = require('fluent-ffmpeg'), 6 | mkdirp = require('mkdirp'); 7 | 8 | /* 9 | * Possible argument positions 10 | * 11 | * converter pathVideo(string) pathVideoFrame(string) startVideoTime(number) endVideoTime(number) 12 | * converter pathVideo(string) pathVideoFrame(string) startVideoTime(number) 13 | * converter pathVideo(string) pathVideoFrame(string) 14 | * 15 | */ 16 | 17 | if(process.argv.length > 3) { 18 | var pathVideo = process.argv[2], 19 | pathVideoFrame = process.argv[3], 20 | startVideoTime = parseInt(process.argv[4]), 21 | endVideoTime = parseInt(process.argv[5]), 22 | tempDirName = path.resolve('/tmp/', '.images'); 23 | var json = {}; 24 | 25 | Promise.resolve().then(function(){ 26 | createOrCleanTempDir(tempDirName); 27 | }). 28 | then(function(){ 29 | return createVideoFrames(pathVideo, startVideoTime, endVideoTime, tempDirName); 30 | }). 31 | then(function(){ 32 | json.frames = generateStringVideoFrames(tempDirName); 33 | saveVideoFrame(pathVideoFrame, JSON.stringify(json)); 34 | }). 35 | catch(function(err){ 36 | console.log('Error -', err); 37 | }) 38 | } else { 39 | console.error('Not enough parameters supplied!'); 40 | } 41 | 42 | function createOrCleanTempDir(tempDirName){ 43 | mkdirp(tempDirName, function(err){ 44 | fs.readdirSync(tempDirName).map(function(elem, index){ 45 | fs.unlinkSync(path.resolve(tempDirName, elem)); 46 | }); 47 | }) 48 | } 49 | 50 | function createVideoFrames(_path, startTime, endTime, tempDirName){ 51 | return promise = new Promise(function(resolve, reject){ 52 | var command = ffmpeg(path.resolve(_path)) 53 | .noAudio() 54 | .on('end', function(){ 55 | resolve(); 56 | }) 57 | .on('error', function(err){ 58 | reject(err); 59 | }) 60 | 61 | if (startTime){ 62 | command = command.seekInput(startTime); 63 | } 64 | if (endTime){ 65 | if (startTime > endTime){ 66 | reject('Start time must be lower than end time') 67 | } 68 | command = command.duration(endTime - startTime); 69 | } 70 | 71 | command.save(path.resolve(tempDirName, 'image-%07d.jpg')); 72 | }); 73 | } 74 | 75 | function generateStringVideoFrames(tempDirName){ 76 | return fs.readdirSync(tempDirName).sort().map(function(elem, index){ 77 | return base64Img(path.resolve(tempDirName, elem)); 78 | }) 79 | } 80 | 81 | function base64Img(src) { 82 | var data = fs.readFileSync(src).toString('base64'); 83 | return util.format('data:%s;base64,%s', mime.lookup(src), data); 84 | } 85 | 86 | function saveVideoFrame(_path, data){ 87 | fs.writeFileSync(_path, data); 88 | } 89 | -------------------------------------------------------------------------------- /converter/nodejs_one/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs_one", 3 | "version": "1.0.0", 4 | "description": "One command converter from video file to json base64 frames", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "AlexBazer", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "fluent-ffmpeg": "^2.0.1", 13 | "mime": "^1.3.4", 14 | "mkdirp": "^0.5.1", 15 | "path": "^0.12.7" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /converter/php/to_data_uri.php: -------------------------------------------------------------------------------- 1 | buffer(file_get_contents($image)); 16 | return 'data:'.$mime.';base64,'.base64_encode(file_get_contents($image)); 17 | } 18 | 19 | $frameStart = $argv[1]; 20 | $frameEnd = $argv[2]; 21 | $folder = $argv[3]; 22 | $outputFile = $argv[4]; 23 | 24 | for ($i= $frameStart; $i < $frameEnd; $i++) { 25 | $image = $folder . $i . '.jpg'; 26 | $frames[] = getDataURI($image); 27 | } 28 | 29 | $json['frames'] = $frames; 30 | 31 | $file = fopen($outputFile, 'w'); 32 | fwrite($file, json_encode($json)); 33 | fclose($file); 34 | -------------------------------------------------------------------------------- /dist/frameplayer.min.css: -------------------------------------------------------------------------------- 1 | .frameplayer{background:#ccc;position:relative}.frameplayer canvas{width:100%;height:100%}.frameplayer>.fp-ctrl{position:absolute;bottom:0;left:0;margin:0;z-index:1;opacity:.6;height:40px;background:#000}.fp-ctrl>.fp-btn{margin:10px 10px 0;float:left}.fp-ctrl>.fp-select{margin-top:10px;float:left;text-transform:capitalize}input.to-frame{width:20px;margin-right:6px}label.to-frame{margin:10px 10px 0;float:left}.frameplayer>.fp-loading{text-align:center;line-height:15;font-size:16px;font-family:Helvetica,Arial,sans-serif;position:absolute;top:0;left:0;width:100%;height:100%}.fp-grayscale{-webkit-filter:grayscale(100%);filter:grayscale(100%)}.fp-sepia{-webkit-filter:sepia(100%);filter:sepia(100%)}.fp-invert{-webkit-filter:invert(100%);filter:invert(100%)} -------------------------------------------------------------------------------- /dist/frameplayer.min.js: -------------------------------------------------------------------------------- 1 | var FramePlayer=function(e,t){this.divCont=document.getElementById(e),this.elem=e,this.jsonVideoSrc=this.divCont.getAttribute("data-vidsrc"),this.rate=20,this.controls=!0,this.paused=!1,this.width="480px",this.height="320px",this.backwards=!1,this.currentFrame=-1,this.startFrame=0,this.radius=null,this.setOptions(t),this.initializeRequestAnimationFrame(),this.img=document.createElement("img"),this.canvas=document.createElement("canvas"),this.context=this.canvas.getContext("2d"),this.divCont.appendChild(this.canvas)};FramePlayer.prototype.setOptions=function(e){if("rate"in e&&(this.rate=e.rate),"controls"in e&&(this.controls=e.controls),"autoplay"in e&&(e.autoplay||(this.paused=!0)),"width"in e&&(this.width=e.width),"height"in e&&(this.height=e.height),"startFrame"in e&&(this.startFrame=this.currentFrame=e.startFrame),"backwards"in e&&(this.backwards=e.backwards),"radius"in e){var t=document.createElement("style");t.setAttribute("id","style-"+this.elem),t.innerHTML="#"+this.elem+", .frames-"+this.elem+"{ border-radius: "+e.radius+"; overflow: hidden;}",document.head.appendChild(t)}this.divCont.style.width=this.width,this.divCont.style.height=this.height,this.controls&&this.createControlBar()},FramePlayer.prototype.render=function(e){var t,a,n=Date.now(),i=1e3/e.rate,r=e.jsonVideoFile.frames.length,s=function(){t=Date.now(),a=t-n,a>i&&(n=t-a%i,e.paused||(e.currentFrame=e.backwards?e.currentFrame-=1:e.currentFrame+=1,e.currentFrame>=r?e.currentFrame=0:e.currentFrame<0&&(e.currentFrame=r-1),e.drawFrame(e))),window.requestAnimationFrame(s)};window.requestAnimationFrame(s)},FramePlayer.prototype.drawFrame=function(e){e.img.src=e.jsonVideoFile.frames[e.currentFrame],e.context.drawImage(e.img,0,0,e.canvas.width,e.canvas.height)},FramePlayer.prototype.createControlBar=function(){var e=this,t=document.createElement("div");t.setAttribute("class","fp-ctrl"),t.style.width=this.width;var a=document.createElement("button");a.setAttribute("id","pause-"+e.elem),a.setAttribute("class","fp-btn"),a.innerHTML="Pause",a.addEventListener("click",function(){e.pause()},!1),t.appendChild(a);var n=document.createElement("button");n.setAttribute("id","play-"+e.elem),n.setAttribute("class","fp-btn"),n.innerHTML="Play",n.addEventListener("click",function(){e.resume()},!1),t.appendChild(n);var i=document.createElement("button");i.setAttribute("id","backwards-"+e.elem),i.setAttribute("class","fp-btn"),i.innerHTML="Backward",i.addEventListener("click",function(){e.reverse()},!1),t.appendChild(i),e.paused?a.style.display="none":n.style.display="none";for(var r=document.createElement("select"),s=["normal","grayscale","sepia","invert"],o=0,l=s.length;l>o;o++){var d=document.createElement("option");d.setAttribute("value",s[o]),d.innerHTML=s[o],r.appendChild(d)}r.setAttribute("id","filter-"+e.elem),r.setAttribute("class","fp-select"),r.addEventListener("change",function(){e.setFilter(this.value)},!1),t.appendChild(r);var c=document.createElement("label"),m=document.createElement("input"),u=document.createElement("input");c.className="to-frame",m.type="text",m.name="frame",m.value=e.startFrame,m.className="to-frame",e.toFrameInput=m,u.type="submit",u.value="go to frame",u.onclick=function(){value=parseInt(m.value,10),e.gotoFrame(value)},c.appendChild(m),c.appendChild(u),t.appendChild(c),this.divCont.appendChild(t)},FramePlayer.prototype.play=function(){this.getFile(this.jsonVideoSrc,function(e){e.paused?(e.render(e),e.drawFrame(e)):e.render(e)})},FramePlayer.prototype.resume=function(){var e=document.getElementById("play-"+this.elem),t=document.getElementById("pause-"+this.elem);e.style.display="none",t.style.display="block",this.paused=!1},FramePlayer.prototype.pause=function(){var e=document.getElementById("play-"+this.elem),t=document.getElementById("pause-"+this.elem);e.style.display="block",t.style.display="none",this.paused=!0},FramePlayer.prototype.reverse=function(){var e=document.getElementById("backwards-"+this.elem);this.backwards=!this.backwards,value=this.backwards?"Forward":"Backward",e.innerHTML=value},FramePlayer.prototype.gotoFrame=function(e){e===parseInt(e,10)&&(this.currentFrame=this.startFrame=this.toFrameInput.value=e,void 0===this.jsonVideoFile?this.play():this.drawFrame(this))},FramePlayer.prototype.setFilter=function(e){var t=document.querySelector("#"+this.elem+" canvas");switch(e){case"normal":t.setAttribute("class","");break;case"grayscale":t.setAttribute("class","fp-grayscale");break;case"sepia":t.setAttribute("class","fp-sepia");break;case"invert":t.setAttribute("class","fp-invert")}},FramePlayer.prototype.getFile=function(e,t){var a=new XMLHttpRequest,n=this,i=document.createElement("p");if(!a)throw"Error loading file.";a.open("GET",e,!0),a.setRequestHeader("Content-Type","application/json;charset=UTF-8"),a.send(null),a.onprogress=function(){i.innerHTML="Loading...",i.setAttribute("class","fp-loading"),n.divCont.appendChild(i)},void 0!==typeof a.onload?a.onload=function(){n.divCont.removeChild(i),n.jsonVideoFile=JSON.parse(this.responseText),t(n),a=null}:a.onreadystatechange=function(){4===a.readyState&&(n.divCont.removeChild(i),n.jsonVideoFile=JSON.parse(this.responseText),t(n),a=null)}},FramePlayer.prototype.initializeRequestAnimationFrame=function(){for(var e=0,t=["ms","moz","webkit","o"],a=0;a 2 | 3 | 4 | 5 | Frame Player 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frame-player", 3 | "version": "0.2.0", 4 | "description": "A video player without video files, just JSON. Based on \"images frames\" thought to mobile devices!", 5 | "main": "src/fameplayer.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/vagnervjs/frame-player.git" 12 | }, 13 | "keywords": [ 14 | "frame", 15 | "player", 16 | "frame-player", 17 | "video", 18 | "json", 19 | "images", 20 | "mobile" 21 | ], 22 | "author": "Vagner Santana", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/vagnervjs/frame-player/issues" 26 | }, 27 | "homepage": "https://github.com/vagnervjs/frame-player", 28 | "devDependencies": { 29 | "del": "^1.1.1", 30 | "gulp": "^3.8.11", 31 | "gulp-concat": "^2.5.2", 32 | "gulp-cssmin": "^0.1.6", 33 | "gulp-uglify": "^1.1.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /run_server.py: -------------------------------------------------------------------------------- 1 | # run_server.py 2 | import SimpleHTTPServer 3 | 4 | m = SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map 5 | m[''] = 'text/plain' 6 | m.update(dict([(k, v + ';charset=UTF-8') for k, v in m.items()])) 7 | 8 | SimpleHTTPServer.test() 9 | -------------------------------------------------------------------------------- /src/frameplayer.css: -------------------------------------------------------------------------------- 1 | /* 2 | frameplayer.css 3 | A video player without video files, just JSON. Based on "images frames" thought to mobile devices! 4 | 5 | @author: Vagner Santana 6 | @link: http://github.com/vagnervjs/frame-video 7 | @version: 0.2.0 8 | @since: 04/10/2013 9 | */ 10 | 11 | /* Player */ 12 | .frameplayer { 13 | background: #ccc; 14 | position: relative; 15 | } 16 | 17 | .frameplayer canvas { 18 | width: 100%; 19 | height: 100%; 20 | } 21 | 22 | /* Control Bar */ 23 | .frameplayer > .fp-ctrl { 24 | position: absolute; 25 | bottom: 0; 26 | left: 0; 27 | margin: 0; 28 | z-index: 1; 29 | opacity: .6; 30 | height: 40px; 31 | background: #000; 32 | } 33 | 34 | /* Buttons */ 35 | .fp-ctrl > .fp-btn { 36 | margin: 10px 10px 0 10px; 37 | float: left; 38 | } 39 | 40 | .fp-ctrl > .fp-select { 41 | margin-top: 10px; 42 | float: left; 43 | text-transform: capitalize; 44 | } 45 | 46 | input.to-frame { 47 | width: 20px; 48 | margin-right:6px; 49 | } 50 | 51 | label.to-frame { 52 | margin: 10px 10px 0 10px; 53 | float: left; 54 | } 55 | 56 | /* Loading Message */ 57 | .frameplayer > .fp-loading { 58 | text-align: center; 59 | line-height: 15; 60 | font-size: 16px; 61 | font-family: Helvetica, Arial, sans-serif; 62 | position: absolute; 63 | top: 0; 64 | left: 0; 65 | width: 100%; 66 | height: 100%; 67 | } 68 | 69 | /* Filters */ 70 | .fp-grayscale { 71 | -webkit-filter: grayscale(100%); 72 | filter: grayscale(100%); 73 | } 74 | .fp-sepia { 75 | -webkit-filter: sepia(100%); 76 | filter: sepia(100%); 77 | } 78 | .fp-invert { 79 | -webkit-filter: invert(100%); 80 | filter: invert(100%); 81 | } 82 | -------------------------------------------------------------------------------- /src/frameplayer.js: -------------------------------------------------------------------------------- 1 | /* 2 | frameplayer.js 3 | A video player without video files, just JSON. Based on "images frames" thought to mobile devices! 4 | 5 | @author: Vagner Santana 6 | @link: http://github.com/vagnervjs/frame-video 7 | @version: 0.2.0 8 | @since: 04/10/2013 9 | */ 10 | 11 | var FramePlayer = function(el, options) { 12 | this.divCont = document.getElementById(el); 13 | this.elem = el; 14 | this.jsonVideoSrc = this.divCont.getAttribute('data-vidsrc'); 15 | this.rate = 20, 16 | this.controls = true, 17 | this.paused = false, 18 | this.width = '480px', 19 | this.height = '320px'; 20 | this.backwards = false; 21 | this.currentFrame = -1; 22 | this.startFrame = 0 23 | this.radius = null; 24 | 25 | this.setOptions(options); 26 | this.initializeRequestAnimationFrame(); 27 | 28 | this.img = document.createElement('img'); 29 | this.canvas = document.createElement('canvas'); 30 | this.context = this.canvas.getContext('2d'); 31 | this.divCont.appendChild(this.canvas); 32 | }; 33 | 34 | FramePlayer.prototype.setOptions = function(options) { 35 | if ('rate' in options) { this.rate = options.rate; } 36 | if ('controls' in options) { this.controls = options.controls;} 37 | if ('autoplay' in options) { if (!options.autoplay) { this.paused = true; } } 38 | if ('width' in options) { this.width = options.width; } 39 | if ('height' in options) { this.height = options.height; } 40 | if ('startFrame' in options) { this.startFrame = this.currentFrame = options.startFrame; } 41 | if ('backwards' in options) { this.backwards = options.backwards; } 42 | if ('radius' in options) { 43 | var currentStyle = document.createElement('style'); 44 | currentStyle.setAttribute('id', 'style-' + this.elem); 45 | currentStyle.innerHTML = '#' + this.elem + ', .frames-' + this.elem + '{ border-radius: ' + options.radius + '; overflow: hidden;}'; 46 | document.head.appendChild(currentStyle); 47 | } 48 | 49 | this.divCont.style.width = this.width; 50 | this.divCont.style.height = this.height; 51 | 52 | if(this.controls) { 53 | this.createControlBar(); 54 | } 55 | }; 56 | 57 | FramePlayer.prototype.render = function(player) { 58 | 59 | var now, 60 | then = Date.now(), 61 | interval = 1000/player.rate, 62 | delta, 63 | videoFramesNum = player.jsonVideoFile.frames.length; 64 | 65 | var processFrame = function() { 66 | 67 | now = Date.now(); 68 | delta = now - then; 69 | 70 | if (delta > interval) { 71 | then = now - (delta % interval); 72 | 73 | if(!player.paused) { 74 | 75 | player.currentFrame = (player.backwards) ? player.currentFrame -= 1 : player.currentFrame += 1; 76 | 77 | if (player.currentFrame >= videoFramesNum) player.currentFrame = 0; 78 | else if (player.currentFrame < 0) player.currentFrame = videoFramesNum-1; 79 | 80 | player.drawFrame(player); 81 | } 82 | } 83 | 84 | window.requestAnimationFrame(processFrame); 85 | }; 86 | 87 | 88 | window.requestAnimationFrame(processFrame); 89 | }; 90 | 91 | FramePlayer.prototype.drawFrame = function(player) { 92 | player.img.src = player.jsonVideoFile.frames[player.currentFrame]; 93 | player.context.drawImage(player.img, 0, 0, player.canvas.width, player.canvas.height); 94 | }; 95 | 96 | FramePlayer.prototype.createControlBar = function() { 97 | var _self = this, 98 | controlBar = document.createElement('div'); 99 | controlBar.setAttribute('class', 'fp-ctrl'); 100 | controlBar.style.width = this.width; 101 | 102 | // Pause Button 103 | var btnPause = document.createElement('button'); 104 | btnPause.setAttribute('id', 'pause-' + _self.elem); 105 | btnPause.setAttribute('class', 'fp-btn'); 106 | btnPause.innerHTML = 'Pause'; 107 | btnPause.addEventListener('click', function() { 108 | _self.pause(); 109 | }, false 110 | ); 111 | controlBar.appendChild(btnPause); 112 | 113 | // Play Button 114 | var btnPlay = document.createElement('button'); 115 | btnPlay.setAttribute('id', 'play-' + _self.elem); 116 | btnPlay.setAttribute('class', 'fp-btn'); 117 | btnPlay.innerHTML = 'Play'; 118 | btnPlay.addEventListener('click', function() { 119 | _self.resume(); 120 | }, false 121 | ); 122 | controlBar.appendChild(btnPlay); 123 | 124 | // Backwards Button 125 | var btnBackwards = document.createElement('button'); 126 | btnBackwards.setAttribute('id', 'backwards-' + _self.elem); 127 | btnBackwards.setAttribute('class', 'fp-btn'); 128 | btnBackwards.innerHTML = 'Backward'; 129 | btnBackwards.addEventListener('click', function() { 130 | _self.reverse() 131 | }, false 132 | ); 133 | controlBar.appendChild(btnBackwards); 134 | 135 | // Display Play/Pause Button 136 | _self.paused ? btnPause.style.display = 'none' : btnPlay.style.display = 'none'; 137 | 138 | // Filter Select 139 | var selectFilter = document.createElement('select'), 140 | options = ['normal', 'grayscale', 'sepia', 'invert']; 141 | 142 | for (var i = 0, t = options.length; i < t; i++) { 143 | var $option = document.createElement('option'); 144 | 145 | $option.setAttribute('value', options[i]); 146 | $option.innerHTML = options[i]; 147 | selectFilter.appendChild($option); 148 | } 149 | 150 | selectFilter.setAttribute('id', 'filter-' + _self.elem); 151 | selectFilter.setAttribute('class', 'fp-select'); 152 | selectFilter.addEventListener('change', function() { 153 | _self.setFilter(this.value); 154 | }, false 155 | ); 156 | controlBar.appendChild(selectFilter); 157 | 158 | var toFrameLabel = document.createElement('label'), 159 | toFrameInput = document.createElement('input'), 160 | toFrameSubmit = document.createElement('input'); 161 | 162 | toFrameLabel.className = "to-frame"; 163 | 164 | toFrameInput.type = 'text'; 165 | toFrameInput.name = 'frame'; 166 | toFrameInput.value = _self.startFrame; 167 | toFrameInput.className = "to-frame"; 168 | _self.toFrameInput = toFrameInput; 169 | 170 | toFrameSubmit.type = 'submit'; 171 | toFrameSubmit.value = 'go to frame'; 172 | 173 | toFrameSubmit.onclick = function() { 174 | value = parseInt(toFrameInput.value, 10) 175 | _self.gotoFrame(value); 176 | } 177 | 178 | toFrameLabel.appendChild(toFrameInput); 179 | toFrameLabel.appendChild(toFrameSubmit); 180 | controlBar.appendChild(toFrameLabel); 181 | 182 | // Add control bar 183 | this.divCont.appendChild(controlBar); 184 | }; 185 | 186 | FramePlayer.prototype.play = function() { 187 | this.getFile(this.jsonVideoSrc, function(player) { 188 | if (player.paused) { 189 | player.render(player); 190 | player.drawFrame(player); 191 | }else{ 192 | player.render(player); 193 | } 194 | }); 195 | }; 196 | 197 | FramePlayer.prototype.resume = function() { 198 | var btnPlay = document.getElementById('play-' + this.elem), 199 | btnPause = document.getElementById('pause-' + this.elem); 200 | 201 | btnPlay.style.display = 'none'; 202 | btnPause.style.display = 'block'; 203 | this.paused = false; 204 | }; 205 | 206 | FramePlayer.prototype.pause = function() { 207 | var btnPlay = document.getElementById('play-' + this.elem), 208 | btnPause = document.getElementById('pause-' + this.elem); 209 | 210 | btnPlay.style.display = 'block'; 211 | btnPause.style.display = 'none'; 212 | this.paused = true; 213 | }; 214 | 215 | FramePlayer.prototype.reverse = function() { 216 | var btnBackwards = document.getElementById('backwards-' + this.elem); 217 | this.backwards = !this.backwards; 218 | value = this.backwards ? 'Forward' :'Backward'; 219 | btnBackwards.innerHTML = value; 220 | }; 221 | 222 | FramePlayer.prototype.gotoFrame = function(value) { 223 | 224 | if (value !== parseInt(value, 10)) return; 225 | 226 | this.currentFrame = this.startFrame = this.toFrameInput.value = value; 227 | 228 | if (this.jsonVideoFile === undefined) { 229 | this.play(); 230 | } else { 231 | this.drawFrame(this); 232 | } 233 | }; 234 | 235 | FramePlayer.prototype.setFilter = function(filter) { 236 | var canvas = document.querySelector('#' + this.elem + ' canvas'); 237 | 238 | switch (filter) { 239 | case 'normal': 240 | canvas.setAttribute('class', ''); 241 | break; 242 | case 'grayscale': 243 | canvas.setAttribute('class', 'fp-grayscale'); 244 | break; 245 | case 'sepia': 246 | canvas.setAttribute('class', 'fp-sepia'); 247 | break; 248 | case 'invert': 249 | canvas.setAttribute('class', 'fp-invert'); 250 | break; 251 | default: 252 | break; 253 | } 254 | }; 255 | 256 | FramePlayer.prototype.getFile = function(src, callback) { 257 | var _HTTP = new XMLHttpRequest(), 258 | _self = this, 259 | p = document.createElement('p'); 260 | 261 | if (_HTTP) { 262 | _HTTP.open('GET', src, true); 263 | _HTTP.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); 264 | _HTTP.send(null); 265 | 266 | _HTTP.onprogress = function() { 267 | p.innerHTML = 'Loading...'; 268 | p.setAttribute('class', 'fp-loading'); 269 | _self.divCont.appendChild(p); 270 | }; 271 | 272 | if (typeof(_HTTP.onload) !== undefined) { 273 | _HTTP.onload = function() { 274 | _self.divCont.removeChild(p); 275 | _self.jsonVideoFile = JSON.parse(this.responseText) 276 | callback(_self); 277 | _HTTP = null; 278 | }; 279 | } else { 280 | _HTTP.onreadystatechange = function() { 281 | if (_HTTP.readyState === 4) { 282 | _self.divCont.removeChild(p); 283 | _self.jsonVideoFile = JSON.parse(this.responseText) 284 | callback(_self); 285 | _HTTP = null; 286 | } 287 | }; 288 | } 289 | } else { 290 | throw('Error loading file.'); 291 | } 292 | }; 293 | 294 | // Polyfill 295 | FramePlayer.prototype.initializeRequestAnimationFrame = function() { 296 | // http://paulirish.com/2011/requestanimationframe-for-smart-animating/ 297 | // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating 298 | // requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel 299 | // MIT license 300 | 301 | var lastTime = 0; 302 | var vendors = ['ms', 'moz', 'webkit', 'o']; 303 | for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 304 | window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; 305 | window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] 306 | || window[vendors[x]+'CancelRequestAnimationFrame']; 307 | } 308 | 309 | if (!window.requestAnimationFrame) 310 | window.requestAnimationFrame = function(callback, element) { 311 | var currTime = new Date().getTime(); 312 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 313 | var id = window.setTimeout(function() { callback(currTime + timeToCall); }, 314 | timeToCall); 315 | lastTime = currTime + timeToCall; 316 | return id; 317 | }; 318 | 319 | if (!window.cancelAnimationFrame) 320 | window.cancelAnimationFrame = function(id) { 321 | clearTimeout(id); 322 | }; 323 | }; 324 | -------------------------------------------------------------------------------- /videos_src/dragons.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vagnervjs/frame-player/682164d688d49440e2539f52370b5abde98c364b/videos_src/dragons.mp4 --------------------------------------------------------------------------------