commit f0a97f8e136dc4ad8cb8ac16d0aa32a0c42fc39c Author: Chris Boustead Date: Fri Nov 10 12:15:08 2017 -0500 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fc6f5de --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# http://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..efc5fcf --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# OS +Thumbs.db +ehthumbs.db +Desktop.ini +.DS_Store +._* + +# Editors +*~ +*.swp +*.tmproj +*.tmproject +*.sublime-* +.idea/ +.project/ +.settings/ +.vscode/ + +# Logs +logs +*.log +npm-debug.log* + +# Dependency directories +bower_components/ +node_modules/ + +# Yeoman meta-data +.yo-rc.json + +# Build-related directories +dist/ +docs/api/ +test/dist/ diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..ea8c75d --- /dev/null +++ b/.npmignore @@ -0,0 +1,3 @@ +# Intentionally left blank, so that npm does not ignore anything by default, +# but relies on the package.json "files" array to explicitly define what ends +# up in the package. diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4deb53e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,32 @@ +sudo: false +dist: trusty +language: node_js +node_js: + - 'node' + - 'lts/argon' +before_script: + + # Check if the current version is equal to the major version for the env. + - 'export IS_INSTALLED="$(npm list video.js | grep "video.js@$VJS")"' + + # We have to add semicolons to the end of each line in the if as Travis runs + # this all on one line. + - 'if [ -z "$IS_INSTALLED" ]; then + echo "INSTALLING video.js@>=$VJS.0.0-RC.0 <$(($VJS+1)).0.0"; + npm i "video.js@>=$VJS.0.0-RC.0 <\$(($VJS+1)).0.0"; + else + echo "video.js@$VJS ALREADY INSTALLED"; + fi' + - export CHROME_BIN=/usr/bin/google-chrome + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start +env: + - VJS=5 + - VJS=6 +addons: + firefox: latest + apt: + sources: + - google-chrome + packages: + - google-chrome-stable diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a184176 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,30 @@ +# CONTRIBUTING + +We welcome contributions from everyone! + +## Getting Started + +Make sure you have NodeJS 0.10 or higher and npm installed. + +1. Fork this repository and clone your fork +1. Install dependencies: `npm install` +1. Run a development server: `npm start` + +### Making Changes + +Refer to the [video.js plugin conventions][conventions] for more detail on best practices and tooling for video.js plugin authorship. + +When you've made your changes, push your commit(s) to your fork and issue a pull request against the original repository. + +### Running Tests + +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]. + +- In all available and supported browsers: `npm test` +- In a specific browser: `npm run test:chrome`, `npm run test:firefox`, etc. +- While development server is running (`npm start`), navigate to [`http://localhost:9999/test/`][local] + + +[karma]: http://karma-runner.github.io/ +[local]: http://localhost:9999/test/ +[conventions]: https://github.com/videojs/generator-videojs-plugin/blob/master/docs/conventions.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bb44497 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) Chris Boustead + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c737c2d --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# videojs-vtt-thumbnails + +Video.js plugin that displays thumbnails on progress bar hover, driven by external VTT files. Based on the spec at: https://support.jwplayer.com/customer/portal/articles/1407439-adding-preview-thumbnails + +Note: Plugin hides the default skin's mouse display timestamp on hover. + +## Table of Contents + + + +## Installation + +- [Installation](#installation) +- [Usage](#usage) + - [` + + +``` + +### Browserify/CommonJS + +When using with Browserify, install videojs-vtt-thumbnails via npm and `require` the plugin as you would any other module. + +```js +var videojs = require('video.js'); + +// The actual plugin function is exported by this module, but it is also +// attached to the `Player.prototype`; so, there is no need to assign it +// to a variable. +require('videojs-vtt-thumbnails'); + +var player = videojs('my-video'); + +player.vttThumbnails(); +``` + +### RequireJS/AMD + +When using with RequireJS (or another AMD library), get the script in whatever way you prefer and `require` the plugin as you normally would: + +```js +require(['video.js', 'videojs-vtt-thumbnails'], function(videojs) { + var player = videojs('my-video'); + + player.vttThumbnails(); +}); +``` + +## License + +MIT. Copyright (c) Chris Boustead <chris@forgemotion.com> + + +[videojs]: http://videojs.com/ diff --git a/example/thumbs.jpg b/example/thumbs.jpg new file mode 100644 index 0000000..a346e55 Binary files /dev/null and b/example/thumbs.jpg differ diff --git a/example/thumbs.vtt b/example/thumbs.vtt new file mode 100644 index 0000000..5e13a7d --- /dev/null +++ b/example/thumbs.vtt @@ -0,0 +1,13 @@ +WEBVTT + +00:00.000 --> 00:05.000 +thumbs.jpg#xywh=0,0,160,70 + +00:05.000 --> 00:10.000 +thumbs.jpg#xywh=160,0,160,70 + +00:10.000 --> 00:15.000 +thumbs.jpg#xywh=0,70,160,70 + +00:15.000 --> 00:20.000 +thumbs.jpg#xywh=160,70,160,70 \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..bd1421c --- /dev/null +++ b/index.html @@ -0,0 +1,30 @@ + + + + + videojs-vtt-thumbnails Demo + + + + + + + + + + + + diff --git a/jsdoc.json b/jsdoc.json new file mode 100644 index 0000000..df0756c --- /dev/null +++ b/jsdoc.json @@ -0,0 +1,3 @@ +{ + "plugins": ["plugins/markdown"] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7854dac --- /dev/null +++ b/package.json @@ -0,0 +1,107 @@ +{ + "name": "videojs-vtt-thumbnails", + "version": "0.0.0", + "description": "Display thumnails on progress bar hover, driven by external VTT files.", + "main": "dist/videojs-vtt-thumbnails.cjs.js", + "module": "dist/videojs-vtt-thumbnails.es.js", + "generator-videojs-plugin": { + "version": "5.0.3" + }, + "scripts": { + "prebuild": "npm run clean", + "build": "npm-run-all -p build:*", + "build:css": "npm-run-all build:css:sass build:css:bannerize", + "build:css:bannerize": "bannerize dist/videojs-vtt-thumbnails.css --banner=scripts/banner.ejs", + "build:css:sass": "node-sass src/plugin.scss dist/videojs-vtt-thumbnails.css --output-style=compressed --linefeed=lf", + "build:js": "npm-run-all build:js:rollup-modules build:js:rollup-umd build:js:bannerize build:js:uglify", + "build:js:bannerize": "bannerize dist/videojs-vtt-thumbnails.js --banner=scripts/banner.ejs", + "build:js:rollup-modules": "rollup -c scripts/modules.rollup.config.js", + "build:js:rollup-umd": "rollup -c scripts/umd.rollup.config.js", + "build:js:uglify": "uglifyjs dist/videojs-vtt-thumbnails.js --comments --mangle --compress -o dist/videojs-vtt-thumbnails.min.js", + "build:test": "rollup -c scripts/test.rollup.config.js", + "clean": "rimraf dist test/dist", + "postclean": "mkdirp dist test/dist", + "docs": "npm-run-all docs:*", + "docs:api": "jsdoc src -r -c jsdoc.json -d docs/api", + "docs:toc": "doctoc README.md", + "lint": "vjsstandard", + "start": "npm-run-all -p start:server watch", + "start:server": "static -a 0.0.0.0 -p 9999 -H '{\"Cache-Control\": \"no-cache, must-revalidate\"}' .", + "pretest": "npm-run-all lint build", + "test": "karma start test/karma.conf.js", + "preversion": "npm test", + "version": "node scripts/version.js", + "watch": "npm-run-all -p watch:*", + "watch:css": "npm-run-all build:css:sass watch:css:sass", + "watch:css:sass": "node-sass src/plugin.scss dist/videojs-vtt-thumbnails.css --output-style=compressed --linefeed=lf --watch src/**/*.scss", + "watch:js-modules": "rollup -c scripts/modules.rollup.config.js -w", + "watch:js-umd": "rollup -c scripts/umd.rollup.config.js -w", + "watch:test": "rollup -c scripts/test.rollup.config.js -w", + "prepublish": "npm run build", + "prepush": "npm run lint", + "precommit": "npm run docs:toc && git add README.md" + }, + "keywords": [ + "videojs", + "videojs-plugin" + ], + "author": "Chris Boustead ", + "license": "MIT", + "vjsstandard": { + "ignore": [ + "dist", + "docs", + "test/dist", + "test/karma.conf.js" + ] + }, + "files": [ + "CONTRIBUTING.md", + "dist/", + "docs/", + "index.html", + "scripts/", + "src/", + "test/" + ], + "dependencies": { + "global": "^4.3.2", + "request": "^2.83.0", + "video.js": "^5.19.2" + }, + "devDependencies": { + "babel-plugin-external-helpers": "^6.22.0", + "babel-plugin-transform-object-assign": "^6.8.0", + "babel-preset-es2015": "^6.14.0", + "bannerize": "^1.0.2", + "conventional-changelog-cli": "^1.3.1", + "conventional-changelog-videojs": "^3.0.0", + "doctoc": "^1.3.0", + "husky": "^0.13.3", + "jsdoc": "^3.4.3", + "karma": "^1.7.0", + "karma-chrome-launcher": "^2.1.1", + "karma-detect-browsers": "^2.2.5", + "karma-firefox-launcher": "^1.0.1", + "karma-ie-launcher": "^1.0.0", + "karma-qunit": "^1.2.1", + "karma-safari-launcher": "^1.0.0", + "mkdirp": "^0.5.1", + "node-sass": "4.5.3", + "node-static": "^0.7.9", + "npm-run-all": "^4.0.2", + "qunitjs": "^2.3.2", + "rimraf": "^2.6.1", + "rollup": "^0.50.0", + "rollup-plugin-babel": "^2.7.1", + "rollup-plugin-commonjs": "^8.0.2", + "rollup-plugin-json": "^2.1.1", + "rollup-plugin-multi-entry": "^2.0.1", + "rollup-plugin-node-resolve": "^3.0.0", + "rollup-watch": "^3.2.2", + "semver": "^5.3.0", + "sinon": "^2.2.0", + "uglify-js": "^3.0.7", + "videojs-standard": "^6.0.0" + } +} diff --git a/scripts/banner.ejs b/scripts/banner.ejs new file mode 100644 index 0000000..4966491 --- /dev/null +++ b/scripts/banner.ejs @@ -0,0 +1,6 @@ +/** + * <%- pkg.name %> + * @version <%- pkg.version %> + * @copyright <%- date.getFullYear() %> <%- pkg.author %> + * @license <%- pkg.license %> + */ diff --git a/scripts/modules.rollup.config.js b/scripts/modules.rollup.config.js new file mode 100644 index 0000000..15895c4 --- /dev/null +++ b/scripts/modules.rollup.config.js @@ -0,0 +1,47 @@ +/** + * Rollup configuration for packaging the plugin in a module that is consumable + * by either CommonJS (e.g. Node or Browserify) or ECMAScript (e.g. Rollup). + * + * These modules DO NOT include their dependencies as we expect those to be + * handled by the module system. + */ +import babel from 'rollup-plugin-babel'; +import json from 'rollup-plugin-json'; + +export default { + name: 'videojsVttThumbnails', + input: 'src/plugin.js', + output: [{ + file: 'dist/videojs-vtt-thumbnails.cjs.js', + format: 'cjs' + }, { + file: 'dist/videojs-vtt-thumbnails.es.js', + format: 'es' + }], + external: [ + 'global', + 'global/document', + 'global/window', + 'video.js' + ], + globals: { + 'video.js': 'videojs' + }, + plugins: [ + json(), + babel({ + babelrc: false, + exclude: 'node_modules/**', + presets: [ + ['es2015', { + loose: true, + modules: false + }] + ], + plugins: [ + 'external-helpers', + 'transform-object-assign' + ] + }) + ] +}; diff --git a/scripts/test.rollup.config.js b/scripts/test.rollup.config.js new file mode 100644 index 0000000..ed63b27 --- /dev/null +++ b/scripts/test.rollup.config.js @@ -0,0 +1,59 @@ +/** + * Rollup configuration for packaging the plugin in a test bundle. + * + * This includes all dependencies for both the plugin and its tests. + */ +import babel from 'rollup-plugin-babel'; +import commonjs from 'rollup-plugin-commonjs'; +import json from 'rollup-plugin-json'; +import multiEntry from 'rollup-plugin-multi-entry'; +import resolve from 'rollup-plugin-node-resolve'; + +export default { + name: 'videojsVttThumbnailsTests', + input: 'test/**/*.test.js', + output: { + file: 'test/dist/bundle.js', + format: 'iife' + }, + external: [ + 'qunit', + 'qunitjs', + 'sinon', + 'video.js' + ], + globals: { + 'qunit': 'QUnit', + 'qunitjs': 'QUnit', + 'sinon': 'sinon', + 'video.js': 'videojs' + }, + plugins: [ + multiEntry({ + exports: false + }), + resolve({ + browser: true, + main: true, + jsnext: true + }), + json(), + commonjs({ + sourceMap: false + }), + babel({ + babelrc: false, + exclude: 'node_modules/**', + presets: [ + ['es2015', { + loose: true, + modules: false + }] + ], + plugins: [ + 'external-helpers', + 'transform-object-assign' + ] + }) + ] +}; diff --git a/scripts/umd.rollup.config.js b/scripts/umd.rollup.config.js new file mode 100644 index 0000000..58f9648 --- /dev/null +++ b/scripts/umd.rollup.config.js @@ -0,0 +1,50 @@ +/** + * Rollup configuration for packaging the plugin in a module that is consumable + * as the `src` of a `script` tag or via AMD or similar client-side loading. + * + * This module DOES include its dependencies. + */ +import babel from 'rollup-plugin-babel'; +import commonjs from 'rollup-plugin-commonjs'; +import json from 'rollup-plugin-json'; +import resolve from 'rollup-plugin-node-resolve'; + +export default { + name: 'videojsVttThumbnails', + input: 'src/plugin.js', + output: { + file: 'dist/videojs-vtt-thumbnails.js', + format: 'umd' + }, + external: [ + 'video.js' + ], + globals: { + 'video.js': 'videojs' + }, + plugins: [ + resolve({ + browser: true, + main: true, + jsnext: true + }), + json(), + commonjs({ + sourceMap: false + }), + babel({ + babelrc: false, + exclude: 'node_modules/**', + presets: [ + ['es2015', { + loose: true, + modules: false + }] + ], + plugins: [ + 'external-helpers', + 'transform-object-assign' + ] + }) + ] +}; diff --git a/scripts/version.js b/scripts/version.js new file mode 100644 index 0000000..d2e93ec --- /dev/null +++ b/scripts/version.js @@ -0,0 +1,10 @@ +const execSync = require('child_process').execSync; +const path = require('path'); +const semver = require('semver'); +const pkg = require('../package.json'); + +if (!semver.prerelease(pkg.version)) { + process.chdir(path.resolve(__dirname, '..')); + execSync('conventional-changelog -p videojs -i CHANGELOG.md -s'); + execSync('git add CHANGELOG.md'); +} diff --git a/src/plugin.js b/src/plugin.js new file mode 100644 index 0000000..39085f6 --- /dev/null +++ b/src/plugin.js @@ -0,0 +1,346 @@ +import videojs from 'video.js' +import { version as VERSION } from '../package.json' +// import request from 'request'; + +// Default options for the plugin. +const defaults = {} + +// Cross-compatibility for Video.js 5 and 6. +const registerPlugin = videojs.registerPlugin || videojs.plugin +// const dom = videojs.dom || videojs; + +/** + * Function to invoke when the player is ready. + * + * This is a great place for your plugin to initialize itself. When this + * function is called, the player will have its DOM and child components + * in place. + * + * @function onPlayerReady + * @param {Player} player + * A Video.js player object. + * + * @param {Object} [options={}] + * A plain object containing options for the plugin. + */ +const onPlayerReady = (player, options) => { + player.addClass('vjs-vtt-thumbnails') + new vttThumbnailsPlugin(player, options) +} + +/** + * A video.js plugin. + * + * In the plugin function, the value of `this` is a video.js `Player` + * instance. You cannot rely on the player being in a "ready" state here, + * depending on how the plugin is invoked. This may or may not be important + * to you; if not, remove the wait for "ready"! + * + * @function vttThumbnails + * @param {Object} [options={}] + * An object of options left to the plugin author to define. + */ +const vttThumbnails = function (options) { + this.ready(() => { + onPlayerReady(this, videojs.mergeOptions(defaults, options)) + }) +} + +/** + * VTT Thumbnails class. + * + * This class performs all functions related to displaying the vtt + * thumbnails. + */ +class vttThumbnailsPlugin { + + /** + * Plugin class constructor, called by videojs on + * ready event. + * + * @function constructor + * @param {Player} player + * A Video.js player object. + * + * @param {Object} [options={}] + * A plain object containing options for the plugin. + */ + constructor (player, options) { + this.player = player + this.options = options + this.initializeThumbnails() + } + + /** + * Bootstrap the plugin. + */ + initializeThumbnails () { + if (!this.options.src) { + return + } + const baseUrl = this.getBaseUrl() + const url = this.getFullyQualifiedUrl(this.options.src, baseUrl) + this.getVttFile(url) + .then((data) => { + this.vttData = this.processVtt(data) + this.setupThumbnailElement() + }) + } + + /** + * Builds a base URL should we require one. + * + * @returns {string} + */ + getBaseUrl () { + return [ + window.location.protocol, + '//', + window.location.hostname, + (window.location.port ? ':' + window.location.port : ''), + window.location.pathname + ].join('').split(/([^\/]*)$/gi).shift() + } + + /** + * Grabs the contents of the VTT file. + * + * @param url + * @returns {Promise} + */ + getVttFile (url) { + return new Promise((resolve, reject) => { + const req = new XMLHttpRequest() + req.data = { + resolve: resolve + } + req.addEventListener('load', this.vttFileLoaded) + req.open('GET', url) + req.send() + }) + } + + /** + * Callback for loaded VTT file. + */ + vttFileLoaded () { + this.data.resolve(this.responseText) + } + + setupThumbnailElement (data) { + const mouseDisplay = this.player.$('.vjs-mouse-display') + this.progressBar = this.player.$('.vjs-progress-control') + const thumbHolder = document.createElement('div') + thumbHolder.setAttribute('class', 'vjs-vtt-thumbnail-display') + this.progressBar.appendChild(thumbHolder) + this.thumbnailHolder = thumbHolder + mouseDisplay.classList.add('vjs-hidden') + this.progressBar.addEventListener('mouseenter', () => { return this.onBarMouseenter() }) + this.progressBar.addEventListener('mouseleave', () => { return this.onBarMouseleave() }) + } + + onBarMouseenter () { + this.mouseMoveCallback = (e) => { this.onBarMousemove(e) } + this.progressBar.addEventListener('mousemove', this.mouseMoveCallback) + this.showThumbnailHolder() + } + + onBarMouseleave () { + this.progressBar.removeEventListener('mousemove', this.mouseMoveCallback) + this.hideThumbnailHolder() + } + + onBarMousemove (event) { + this.updateThumbnailStyle( + event.clientX - this.progressBar.offsetLeft, + this.progressBar.offsetWidth + ) + } + + getStyleForTime (time) { + for (let i = 0; i < this.vttData.length; ++i) { + let item = this.vttData[i] + if (time >= item.start && time < item.end) { + return item.css + } + } + } + + showThumbnailHolder () { + this.thumbnailHolder.style.opacity = '1' + } + + hideThumbnailHolder () { + this.thumbnailHolder.style.opacity = '0' + } + + updateThumbnailStyle (x, width) { + const duration = this.player.duration() + const time = ((1 - ((width - x) / width))) * duration + const currentStyle = this.getStyleForTime(time) + if (!currentStyle) { + return this.hideThumbnailHolder() + } + const xPos = ((1 - ((width - x) / width))) * width + this.thumbnailHolder.style.transform = 'translateX(' + xPos + 'px)' + this.thumbnailHolder.style.marginLeft = '-' + (parseInt(currentStyle.width) / 2) + 'px' + + if (this.lastStyle && this.lastStyle === currentStyle) { + return + } + this.lastStyle = currentStyle + + for (let style in currentStyle) { + if (currentStyle.hasOwnProperty(style)) { + this.thumbnailHolder.style[style] = currentStyle[style] + } + } + } + + processVtt (data) { + const processedVtts = [] + const vttDefinitions = data.split(/[\r\n][\r\n]/i) + vttDefinitions.forEach((vttDef) => { + if (vttDef.match(/([0-9]{2}:)?([0-9]{2}:)?[0-9]{2}(.[0-9]{3})?( ?--> ?)([0-9]{2}:)?([0-9]{2}:)?[0-9]{2}(.[0-9]{3})?[\r\n]{1}.*/gi)) { + let vttDefSplit = vttDef.split(/[\r\n]/i) + let vttTiming = vttDefSplit[0] + let vttTimingSplit = vttTiming.split(/ ?--> ?/i) + let vttTimeStart = vttTimingSplit[0] + let vttTimeEnd = vttTimingSplit[1] + let vttImageDef = vttDefSplit[1] + let vttCssDef = this.getVttCss(vttImageDef) + + processedVtts.push({ + start: this.getSecondsFromTimestamp(vttTimeStart), + end: this.getSecondsFromTimestamp(vttTimeEnd), + css: vttCssDef + }) + + } + }) + return processedVtts + } + + getFullyQualifiedUrl (path, base) { + if (!path.match(/\/\//i)) { + return [ + this.trim(base, '/'), + this.trim(path, '/') + ].join('/') + } + return path + } + + getPropsFromDef (def) { + const imageDefSplit = def.split(/#xywh=/i) + const imageUrl = imageDefSplit[0] + const imageCoords = imageDefSplit[1] + const splitCoords = imageCoords.match(/[0-9]+/gi) + return { + x: splitCoords[0], + y: splitCoords[1], + w: splitCoords[2], + h: splitCoords[3], + image: imageUrl + } + } + + getVttCss (vttImageDef) { + + const cssObj = {} + + // If there isn't a protocol, use the VTT source URL. + const baseSplit = this.options.src.split(/([^\/]*)$/gi) + vttImageDef = this.getFullyQualifiedUrl(vttImageDef, baseSplit[0]) + + if (!vttImageDef.match(/#xywh=/i)) { + cssObj.background = 'url("' + vttImageDef + '")' + return cssObj + } + + const imageProps = this.getPropsFromDef(vttImageDef) + cssObj.background = 'url("' + imageProps.image + '") no-repeat -' + imageProps.x + 'px -' + imageProps.y + 'px' + cssObj.width = imageProps.w + 'px' + cssObj.height = imageProps.h + 'px' + + return cssObj + } + + doconstructTimestamp (timestamp) { + const splitStampMilliseconds = timestamp.split('.') + const timeParts = splitStampMilliseconds[0] + const timePartsSplit = timeParts.split(':') + return { + milliseconds: parseInt(splitStampMilliseconds[1]) || 0, + seconds: parseInt(timePartsSplit.pop()) || 0, + minutes: parseInt(timePartsSplit.pop()) || 0, + hours: parseInt(timePartsSplit.pop()) || 0, + } + + } + + getSecondsFromTimestamp (timestamp) { + const timestampParts = this.doconstructTimestamp(timestamp) + return parseInt((timestampParts.hours * (60 * 60)) + + (timestampParts.minutes * 60) + + timestampParts.seconds + + (timestampParts.milliseconds * 1000)) + } + + trim (str, charlist) { + let whitespace = [ + ' ', + '\n', + '\r', + '\t', + '\f', + '\x0b', + '\xa0', + '\u2000', + '\u2001', + '\u2002', + '\u2003', + '\u2004', + '\u2005', + '\u2006', + '\u2007', + '\u2008', + '\u2009', + '\u200a', + '\u200b', + '\u2028', + '\u2029', + '\u3000' + ].join('') + let l = 0 + let i = 0 + str += '' + if (charlist) { + whitespace = (charlist + '').replace(/([[\]().?/*{}+$^:])/g, '$1') + } + l = str.length + for (i = 0; i < l; i++) { + if (whitespace.indexOf(str.charAt(i)) === -1) { + str = str.substring(i) + break + } + } + l = str.length + for (i = l - 1; i >= 0; i--) { + if (whitespace.indexOf(str.charAt(i)) === -1) { + str = str.substring(0, i + 1) + break + } + } + return whitespace.indexOf(str.charAt(0)) === -1 ? str : '' + } + +} + +// Register the plugin with video.js. +registerPlugin('vttThumbnails', vttThumbnails) + +// Include the version number. +vttThumbnails.VERSION = VERSION + +export default vttThumbnails diff --git a/src/plugin.scss b/src/plugin.scss new file mode 100644 index 0000000..2e49816 --- /dev/null +++ b/src/plugin.scss @@ -0,0 +1,18 @@ +// Sass for videojs-vtt-thumbnails + +.video-js { + + // This class is added to the video.js element by the plugin by default. + &.vjs-vtt-thumbnails { + display: block; + } + + .vjs-vtt-thumbnail-display { + position: absolute; + transition: transform .1s, opacity .2s; + bottom: 85%; + pointer-events: none; + box-shadow: 0 0 7px rgba(0,0,0,.6); + } + +} diff --git a/test/index.html b/test/index.html new file mode 100644 index 0000000..86b9a01 --- /dev/null +++ b/test/index.html @@ -0,0 +1,19 @@ + + + + + videojs-vtt-thumbnails Unit Tests + + + + + +
+
+ + + + + + + diff --git a/test/karma.conf.js b/test/karma.conf.js new file mode 100644 index 0000000..3e3c0d2 --- /dev/null +++ b/test/karma.conf.js @@ -0,0 +1,43 @@ +module.exports = function(config) { + var detectBrowsers = { + enabled: false, + usePhantomJS: false + }; + + // On Travis CI, we can only run in Firefox and Chrome; so, enforce that. + if (process.env.TRAVIS) { + config.browsers = ['Firefox', 'travisChrome']; + } + + // If no browsers are specified, we enable `karma-detect-browsers` + // this will detect all browsers that are available for testing + if (!config.browsers.length) { + detectBrowsers.enabled = true; + } + + config.set({ + basePath: '..', + frameworks: ['qunit', 'detectBrowsers'], + files: [ + 'node_modules/video.js/dist/video-js.css', + 'dist/videojs-vtt-thumbnails.css', + + 'node_modules/sinon/pkg/sinon.js', + 'node_modules/video.js/dist/video.js', + 'test/dist/bundle.js' + ], + customLaunchers: { + travisChrome: { + base: 'Chrome', + flags: ['--no-sandbox'] + } + }, + detectBrowsers: detectBrowsers, + reporters: ['dots'], + port: 9876, + colors: true, + autoWatch: false, + singleRun: true, + concurrency: Infinity + }); +}; diff --git a/test/plugin.test.js b/test/plugin.test.js new file mode 100644 index 0000000..32a4790 --- /dev/null +++ b/test/plugin.test.js @@ -0,0 +1,58 @@ +import document from 'global/document'; + +import QUnit from 'qunit'; +import sinon from 'sinon'; +import videojs from 'video.js'; + +import plugin from '../src/plugin'; + +const Player = videojs.getComponent('Player'); + +QUnit.test('the environment is sane', function(assert) { + assert.strictEqual(typeof Array.isArray, 'function', 'es5 exists'); + assert.strictEqual(typeof sinon, 'object', 'sinon exists'); + assert.strictEqual(typeof videojs, 'function', 'videojs exists'); + assert.strictEqual(typeof plugin, 'function', 'plugin is a function'); +}); + +QUnit.module('videojs-vtt-thumbnails', { + + beforeEach() { + + // Mock the environment's timers because certain things - particularly + // player readiness - are asynchronous in video.js 5. This MUST come + // before any player is created; otherwise, timers could get created + // with the actual timer methods! + this.clock = sinon.useFakeTimers(); + + this.fixture = document.getElementById('qunit-fixture'); + this.video = document.createElement('video'); + this.fixture.appendChild(this.video); + this.player = videojs(this.video); + }, + + afterEach() { + this.player.dispose(); + this.clock.restore(); + } +}); + +QUnit.test('registers itself with video.js', function(assert) { + assert.expect(2); + + assert.strictEqual( + typeof Player.prototype.vttThumbnails, + 'function', + 'videojs-vtt-thumbnails plugin was registered' + ); + + this.player.vttThumbnails(); + + // Tick the clock forward enough to trigger the player to be "ready". + this.clock.tick(2); + + assert.ok( + this.player.hasClass('vjs-vtt-thumbnails'), + 'the plugin adds a class to the player' + ); +});