├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bower.json ├── index.html ├── package.json ├── screenshot.png ├── scripts ├── banner.ejs ├── build-test.js ├── postversion.js ├── server.js └── version.js ├── src ├── plugin.js └── plugin.scss └── test ├── index.html ├── karma.conf.js └── plugin.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS 2 | Thumbs.db 3 | ehthumbs.db 4 | Desktop.ini 5 | .DS_Store 6 | ._* 7 | 8 | # Editors 9 | *~ 10 | *.swp 11 | *.tmproj 12 | *.tmproject 13 | *.sublime-* 14 | .idea/ 15 | .project/ 16 | .settings/ 17 | .vscode/ 18 | 19 | # Logs 20 | logs 21 | *.log 22 | npm-debug.log* 23 | 24 | # Dependency directories 25 | bower_components/ 26 | node_modules/ 27 | 28 | # Yeoman meta-data 29 | .yo-rc.json 30 | 31 | # Build-related directories 32 | dist/ 33 | docs/api/ 34 | es5/ 35 | test/dist/ 36 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Intentionally left blank, so that npm does not ignore anything by default, 2 | # but relies on the package.json "files" array to explicitly define what ends 3 | # up in the package. 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 'node' 5 | - '4.2' 6 | - '0.12' 7 | - '0.10' 8 | 9 | before_script: 10 | 11 | # Set up a virtual screen for Firefox. 12 | - export DISPLAY=:99.0 13 | - sh -e /etc/init.d/xvfb start 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | ## HEAD (Unreleased) 5 | _(none)_ 6 | 7 | -------------------- 8 | 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | We welcome contributions from everyone! 4 | 5 | ## Getting Started 6 | 7 | Make sure you have NodeJS 0.10 or higher and npm installed. 8 | 9 | 1. Fork this repository and clone your fork 10 | 1. Install dependencies: `npm install` 11 | 1. Run a development server: `npm start` 12 | 13 | ### Making Changes 14 | 15 | Refer to the [video.js plugin standards][standards] for more detail on best practices and tooling for video.js plugin authorship. 16 | 17 | When you've made your changes, push your commit(s) to your fork and issue a pull request against the original repository. 18 | 19 | ### Running Tests 20 | 21 | Testing is a crucial part of any software project. For all but the most trivial changes (typos, etc) test cases are expected. Tests are run in actual browsers using [Karma][karma]. 22 | 23 | - In all available and supported browsers: `npm test` 24 | - In a specific browser: `npm run test:chrome`, `npm run test:firefox`, etc. 25 | - While development server is running (`npm start`), navigate to [`http://localhost:9999/test/`][local] 26 | 27 | 28 | [karma]: http://karma-runner.github.io/ 29 | [local]: http://localhost:9999/test/ 30 | [standards]: https://github.com/videojs/generator-videojs-plugin/docs/standards.md 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Emmanuel Alves <manel.pb@gmail.com> 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # videojs-playlist-thumbs 2 | 3 | Continous plays videos and display the list on a sidebar with thumbnail and title 4 | 5 | ![alt tag](https://raw.githubusercontent.com/manelpb/videojs-playlist-thumbs/master/screenshot.png) 6 | 7 | ## Installation 8 | 9 | ```sh 10 | npm install --save videojs-playlist-thumbs 11 | ``` 12 | 13 | ## Usage 14 | 15 | To include videojs-playlist on your website or web application, use any of the following methods. 16 | 17 | ### ` 23 | 24 | 25 | 26 | 27 | 46 | ``` 47 | 48 | ## Documentation 49 | 50 | ### videos 51 | 52 | You should pass an array of objects with the following structure 53 | 54 | ``` 55 | var playlist = [ 56 | { 57 | "src" : "https://www.youtube.com/watch?v=fk4BbF7B29w", 58 | "type": "video/youtube", 59 | "title": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", 60 | "thumbnail": "https://i.ytimg.com/vi/fk4BbF7B29w/hqdefault.jpg" 61 | }, 62 | { 63 | "src" : "http://vjs.zencdn.net/v/oceans.mp4", 64 | "type": "video/mp4", 65 | "title": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", 66 | "thumbnail": "https://i.ytimg.com/vi/nmcdLOjGVzw/hqdefault.jpg" 67 | }, 68 | { 69 | "src" : "https://www.youtube.com/watch?v=_gMq3hRLDD0", 70 | "type": "video/youtube", 71 | "title": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", 72 | "thumbnail": "https://i.ytimg.com/vi/_gMq3hRLDD0/hqdefault.jpg" 73 | }, 74 | { 75 | "src" : "https://www.youtube.com/watch?v=_wYtG7aQTHA", 76 | "type": "video/youtube", 77 | "title": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", 78 | "thumbnail": "https://i.ytimg.com/vi/_wYtG7aQTHA/hqdefault.jpg" 79 | } 80 | ]; 81 | ``` 82 | 83 | ### playlist options 84 | 85 | #### hideSidebar 86 | 87 | It just hides the side bar, but the playlist keeps working 88 | 89 | #### upNext 90 | 91 | Shows a legend on the first video of the list 92 | 93 | #### hideIcons 94 | 95 | Hides the buttons (next/prev) on the control bar 96 | 97 | #### thumbnailSize 98 | 99 | Size of the video thumbnail on the sidebar 100 | 101 | #### items 102 | 103 | Number of videos on the sidebar 104 | 105 | 106 | ## License 107 | 108 | MIT. Copyright (c) Emmanuel Alves / http://github.com/manelpb 109 | 110 | 111 | [videojs]: http://videojs.com/ 112 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "videojs-playlist-thumbs", 3 | "author": "Emmanuel Alves <manel.pb@gmail.com>", 4 | "license": "MIT", 5 | "main": [ 6 | "dist/videojs-playlist.css", 7 | "dist/videojs-playlist.min.js" 8 | ], 9 | "keywords": [ 10 | "videojs", 11 | "videojs-plugin" 12 | ] 13 | } 14 | 15 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | videojs-playlist Demo 6 | 7 | 8 | 9 | 10 | 16 | 17 | 18 | 19 | 21 | 22 | 26 | 27 | 28 | 29 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "videojs-playlist-thumbs", 3 | "version": "0.1.5", 4 | "description": "Continous play videos with thumbnail and looping", 5 | "main": "es5/plugin.js", 6 | "scripts": { 7 | "prebuild": "npm run clean", 8 | "build": "npm-run-all -p build:*", 9 | "build:css": "npm-run-all build:css:sass build:css:bannerize", 10 | "build:css:bannerize": "bannerize dist/videojs-playlist.css --banner=scripts/banner.ejs", 11 | "build:css:sass": "node-sass src/plugin.scss dist/videojs-playlist.css --output-style=compressed --linefeed=lf", 12 | "build:js": "npm-run-all build:js:babel build:js:browserify build:js:bannerize build:js:uglify", 13 | "build:js:babel": "babel src -d es5", 14 | "build:js:bannerize": "bannerize dist/videojs-playlist.js --banner=scripts/banner.ejs", 15 | "build:js:browserify": "browserify . -s videojs-playlist -o dist/videojs-playlist.js", 16 | "build:js:uglify": "uglifyjs dist/videojs-playlist.js --comments --mangle --compress -o dist/videojs-playlist.min.js", 17 | "build:test": "babel-node scripts/build-test.js", 18 | "change": "chg add", 19 | "clean": "rimraf dist test/dist es5 && mkdirp dist test/dist es5", 20 | "lint": "vjsstandard", 21 | "start": "babel-node scripts/server.js", 22 | "pretest": "npm-run-all lint build", 23 | "test": "karma start test/karma.conf.js", 24 | "test:chrome": "npm run pretest && karma start test/karma.conf.js --browsers Chrome", 25 | "test:firefox": "npm run pretest && karma start test/karma.conf.js --browsers Firefox", 26 | "test:ie": "npm run pretest && karma start test/karma.conf.js --browsers IE", 27 | "test:safari": "npm run pretest && karma start test/karma.conf.js --browsers Safari", 28 | "preversion": "npm test", 29 | "version": "babel-node scripts/version.js", 30 | "postversion": "babel-node scripts/postversion.js", 31 | "prepublish": "npm run build" 32 | }, 33 | "repository" : 34 | { "type" : "git" 35 | , "url" : "https://github.com/manelpb/videojs-playlist-thumbs.git" 36 | }, 37 | "keywords": [ 38 | "videojs", 39 | "videojs-plugin" 40 | ], 41 | "author": "Emmanuel Alves ", 42 | "license": "MIT", 43 | "browserify": { 44 | "transform": [ 45 | "browserify-shim", 46 | "browserify-versionify" 47 | ] 48 | }, 49 | "browserify-shim": { 50 | "qunit": "global:QUnit", 51 | "sinon": "global:sinon", 52 | "video.js": "global:videojs" 53 | }, 54 | "style": "dist/videojs-playlist.css", 55 | "videojs-plugin": { 56 | "style": "dist/videojs-playlist.css", 57 | "script": "dist/videojs-playlist.min.js" 58 | }, 59 | "vjsstandard": { 60 | "ignore": [ 61 | "dist", 62 | "docs", 63 | "es5", 64 | "test/dist", 65 | "test/karma.conf.js" 66 | ] 67 | }, 68 | "files": [ 69 | "CONTRIBUTING.md", 70 | "bower.json", 71 | "dist/", 72 | "docs/", 73 | "es5/", 74 | "index.html", 75 | "scripts/", 76 | "src/", 77 | "test/" 78 | ], 79 | "dependencies": { 80 | "video.js": "^5.6.0", 81 | "videojs-youtube": "^2.1.0" 82 | }, 83 | "devDependencies": { 84 | "babel": "^5.8.35", 85 | "babelify": "^6.4.0", 86 | "bannerize": "^1.0.2", 87 | "bluebird": "^3.2.2", 88 | "browserify": "^12.0.2", 89 | "browserify-shim": "^3.8.12", 90 | "browserify-versionify": "^1.0.6", 91 | "budo": "^8.0.4", 92 | "chg": "^0.3.2", 93 | "glob": "^6.0.3", 94 | "global": "^4.3.0", 95 | "karma": "^0.13.19", 96 | "karma-chrome-launcher": "^0.2.2", 97 | "karma-detect-browsers": "^2.0.2", 98 | "karma-firefox-launcher": "^0.1.7", 99 | "karma-ie-launcher": "^0.2.0", 100 | "karma-qunit": "^0.1.9", 101 | "karma-safari-launcher": "^0.1.1", 102 | "mkdirp": "^0.5.1", 103 | "node-sass": "^3.4.2", 104 | "npm-run-all": "^1.5.1", 105 | "qunitjs": "^1.21.0", 106 | "rimraf": "^2.5.1", 107 | "sinon": "~1.14.0", 108 | "uglify-js": "^2.6.1", 109 | "videojs-standard": "^4.0.0" 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manelpb/videojs-playlist-thumbs/c989884a0f1ee6fc1fb731f9e596ae20752da82f/screenshot.png -------------------------------------------------------------------------------- /scripts/banner.ejs: -------------------------------------------------------------------------------- 1 | /** 2 | * <%- pkg.name %> 3 | * @version <%- pkg.version %> 4 | * @copyright <%- date.getFullYear() %> <%- pkg.author %> 5 | * @license <%- pkg.license %> 6 | */ 7 | -------------------------------------------------------------------------------- /scripts/build-test.js: -------------------------------------------------------------------------------- 1 | import browserify from 'browserify'; 2 | import fs from 'fs'; 3 | import glob from 'glob'; 4 | 5 | /* eslint no-console: 0 */ 6 | 7 | glob('test/**/*.test.js', (err, files) => { 8 | if (err) { 9 | throw err; 10 | } 11 | browserify(files) 12 | .transform('babelify') 13 | .bundle() 14 | .pipe(fs.createWriteStream('test/dist/bundle.js')); 15 | }); 16 | -------------------------------------------------------------------------------- /scripts/postversion.js: -------------------------------------------------------------------------------- 1 | import {exec} from 'child_process'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | /* eslint no-console: 0 */ 6 | 7 | /** 8 | * Determines whether or not the project has the Bower setup by checking for 9 | * the presence of a bower.json file. 10 | * 11 | * @return {Boolean} 12 | */ 13 | const hasBower = () => { 14 | try { 15 | fs.statSync(path.join(__dirname, '../bower.json')); 16 | return true; 17 | } catch (x) { 18 | return false; 19 | } 20 | }; 21 | 22 | // If the project supports Bower, roll HEAD back one commit to avoid having 23 | // the tagged commit - with `dist/` - in the main history. 24 | if (hasBower()) { 25 | exec('git reset --hard HEAD~1', (err, stdout, stderr) => { 26 | if (err) { 27 | process.stdout.write(err.stack); 28 | process.exit(err.status || 1); 29 | } else { 30 | process.stdout.write(stdout); 31 | } 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /scripts/server.js: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird'; 2 | import browserify from 'browserify'; 3 | import budo from 'budo'; 4 | import fs from 'fs'; 5 | import glob from 'glob'; 6 | import mkdirp from 'mkdirp'; 7 | import sass from 'node-sass'; 8 | import path from 'path'; 9 | 10 | /* eslint no-console: 0 */ 11 | 12 | const pkg = require(path.join(__dirname, '../package.json')); 13 | 14 | // Replace "%s" tokens with the plugin name in a string. 15 | const nameify = (str) => 16 | str.replace(/%s/g, pkg.name.split('/').reverse()[0]); 17 | 18 | const srces = { 19 | css: 'src/plugin.scss', 20 | js: 'src/plugin.js', 21 | tests: glob.sync('test/**/*.test.js') 22 | }; 23 | 24 | const dests = { 25 | css: nameify('dist/%s.css'), 26 | js: nameify('dist/%s.js'), 27 | tests: 'test/dist/bundle.js' 28 | }; 29 | 30 | const bundlers = { 31 | 32 | js: browserify({ 33 | debug: true, 34 | entries: [srces.js], 35 | standalone: nameify('%s'), 36 | transform: [ 37 | 'babelify', 38 | 'browserify-shim', 39 | 'browserify-versionify' 40 | ] 41 | }), 42 | 43 | tests: browserify({ 44 | debug: true, 45 | entries: srces.tests, 46 | transform: [ 47 | 'babelify', 48 | 'browserify-shim', 49 | 'browserify-versionify' 50 | ] 51 | }) 52 | }; 53 | 54 | const bundle = (name) => { 55 | return new Promise((resolve, reject) => { 56 | bundlers[name] 57 | .bundle() 58 | .pipe(fs.createWriteStream(dests[name])) 59 | .on('finish', resolve) 60 | .on('error', reject); 61 | }); 62 | }; 63 | 64 | mkdirp.sync('dist'); 65 | 66 | // Start the server _after_ the initial bundling is done. 67 | Promise.all([bundle('js'), bundle('tests')]).then(() => { 68 | const server = budo({ 69 | port: 9999, 70 | stream: process.stdout 71 | }).on('reload', (f) => console.log('reloading %s', f || 'everything')); 72 | 73 | /** 74 | * A collection of functions which are mapped to strings that are used to 75 | * generate RegExp objects. If a filepath matches the RegExp, the function 76 | * will be used to handle that watched file. 77 | * 78 | * @type {Object} 79 | */ 80 | const handlers = { 81 | 82 | /** 83 | * Handler for Sass source. 84 | * 85 | * @param {String} event 86 | * @param {String} file 87 | */ 88 | '^src/.+\.scss$'(event, file) { 89 | console.log('re-compiling sass'); 90 | let result = sass.renderSync({file: srces.css, outputStyle: 'compressed'}); 91 | 92 | fs.writeFileSync(dests.css, result.css); 93 | server.reload(); 94 | }, 95 | 96 | /** 97 | * Handler for JavaScript source. 98 | * 99 | * @param {String} event 100 | * @param {String} file 101 | */ 102 | '^src/.+\.js$'(event, file) { 103 | console.log('re-bundling javascript and tests'); 104 | Promise.all([bundle('js'), bundle('tests')]).then(() => server.reload()); 105 | }, 106 | 107 | /** 108 | * Handler for JavaScript tests. 109 | * 110 | * @param {String} event 111 | * @param {String} file 112 | */ 113 | '^test/.+\.test\.js$'(event, file) { 114 | console.log('re-bundling tests'); 115 | bundle('tests').then(() => server.reload()); 116 | } 117 | }; 118 | 119 | /** 120 | * Finds the first handler function for the file that matches a RegExp 121 | * derived from the keys. 122 | * 123 | * @param {String} file 124 | * @return {Function|Undefined} 125 | */ 126 | const findHandler = (file) => { 127 | const keys = Object.keys(handlers); 128 | 129 | for (let i = 0; i < keys.length; i++) { 130 | let regex = new RegExp(keys[i]); 131 | 132 | if (regex.test(file)) { 133 | return handlers[keys[i]]; 134 | } 135 | } 136 | }; 137 | 138 | server 139 | .live() 140 | .watch([ 141 | 'index.html', 142 | 'src/**/*.{scss,js}', 143 | 'test/**/*.test.js', 144 | 'test/index.html' 145 | ]) 146 | .on('watch', (event, file) => { 147 | const handler = findHandler(file); 148 | 149 | console.log(`detected a "${event}" event in "${file}"`); 150 | 151 | if (handler) { 152 | handler(event, file); 153 | } else { 154 | server.reload(); 155 | } 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /scripts/version.js: -------------------------------------------------------------------------------- 1 | import {exec} from 'child_process'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | /* eslint no-console: 0 */ 6 | 7 | const pkg = require(path.join(__dirname, '../package.json')); 8 | 9 | /** 10 | * Determines whether or not the project has the CHANGELOG setup by checking 11 | * for the presence of a CHANGELOG.md file and the necessary dependency and 12 | * npm script. 13 | * 14 | * @return {Boolean} 15 | */ 16 | const hasChangelog = () => { 17 | try { 18 | fs.statSync(path.join(__dirname, '../CHANGELOG.md')); 19 | } catch (x) { 20 | return false; 21 | } 22 | return pkg.devDependencies.hasOwnProperty('chg') && 23 | pkg.scripts.hasOwnProperty('change'); 24 | }; 25 | 26 | /** 27 | * Determines whether or not the project has the Bower setup by checking for 28 | * the presence of a bower.json file. 29 | * 30 | * @return {Boolean} 31 | */ 32 | const hasBower = () => { 33 | try { 34 | fs.statSync(path.join(__dirname, '../bower.json')); 35 | return true; 36 | } catch (x) { 37 | return false; 38 | } 39 | }; 40 | 41 | const commands = []; 42 | 43 | // If the project has a CHANGELOG, update it for the new release. 44 | if (hasChangelog()) { 45 | commands.push(`chg release "${pkg.version}"`); 46 | commands.push('git add CHANGELOG.md'); 47 | } 48 | 49 | // If the project supports Bower, perform special extra versioning step. 50 | if (hasBower()) { 51 | commands.push('git add package.json'); 52 | commands.push(`git commit -m "${pkg.version}"`); 53 | 54 | // We only need a build in the Bower-supported case because of the 55 | // temporary addition of the dist/ directory. 56 | commands.push('npm run build'); 57 | commands.push('git add -f dist'); 58 | } 59 | 60 | if (commands.length) { 61 | exec(commands.join(' && '), (err, stdout, stderr) => { 62 | if (err) { 63 | process.stdout.write(err.stack); 64 | process.exit(err.status || 1); 65 | } else { 66 | process.stdout.write(stdout); 67 | } 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /src/plugin.js: -------------------------------------------------------------------------------- 1 | import videojs from 'video.js'; 2 | 3 | // Default options for the plugin. 4 | let defaults = { 5 | thumbnailSize : 190, 6 | playlistItems: 3, 7 | hideIcons: false, 8 | upNext : true, 9 | hideSidebar : false 10 | }; 11 | 12 | let player; 13 | let currentIdx = []; 14 | let videos = []; 15 | let playlistsElemen = null; 16 | let players = []; 17 | let playlistsElemens = []; 18 | 19 | /** 20 | * creates each video on the playlist 21 | */ 22 | const createVideoElement = (player_id, idx, title, thumbnail) => { 23 | let videoElement = document.createElement("li"); 24 | let videoTitle = document.createElement("div"); 25 | videoTitle.className = "vjs-playlist-video-title"; 26 | 27 | if(idx == 0) { 28 | if(defaults.upNext) { 29 | let upNext = document.createElement("div"); 30 | upNext.className = "vjs-playlist-video-upnext"; 31 | upNext.innerText = "UP Next"; 32 | 33 | videoTitle.appendChild(upNext); 34 | } 35 | } 36 | 37 | if(title) { 38 | let videoTitleText = document.createElement("div"); 39 | videoTitleText.innerText = title; 40 | 41 | videoTitle.appendChild(videoTitleText);// = "" + title + ""; 42 | 43 | videoElement.appendChild(videoTitle); 44 | } 45 | 46 | videoElement.setAttribute("style", "background-image: url('"+ thumbnail +"');"); 47 | videoElement.setAttribute("data-index", idx); 48 | 49 | // when the user clicks on the playlist, the video will start playing 50 | videoElement.onclick = function(ev) { 51 | var idx = parseInt(ev.target.getAttribute("data-index")); 52 | 53 | // updates the list and everything before this index should be moved to the end 54 | let videosBefore = videos[player_id].splice(0, idx); 55 | 56 | videosBefore.map(function(video) { 57 | // adds to the end of the array 58 | videos[player_id].push(video); 59 | }); 60 | 61 | // and play this video 62 | updatePlaylistAndPlay(player_id, true); 63 | }; 64 | 65 | return videoElement; 66 | }; 67 | 68 | /** 69 | * Function to invoke when the player is ready. 70 | * 71 | * This is a great place for your plugin to initialize itself. When this 72 | * function is called, the player will have its DOM and child components 73 | * in place. 74 | * 75 | * @function onPlayerReady 76 | * @param {Player} player 77 | * @param {Object} [options={}] 78 | */ 79 | const onPlayerReady = (player, options) => { 80 | videos[player.id_] = options.videos; 81 | currentIdx[player.id_] = 0; 82 | 83 | if(options.playlist && options.playlist.thumbnailSize) { 84 | defaults.thumbnailSize = options.playlist.thumbnailSize.toString().replace("px", ""); 85 | } 86 | 87 | if(options.playlist && options.playlist.items) { 88 | defaults.playlistItems = options.playlist.items; 89 | } 90 | 91 | if(options.playlist && options.playlist.hideIcons) { 92 | defaults.hideIcons = options.playlist.hideIcons; 93 | } 94 | 95 | if(options.playlist && options.playlist.hideSidebar) { 96 | defaults.hideSidebar = options.playlist.hideSidebar; 97 | } 98 | 99 | createElements(player, options); 100 | updateElementWidth(player); 101 | }; 102 | 103 | const updatePlaylistAndPlay = (player_id, autoplay) => { 104 | 105 | if (!player_id) { 106 | player_id = player.id_; 107 | } 108 | 109 | // plays the first video on the playlist 110 | playVideo(player_id, 0, autoplay); 111 | 112 | // and move this video to the end of the playlist 113 | let first = videos[player_id].splice(0, 1); 114 | 115 | // then add at the end of the array 116 | videos[player_id].push(first[0]); 117 | 118 | // clean the playlist 119 | while (playlistsElemens[player_id].firstChild) { 120 | playlistsElemens[player_id].removeChild(playlistsElemens[player_id].firstChild); 121 | } 122 | 123 | // add each video on the playlist 124 | videos.map(function(video, idx) { 125 | playlistsElemens[player_id].appendChild(createVideoElement(player_id, idx, video.title, video.thumbnail)); 126 | }); 127 | }; 128 | 129 | /** 130 | * Creates the root html elements for the playlist 131 | */ 132 | const createElements = (player, options) => { 133 | // creates the playlist items and add on the video player 134 | playlistsElemen = document.createElement("ul"); 135 | playlistsElemen.className = "vjs-playlist-items"; 136 | 137 | if(!defaults.hideSidebar) { 138 | player.el().appendChild(playlistsElemen); 139 | } 140 | 141 | // plays the first video 142 | if(videos.length > 0) { 143 | updatePlaylistAndPlay(false); 144 | } 145 | 146 | // create next and previous button 147 | if(!defaults.hideIcons) { 148 | let prevBtn = document.createElement("button"); 149 | prevBtn.className = "vjs-button-prev"; 150 | prevBtn.onclick = onPrevClick; 151 | 152 | player.controlBar.el().insertBefore(prevBtn, player.controlBar.playToggle.el()); 153 | 154 | let nextBtn = document.createElement("button"); 155 | nextBtn.className = "vjs-button-next"; 156 | nextBtn.onclick = onNextClick; 157 | 158 | player.controlBar.el().insertBefore(nextBtn, player.controlBar.volumeMenuButton.el()); 159 | } 160 | 161 | // creates the loading next on video ends 162 | player.on("ended", createPlayingNext); 163 | 164 | // adds the main class on the player 165 | player.addClass('vjs-playlist'); 166 | }; 167 | 168 | const createPlayingNext = () => { 169 | nextVideo(); 170 | }; 171 | 172 | const onNextClick = (ev) => { 173 | var player_id = ev.target.parentNode.parentNode.id; 174 | nextVideo(player_id); 175 | }; 176 | 177 | const onPrevClick = (ev) => { 178 | var player_id = ev.target.parentNode.parentNode.id; 179 | previousVideo(player_id); 180 | }; 181 | 182 | /** 183 | * updates the main video player width 184 | */ 185 | const updateElementWidth = (player) => { 186 | let resize = function(p) { 187 | let itemWidth = defaults.thumbnailSize; 188 | 189 | let playerWidth = p.el().offsetWidth; 190 | let playerHeight = p.el().offsetHeight; 191 | let itemHeight = Math.round(playerHeight / defaults.playlistItems); 192 | 193 | let youtube = p.$(".vjs-tech"); 194 | let newSize = playerWidth - itemWidth; 195 | 196 | let playerId = p.el().id; 197 | 198 | if(newSize >= 0) { 199 | let style = document.createElement('style'); 200 | let def = ' #' + playerId + '.vjs-playlist .vjs-poster { width: ' + newSize + 'px !important; }' + 201 | ' #' + playerId + '.vjs-playlist .vjs-playlist-items { width: ' + itemWidth + 'px !important; }' + 202 | ' #' + playerId + '.vjs-playlist .vjs-playlist-items li { width: ' + itemWidth + 'px !important; height: ' + itemHeight + 'px !important; }' + 203 | ' #' + playerId + '.vjs-playlist .vjs-modal-dialog { width: ' + newSize + 'px !important; } ' + 204 | ' #' + playerId + '.vjs-playlist .vjs-control-bar, #' + playerId + '.vjs-playlist .vjs-tech { width: ' + newSize + 'px !important; } ' + 205 | ' #' + playerId + '.vjs-playlist .vjs-big-play-button, #' + playerId + '.vjs-playlist .vjs-loading-spinner { left: ' + Math.round(newSize / 2) + 'px !important; } ' + 206 | ' #' + playerId + ' .vimeoFrame { width: ' + newSize + 'px !important; } ' + 207 | ' #' + playerId + ' .vimeoFrame.vimeoHidden { padding-bottom: 0 !important; } '; 208 | 209 | style.setAttribute('type', 'text/css'); 210 | document.getElementsByTagName('head')[0].appendChild(style); 211 | 212 | if(style.styleSheet) { 213 | style.styleSheet.cssText = def; 214 | } else { 215 | style.appendChild(document.createTextNode(def)); 216 | } 217 | } 218 | }; 219 | 220 | if(!defaults.hideSidebar) { 221 | window.onresize = function() { 222 | resize(player); 223 | }; 224 | 225 | if(player) { 226 | resize(player); 227 | } 228 | } 229 | }; 230 | 231 | /** 232 | * plays the video based on an index 233 | */ 234 | const playVideo = (player_id, idx, autoPlay) => { 235 | if (!player_id) { 236 | player_id = player.id_; 237 | } 238 | players[player_id].pause(); 239 | players[player_id].error(null); 240 | let video = { type: videos[player_id][idx].type, src: videos[player_id][idx].src}; 241 | 242 | let curVideoId = 'vimeo_wrapper_' + player_id; 243 | let vimeos = players[player_id].el().getElementsByClassName('vimeoFrame'); 244 | for (let i = 0; i < vimeos.length; i++) 245 | { 246 | vimeos[i].classList.add('vimeoHidden'); 247 | } 248 | if (video.type == 'video/vimeo') 249 | { 250 | document.getElementById(curVideoId).classList.remove('vimeoHidden'); 251 | } 252 | 253 | players[player_id].src(video); 254 | players[player_id].poster(videos[player_id][idx].thumbnail); 255 | 256 | if(autoPlay || players[player_id].options_.autoplay) { 257 | try { 258 | players[player_id].play(); 259 | } catch(e) { 260 | } 261 | } 262 | }; 263 | 264 | /** 265 | * plays the next video, if it comes to the end, loop 266 | */ 267 | const nextVideo = (player_id) => { 268 | if (!player_id) { 269 | player_id = player.id_; 270 | } 271 | if(currentIdx[player_id] < videos[player_id].length) { 272 | currentIdx[player_id]++; 273 | } else { 274 | currentIdx[player_id] = 0; 275 | } 276 | 277 | updatePlaylistAndPlay(true); 278 | }; 279 | 280 | /** 281 | * plays the previous video, if it comes to the first video, loop 282 | */ 283 | const previousVideo = (player_id) => { 284 | if (!player_id) { 285 | player_id = player.id_; 286 | } 287 | if(currentIdx[player_id] > 0) { 288 | currentIdx[player_id]--; 289 | } else { 290 | currentIdx[player_id] = videos.length - 1; 291 | } 292 | playVideo(player_id, currentIdx[player_id], true); 293 | }; 294 | 295 | /** 296 | * A video.js plugin. 297 | * 298 | * In the plugin function, the value of `this` is a video.js `Player` 299 | * instance. You cannot rely on the player being in a "ready" state here, 300 | * depending on how the plugin is invoked. This may or may not be important 301 | * to you; if not, remove the wait for "ready"! 302 | * 303 | * @function playlist 304 | * @param {Object} [options={}] 305 | * An object of options left to the plugin author to define. 306 | */ 307 | const playlist = function(options) { 308 | this.ready(() => { 309 | player = this; 310 | players[player.id_] = player; 311 | onPlayerReady(this, videojs.mergeOptions(defaults, options)); 312 | }); 313 | }; 314 | 315 | // Register the plugin with video.js. 316 | videojs.plugin('playlist', playlist); 317 | 318 | // Include the version number. 319 | playlist.VERSION = '__VERSION__'; 320 | 321 | export default playlist; 322 | -------------------------------------------------------------------------------- /src/plugin.scss: -------------------------------------------------------------------------------- 1 | // Sass for videojs-playlist 2 | 3 | .video-js { 4 | 5 | position: relative; 6 | 7 | .vjs-big-play-button { 8 | top: 50%; 9 | left: 50%; 10 | margin-left: -50px; 11 | margin-top: -20px; 12 | } 13 | 14 | // This class is added to the video.js element by the plugin by default. 15 | &.vjs-playlist { 16 | display: block; 17 | } 18 | 19 | .vjs-button-prev, .vjs-button-next { 20 | cursor: pointer; 21 | font-size: 15px; 22 | margin-bottom: 2px; 23 | font-weight: bold; 24 | margin-left: 10px; 25 | margin-right: 10px; 26 | } 27 | 28 | .vjs-button-prev:before { 29 | content: "<<"; 30 | } 31 | 32 | .vjs-button-next:before { 33 | content: ">>"; 34 | } 35 | 36 | .vjs-playlist-items { 37 | border: 1px solid #000; 38 | height: 100%; 39 | position: absolute; 40 | right: 0; 41 | overflow: scroll; 42 | margin: 0; 43 | padding: 0; 44 | 45 | li { 46 | background-size: cover; 47 | height: 120px; 48 | position: relative; 49 | } 50 | 51 | li .vjs-playlist-video-title { 52 | position: absolute; 53 | bottom: 10px; 54 | text-shadow: 1px 2px 3px #000; 55 | left: 10px; 56 | text-transform: uppercase; 57 | } 58 | 59 | li .vjs-playlist-video-title div { 60 | margin-top: 8px 61 | } 62 | 63 | li div.vjs-playlist-video-upnext { 64 | font-size: 12px; 65 | color: red; 66 | } 67 | 68 | li:hover { 69 | cursor: pointer; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | videojs-playlist Unit Tests 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | var browsers = config.browsers; 3 | var frameworks = ['qunit']; 4 | var plugins = ['karma-qunit']; 5 | 6 | var addBrowserLauncher = function(browser) { 7 | plugins.push('karma-' + browser.toLowerCase() + '-launcher'); 8 | }; 9 | 10 | // On Travis CI, we can only run in Firefox. 11 | if (process.env.TRAVIS) { 12 | browsers = ['Firefox']; 13 | browsers.forEach(addBrowserLauncher); 14 | 15 | // If specific browsers are requested on the command line, load their 16 | // launchers. 17 | } else if (browsers.length) { 18 | browsers.forEach(addBrowserLauncher); 19 | 20 | // If no browsers are specified, we will do a `karma-detect-browsers` run, 21 | // which means we need to set up that plugin and all the browser plugins 22 | // we are supporting. 23 | } else { 24 | frameworks.push('detectBrowsers'); 25 | plugins.push('karma-detect-browsers'); 26 | ['chrome', 'firefox', 'ie', 'safari'].forEach(addBrowserLauncher); 27 | } 28 | 29 | config.set({ 30 | basePath: '..', 31 | frameworks: frameworks, 32 | 33 | files: [ 34 | 'dist/videojs-playlist.css', 35 | 'node_modules/sinon/pkg/sinon.js', 36 | 'node_modules/sinon/pkg/sinon-ie.js', 37 | 'node_modules/video.js/dist/video.js', 38 | 'node_modules/video.js/dist/video-js.css', 39 | 'test/dist/bundle.js' 40 | ], 41 | 42 | browsers: browsers, 43 | plugins: plugins, 44 | 45 | detectBrowsers: { 46 | usePhantomJS: false 47 | }, 48 | 49 | reporters: ['dots'], 50 | port: 9876, 51 | colors: true, 52 | autoWatch: false, 53 | singleRun: true, 54 | concurrency: Infinity 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /test/plugin.test.js: -------------------------------------------------------------------------------- 1 | import document from 'global/document'; 2 | 3 | import QUnit from 'qunit'; 4 | import sinon from 'sinon'; 5 | import videojs from 'video.js'; 6 | 7 | import plugin from '../src/plugin'; 8 | 9 | const Player = videojs.getComponent('Player'); 10 | 11 | QUnit.test('the environment is sane', function(assert) { 12 | assert.strictEqual(typeof Array.isArray, 'function', 'es5 exists'); 13 | assert.strictEqual(typeof sinon, 'object', 'sinon exists'); 14 | assert.strictEqual(typeof videojs, 'function', 'videojs exists'); 15 | assert.strictEqual(typeof plugin, 'function', 'plugin is a function'); 16 | }); 17 | 18 | QUnit.module('videojs-playlist', { 19 | 20 | beforeEach() { 21 | 22 | // Mock the environment's timers because certain things - particularly 23 | // player readiness - are asynchronous in video.js 5. This MUST come 24 | // before any player is created; otherwise, timers could get created 25 | // with the actual timer methods! 26 | this.clock = sinon.useFakeTimers(); 27 | 28 | this.fixture = document.getElementById('qunit-fixture'); 29 | this.video = document.createElement('video'); 30 | this.fixture.appendChild(this.video); 31 | this.player = videojs(this.video); 32 | }, 33 | 34 | afterEach() { 35 | this.player.dispose(); 36 | this.clock.restore(); 37 | } 38 | }); 39 | 40 | QUnit.test('registers itself with video.js', function(assert) { 41 | assert.expect(2); 42 | 43 | assert.strictEqual( 44 | Player.prototype.playlist, 45 | plugin, 46 | 'videojs-playlist plugin was registered' 47 | ); 48 | 49 | this.player.playlist({ videos: [], playlist : {} }); 50 | 51 | // Tick the clock forward enough to trigger the player to be "ready". 52 | this.clock.tick(1); 53 | 54 | assert.ok( 55 | this.player.hasClass('vjs-playlist'), 56 | 'the plugin adds a class to the player' 57 | ); 58 | }); 59 | --------------------------------------------------------------------------------