├── .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 | 
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 |
--------------------------------------------------------------------------------